diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java index 5c527975acd..b030219d7a8 100644 --- a/src/com/android/settings/datausage/DataUsageList.java +++ b/src/com/android/settings/datausage/DataUsageList.java @@ -14,32 +14,23 @@ package com.android.settings.datausage; -import static android.app.usage.NetworkStats.Bucket.UID_REMOVED; -import static android.app.usage.NetworkStats.Bucket.UID_TETHERING; -import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; - import android.app.Activity; import android.app.ActivityManager; import android.app.settings.SettingsEnums; import android.app.usage.NetworkStats; -import android.app.usage.NetworkStats.Bucket; import android.content.Context; import android.content.Intent; -import android.content.pm.UserInfo; import android.graphics.Color; import android.net.ConnectivityManager; import android.net.NetworkPolicy; import android.net.NetworkTemplate; import android.os.Bundle; -import android.os.Process; -import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.util.EventLog; import android.util.Log; -import android.util.SparseArray; import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.accessibility.AccessibilityEvent; @@ -60,6 +51,7 @@ import androidx.preference.PreferenceGroup; import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.datausage.CycleAdapter.SpinnerInterface; +import com.android.settings.datausage.lib.AppDataUsageRepository; import com.android.settings.network.MobileDataEnabledListener; import com.android.settings.network.MobileNetworkRepository; import com.android.settings.network.ProxySubscriptionManager; @@ -69,13 +61,10 @@ import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity; import com.android.settingslib.net.NetworkCycleChartData; import com.android.settingslib.net.NetworkCycleChartDataLoader; import com.android.settingslib.net.NetworkStatsSummaryLoader; -import com.android.settingslib.net.UidDetail; import com.android.settingslib.net.UidDetailProvider; import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -423,110 +412,19 @@ public class DataUsageList extends DataUsageBaseFragment } /** - * Bind the given {@link NetworkStats}, or {@code null} to clear list. + * Bind the given buckets. */ - private void bindStats(NetworkStats stats, int[] restrictedUids) { + private void bindStats(List buckets) { mApps.removeAll(); - if (stats == null) { - if (LOGD) { - Log.d(TAG, "No network stats data. App list cleared."); - } - return; - } - - final ArrayList items = new ArrayList<>(); - long largest = 0; - - final int currentUserId = ActivityManager.getCurrentUser(); - final UserManager userManager = UserManager.get(getContext()); - final List profiles = userManager.getUserProfiles(); - final SparseArray knownItems = new SparseArray(); - - final Bucket bucket = new Bucket(); - while (stats.hasNextBucket() && stats.getNextBucket(bucket)) { - // Decide how to collapse items together - final int uid = bucket.getUid(); - final int collapseKey; - final int category; - final int userId = UserHandle.getUserId(uid); - if (UserHandle.isApp(uid) || Process.isSdkSandboxUid(uid)) { - if (profiles.contains(new UserHandle(userId))) { - if (userId != currentUserId) { - // Add to a managed user item. - final int managedKey = UidDetailProvider.buildKeyForUser(userId); - largest = accumulate(managedKey, knownItems, bucket, - AppItem.CATEGORY_USER, items, largest); - } - // Map SDK sandbox back to its corresponding app - if (Process.isSdkSandboxUid(uid)) { - collapseKey = Process.getAppUidForSdkSandboxUid(uid); - } else { - collapseKey = uid; - } - category = AppItem.CATEGORY_APP; - } else { - // If it is a removed user add it to the removed users' key - final UserInfo info = userManager.getUserInfo(userId); - if (info == null) { - collapseKey = UID_REMOVED; - category = AppItem.CATEGORY_APP; - } else { - // Add to other user item. - collapseKey = UidDetailProvider.buildKeyForUser(userId); - category = AppItem.CATEGORY_USER; - } - } - } else if (uid == UID_REMOVED || uid == UID_TETHERING - || uid == Process.OTA_UPDATE_UID) { - collapseKey = uid; - category = AppItem.CATEGORY_APP; - } else { - collapseKey = android.os.Process.SYSTEM_UID; - category = AppItem.CATEGORY_APP; - } - largest = accumulate(collapseKey, knownItems, bucket, category, items, largest); - } - stats.close(); - - for (final int uid : restrictedUids) { - // Only splice in restricted state for current user or managed users - if (!profiles.contains(UserHandle.getUserHandleForUid(uid))) { - continue; - } - - AppItem item = knownItems.get(uid); - if (item == null) { - item = new AppItem(uid); - item.total = -1; - item.addUid(uid); - items.add(item); - knownItems.put(item.key, item); - } - item.restricted = true; - } - - Collections.sort(items); - final List packageNames = Arrays.asList(getContext().getResources().getStringArray( - R.array.datausage_hiding_carrier_service_package_names)); - // When there is no specified SubscriptionInfo, Wi-Fi data usage will be displayed. - // In this case, the carrier service package also needs to be hidden. - boolean shouldHidePackageName = mSubscriptionInfoEntity == null - || Arrays.stream(getContext().getResources().getIntArray( - R.array.datausage_hiding_carrier_service_carrier_id)) - .anyMatch(carrierId -> (carrierId == mSubscriptionInfoEntity.carrierId)); - - for (var item : items) { - UidDetail detail = mUidDetailProvider.getUidDetail(item.key, true); - // Do not show carrier service package in data usage list if it should be hidden for - // the carrier. - if (detail != null && shouldHidePackageName && packageNames.contains( - detail.packageName)) { - continue; - } - - final int percentTotal = largest != 0 ? (int) (item.total * 100 / largest) : 0; + AppDataUsageRepository repository = new AppDataUsageRepository( + requireContext(), + ActivityManager.getCurrentUser(), + mSubscriptionInfoEntity == null ? null : mSubscriptionInfoEntity.carrierId, + appItem -> mUidDetailProvider.getUidDetail(appItem.key, true).packageName + ); + for (var itemPercentPair : repository.getAppPercent(buckets)) { final AppDataUsagePreference preference = new AppDataUsagePreference(getContext(), - item, percentTotal, mUidDetailProvider); + itemPercentPair.getFirst(), itemPercentPair.getSecond(), mUidDetailProvider); preference.setOnPreferenceClickListener(p -> { AppDataUsagePreference pref = (AppDataUsagePreference) p; startAppDataUsage(pref.getItem()); @@ -565,30 +463,6 @@ public class DataUsageList extends DataUsageBaseFragment .launch(); } - /** - * Accumulate data usage of a network stats entry for the item mapped by the collapse key. - * Creates the item if needed. - * - * @param collapseKey the collapse key used to map the item. - * @param knownItems collection of known (already existing) items. - * @param bucket the network stats bucket to extract data usage from. - * @param itemCategory the item is categorized on the list view by this category. Must be - */ - private static long accumulate(int collapseKey, final SparseArray knownItems, - Bucket bucket, int itemCategory, ArrayList items, long largest) { - final int uid = bucket.getUid(); - AppItem item = knownItems.get(collapseKey); - if (item == null) { - item = new AppItem(collapseKey); - item.category = itemCategory; - items.add(item); - knownItems.put(item.key, item); - } - item.addUid(uid); - item.total += bucket.getRxBytes() + bucket.getTxBytes(); - return Math.max(largest, item.total); - } - private final OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { @@ -643,15 +517,13 @@ public class DataUsageList extends DataUsageBaseFragment @Override public void onLoadFinished( @NonNull Loader loader, NetworkStats data) { - final int[] restrictedUids = services.mPolicyManager.getUidsWithPolicy( - POLICY_REJECT_METERED_BACKGROUND); - bindStats(data, restrictedUids); + bindStats(AppDataUsageRepository.Companion.convertToBuckets(data)); updateEmptyVisible(); } @Override public void onLoaderReset(@NonNull Loader loader) { - bindStats(null, new int[0]); + mApps.removeAll(); updateEmptyVisible(); } diff --git a/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt b/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt new file mode 100644 index 00000000000..3813af5d200 --- /dev/null +++ b/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datausage.lib + +import android.app.usage.NetworkStats +import android.content.Context +import android.net.NetworkPolicyManager +import android.os.Process +import android.os.UserHandle +import android.util.SparseArray +import com.android.settings.R +import com.android.settingslib.AppItem +import com.android.settingslib.net.UidDetailProvider +import com.android.settingslib.spaprivileged.framework.common.userManager + +class AppDataUsageRepository( + private val context: Context, + private val currentUserId: Int, + private val carrierId: Int?, + private val getPackageName: (AppItem) -> String, +) { + data class Bucket( + val uid: Int, + val bytes: Long, + ) + + fun getAppPercent(buckets: List): List> { + val items = ArrayList() + val knownItems = SparseArray() + val profiles = context.userManager.userProfiles + bindStats(buckets, profiles, knownItems, items) + val restrictedUids = context.getSystemService(NetworkPolicyManager::class.java)!! + .getUidsWithPolicy(NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND) + for (uid in restrictedUids) { + // Only splice in restricted state for current user or managed users + if (!profiles.contains(UserHandle.getUserHandleForUid(uid))) { + continue + } + var item = knownItems[uid] + if (item == null) { + item = AppItem(uid) + item.total = 0 + item.addUid(uid) + items.add(item) + knownItems.put(item.key, item) + } + item.restricted = true + } + + val filteredItems = filterItems(items).sorted() + val largest: Long = filteredItems.maxOfOrNull { it.total } ?: 0 + return filteredItems.map { item -> + val percentTotal = if (largest > 0) (item.total * 100 / largest).toInt() else 0 + item to percentTotal + } + } + + private fun filterItems(items: List): List { + // When there is no specified SubscriptionInfo, Wi-Fi data usage will be displayed. + // In this case, the carrier service package also needs to be hidden. + if (carrierId != null && carrierId !in context.resources.getIntArray( + R.array.datausage_hiding_carrier_service_carrier_id + ) + ) { + return items + } + val hiddenPackageNames = context.resources.getStringArray( + R.array.datausage_hiding_carrier_service_package_names + ) + return items.filter { item -> + // Do not show carrier service package in data usage list if it should be hidden for + // the carrier. + getPackageName(item) !in hiddenPackageNames + } + } + + private fun bindStats( + buckets: List, + profiles: MutableList, + knownItems: SparseArray, + items: ArrayList, + ) { + for (bucket in buckets) { + // Decide how to collapse items together + val uid = bucket.uid + val collapseKey: Int + val category: Int + val userId = UserHandle.getUserId(uid) + if (UserHandle.isApp(uid) || Process.isSdkSandboxUid(uid)) { + if (profiles.contains(UserHandle(userId))) { + if (userId != currentUserId) { + // Add to a managed user item. + accumulate( + collapseKey = UidDetailProvider.buildKeyForUser(userId), + knownItems = knownItems, + bucket = bucket, + itemCategory = AppItem.CATEGORY_USER, + items = items, + ) + } + // Map SDK sandbox back to its corresponding app + collapseKey = if (Process.isSdkSandboxUid(uid)) { + Process.getAppUidForSdkSandboxUid(uid) + } else { + uid + } + category = AppItem.CATEGORY_APP + } else { + // If it is a removed user add it to the removed users' key + if (context.userManager.getUserInfo(userId) == null) { + collapseKey = NetworkStats.Bucket.UID_REMOVED + category = AppItem.CATEGORY_APP + } else { + // Add to other user item. + collapseKey = UidDetailProvider.buildKeyForUser(userId) + category = AppItem.CATEGORY_USER + } + } + } else if (uid == NetworkStats.Bucket.UID_REMOVED || + uid == NetworkStats.Bucket.UID_TETHERING || + uid == Process.OTA_UPDATE_UID + ) { + collapseKey = uid + category = AppItem.CATEGORY_APP + } else { + collapseKey = Process.SYSTEM_UID + category = AppItem.CATEGORY_APP + } + accumulate( + collapseKey = collapseKey, + knownItems = knownItems, + bucket = bucket, + itemCategory = category, + items = items, + ) + } + } + + /** + * Accumulate data usage of a network stats entry for the item mapped by the collapse key. + * Creates the item if needed. + * + * @param collapseKey the collapse key used to map the item. + * @param knownItems collection of known (already existing) items. + * @param bucket the network stats bucket to extract data usage from. + * @param itemCategory the item is categorized on the list view by this category. Must be + */ + private fun accumulate( + collapseKey: Int, + knownItems: SparseArray, + bucket: Bucket, + itemCategory: Int, + items: ArrayList, + ) { + var item = knownItems[collapseKey] + if (item == null) { + item = AppItem(collapseKey) + item.category = itemCategory + items.add(item) + knownItems.put(item.key, item) + } + item.addUid(bucket.uid) + item.total += bucket.bytes + } + + companion object { + fun convertToBuckets(stats: NetworkStats): List { + val buckets = mutableListOf() + stats.use { + val bucket = NetworkStats.Bucket() + while (it.getNextBucket(bucket)) { + buckets += Bucket(uid = bucket.uid, bytes = bucket.rxBytes + bucket.txBytes) + } + } + return buckets + } + } +} diff --git a/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageRepositoryTest.kt new file mode 100644 index 00000000000..016d6d2e484 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageRepositoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datausage.lib + +import android.content.Context +import android.content.pm.UserInfo +import android.content.res.Resources +import android.net.NetworkPolicyManager +import android.os.UserHandle +import android.os.UserManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.datausage.lib.AppDataUsageRepository.Bucket +import com.android.settingslib.AppItem +import com.android.settingslib.spaprivileged.framework.common.userManager +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy + +@RunWith(AndroidJUnit4::class) +class AppDataUsageRepositoryTest { + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + private val mockUserManager = mock { + on { userProfiles } doReturn listOf(UserHandle.of(USER_ID)) + on { getUserInfo(USER_ID) } doReturn UserInfo(USER_ID, "", 0) + } + + private val mockNetworkPolicyManager = mock { + on { getUidsWithPolicy(NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND) } doReturn + intArrayOf() + } + + private val mockResources = mock { + on { getIntArray(R.array.datausage_hiding_carrier_service_carrier_id) } doReturn + intArrayOf(HIDING_CARRIER_ID) + + on { getStringArray(R.array.datausage_hiding_carrier_service_package_names) } doReturn + arrayOf(HIDING_PACKAGE_NAME) + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { userManager } doReturn mockUserManager + on { getSystemService(NetworkPolicyManager::class.java) } doReturn mockNetworkPolicyManager + on { resources } doReturn mockResources + } + + @Test + fun getAppPercent_noAppToHide() { + val repository = AppDataUsageRepository( + context = context, + currentUserId = USER_ID, + carrierId = null, + getPackageName = { "" }, + ) + val buckets = listOf( + Bucket(uid = APP_ID_1, bytes = 1), + Bucket(uid = APP_ID_2, bytes = 2), + ) + + val appPercentList = repository.getAppPercent(buckets) + + assertThat(appPercentList).hasSize(2) + appPercentList[0].first.apply { + assertThat(key).isEqualTo(APP_ID_2) + assertThat(category).isEqualTo(AppItem.CATEGORY_APP) + assertThat(total).isEqualTo(2) + } + assertThat(appPercentList[0].second).isEqualTo(100) + appPercentList[1].first.apply { + assertThat(key).isEqualTo(APP_ID_1) + assertThat(category).isEqualTo(AppItem.CATEGORY_APP) + assertThat(total).isEqualTo(1) + } + assertThat(appPercentList[1].second).isEqualTo(50) + } + + @Test + fun getAppPercent_hasAppToHide() { + val repository = AppDataUsageRepository( + context = context, + currentUserId = USER_ID, + carrierId = HIDING_CARRIER_ID, + getPackageName = { if (it.key == APP_ID_1) HIDING_PACKAGE_NAME else "" }, + ) + val buckets = listOf( + Bucket(uid = APP_ID_1, bytes = 1), + Bucket(uid = APP_ID_2, bytes = 2), + ) + + val appPercentList = repository.getAppPercent(buckets) + + assertThat(appPercentList).hasSize(1) + appPercentList[0].first.apply { + assertThat(key).isEqualTo(APP_ID_2) + assertThat(category).isEqualTo(AppItem.CATEGORY_APP) + assertThat(total).isEqualTo(2) + } + assertThat(appPercentList[0].second).isEqualTo(100) + } + + private companion object { + const val USER_ID = 1 + const val APP_ID_1 = 110001 + const val APP_ID_2 = 110002 + const val HIDING_CARRIER_ID = 4 + const val HIDING_PACKAGE_NAME = "hiding.package.name" + } +}