Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c31474c0 authored by Chaohui Wang's avatar Chaohui Wang
Browse files

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
parent 2c114331
Loading
Loading
Loading
Loading
+13 −141
Original line number Diff line number Diff line
@@ -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<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 @@ 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) {
@@ -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();
                }

+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
        }
    }
}
+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"
    }
}