Loading src/com/android/settings/datausage/DataUsageList.java +13 −141 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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<AppDataUsageRepository.Bucket> buckets) { mApps.removeAll(); if (stats == null) { if (LOGD) { Log.d(TAG, "No network stats data. App list cleared."); } return; } final ArrayList<AppItem> items = new ArrayList<>(); long largest = 0; final int currentUserId = ActivityManager.getCurrentUser(); final UserManager userManager = UserManager.get(getContext()); final List<UserHandle> profiles = userManager.getUserProfiles(); final SparseArray<AppItem> knownItems = new SparseArray<AppItem>(); 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<String> 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()); Loading Loading @@ -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<AppItem> knownItems, Bucket bucket, int itemCategory, ArrayList<AppItem> 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) { Loading Loading @@ -643,15 +517,13 @@ public class DataUsageList extends DataUsageBaseFragment @Override public void onLoadFinished( @NonNull Loader<NetworkStats> 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<NetworkStats> loader) { bindStats(null, new int[0]); mApps.removeAll(); updateEmptyVisible(); } Loading src/com/android/settings/datausage/lib/AppDataUsageRepository.kt 0 → 100644 +192 −0 Original line number Diff line number Diff line /* * 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<Bucket>): List<Pair<AppItem, Int>> { val items = ArrayList<AppItem>() val knownItems = SparseArray<AppItem>() 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<AppItem>): List<AppItem> { // 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<Bucket>, profiles: MutableList<UserHandle>, knownItems: SparseArray<AppItem>, items: ArrayList<AppItem>, ) { 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<AppItem>, bucket: Bucket, itemCategory: Int, items: ArrayList<AppItem>, ) { 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<Bucket> { val buckets = mutableListOf<Bucket>() stats.use { val bucket = NetworkStats.Bucket() while (it.getNextBucket(bucket)) { buckets += Bucket(uid = bucket.uid, bytes = bucket.rxBytes + bucket.txBytes) } } return buckets } } } tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageRepositoryTest.kt 0 → 100644 +131 −0 Original line number Diff line number Diff line /* * 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<UserManager> { on { userProfiles } doReturn listOf(UserHandle.of(USER_ID)) on { getUserInfo(USER_ID) } doReturn UserInfo(USER_ID, "", 0) } private val mockNetworkPolicyManager = mock<NetworkPolicyManager> { on { getUidsWithPolicy(NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND) } doReturn intArrayOf() } private val mockResources = mock<Resources> { 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" } } Loading
src/com/android/settings/datausage/DataUsageList.java +13 −141 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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<AppDataUsageRepository.Bucket> buckets) { mApps.removeAll(); if (stats == null) { if (LOGD) { Log.d(TAG, "No network stats data. App list cleared."); } return; } final ArrayList<AppItem> items = new ArrayList<>(); long largest = 0; final int currentUserId = ActivityManager.getCurrentUser(); final UserManager userManager = UserManager.get(getContext()); final List<UserHandle> profiles = userManager.getUserProfiles(); final SparseArray<AppItem> knownItems = new SparseArray<AppItem>(); 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<String> 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()); Loading Loading @@ -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<AppItem> knownItems, Bucket bucket, int itemCategory, ArrayList<AppItem> 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) { Loading Loading @@ -643,15 +517,13 @@ public class DataUsageList extends DataUsageBaseFragment @Override public void onLoadFinished( @NonNull Loader<NetworkStats> 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<NetworkStats> loader) { bindStats(null, new int[0]); mApps.removeAll(); updateEmptyVisible(); } Loading
src/com/android/settings/datausage/lib/AppDataUsageRepository.kt 0 → 100644 +192 −0 Original line number Diff line number Diff line /* * 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<Bucket>): List<Pair<AppItem, Int>> { val items = ArrayList<AppItem>() val knownItems = SparseArray<AppItem>() 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<AppItem>): List<AppItem> { // 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<Bucket>, profiles: MutableList<UserHandle>, knownItems: SparseArray<AppItem>, items: ArrayList<AppItem>, ) { 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<AppItem>, bucket: Bucket, itemCategory: Int, items: ArrayList<AppItem>, ) { 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<Bucket> { val buckets = mutableListOf<Bucket>() stats.use { val bucket = NetworkStats.Bucket() while (it.getNextBucket(bucket)) { buckets += Bucket(uid = bucket.uid, bytes = bucket.rxBytes + bucket.txBytes) } } return buckets } } }
tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageRepositoryTest.kt 0 → 100644 +131 −0 Original line number Diff line number Diff line /* * 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<UserManager> { on { userProfiles } doReturn listOf(UserHandle.of(USER_ID)) on { getUserInfo(USER_ID) } doReturn UserInfo(USER_ID, "", 0) } private val mockNetworkPolicyManager = mock<NetworkPolicyManager> { on { getUidsWithPolicy(NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND) } doReturn intArrayOf() } private val mockResources = mock<Resources> { 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" } }