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

Commit d8356491 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "Extract app data usage to AppDataUsageRepository" into main

parents fc7a1e01 c31474c0
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"
    }
}