Extract app data usage to AppDataUsageRepository
From DataUsageList for better organization and testing.
Bug: 290856342
Test: manual - on DataUsageList
Test: unit test
Change-Id: I97e327a220d40942b9345ec7f1f8c466ac1fc9da
diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java
index 5c52797..b030219 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 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.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 @@
}
/**
- * 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());
@@ -565,30 +463,6 @@
.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) {
@@ -643,15 +517,13 @@
@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();
}
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 0000000..3813af5
--- /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<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
+ }
+ }
+}
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 0000000..016d6d2
--- /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<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"
+ }
+}