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

Commit 0cb8d91e authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Create DataUsageListAppsController

Move apps group logic from DataUsageList.

Also add key to AppDataUsagePreference, which reduce flaky and keep
scroll position when back from app detail page.

Bug: 290856342
Test: manual - on DataUsageList
Test: unit test
Change-Id: I61e2b6bd9b192b7230e3553dbc6038f5d59bd303
parent 089318d9
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -14,7 +14,8 @@
     limitations under the License.
-->

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory
        android:key="usage_amount"
@@ -32,6 +33,7 @@

    <PreferenceCategory
        android:key="apps_group"
        android:layout="@layout/preference_category_no_label" />
        android:layout="@layout/preference_category_no_label"
        settings:controller="com.android.settings.datausage.DataUsageListAppsController" />

</PreferenceScreen>
+1 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ public class AppDataUsagePreference extends AppPreference {
    public AppDataUsagePreference(Context context, AppItem item, int percent,
            UidDetailProvider provider) {
        super(context);
        setKey("app_data_usage_" + item.key);
        mItem = item;
        mPercent = percent;

+15 −121
Original line number Diff line number Diff line
@@ -15,9 +15,7 @@
package com.android.settings.datausage;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.settings.SettingsEnums;
import android.app.usage.NetworkStats;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
@@ -46,25 +44,19 @@ import androidx.lifecycle.Lifecycle;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import androidx.preference.Preference;
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;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.AppItem;
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.UidDetailProvider;
import com.android.settingslib.utils.ThreadUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -85,14 +77,11 @@ public class DataUsageList extends DataUsageBaseFragment

    private static final String KEY_USAGE_AMOUNT = "usage_amount";
    private static final String KEY_CHART_DATA = "chart_data";
    private static final String KEY_APPS_GROUP = "apps_group";
    private static final String KEY_TEMPLATE = "template";
    private static final String KEY_APP = "app";

    @VisibleForTesting
    static final int LOADER_CHART_DATA = 2;
    @VisibleForTesting
    static final int LOADER_SUMMARY = 3;

    @VisibleForTesting
    MobileDataEnabledListener mDataStateListener;
@@ -113,18 +102,15 @@ public class DataUsageList extends DataUsageBaseFragment
    @Nullable
    private List<NetworkCycleChartData> mCycleData;

    // Caches the cycles for startAppDataUsage usage, which need be cleared when resumed.
    private ArrayList<Long> mCycles;
    // Spinner will keep the selected cycle even after paused, this only keeps the displayed cycle,
    // which need be cleared when resumed.
    private CycleAdapter.CycleItem mLastDisplayedCycle;
    private UidDetailProvider mUidDetailProvider;
    private CycleAdapter mCycleAdapter;
    private Preference mUsageAmount;
    private PreferenceGroup mApps;
    private View mHeader;
    private MobileNetworkRepository mMobileNetworkRepository;
    private SubscriptionInfoEntity mSubscriptionInfoEntity;
    private DataUsageListAppsController mDataUsageListAppsController;

    @Override
    public int getMetricsCategory() {
@@ -148,14 +134,19 @@ public class DataUsageList extends DataUsageBaseFragment
            return;
        }

        mUidDetailProvider = new UidDetailProvider(activity);
        mUsageAmount = findPreference(KEY_USAGE_AMOUNT);
        mChart = findPreference(KEY_CHART_DATA);
        mApps = findPreference(KEY_APPS_GROUP);

        processArgument();
        if (mTemplate == null) {
            Log.e(TAG, "No template; leaving");
            finish();
            return;
        }
        updateSubscriptionInfoEntity();
        mDataStateListener = new MobileDataEnabledListener(activity, this);
        mDataUsageListAppsController = use(DataUsageListAppsController.class);
        mDataUsageListAppsController.init(mTemplate);
    }

    @Override
@@ -216,7 +207,6 @@ public class DataUsageList extends DataUsageBaseFragment
        super.onResume();
        mLoadingViewController.showLoadingViewDelayed();
        mDataStateListener.start(mSubId);
        mCycles = null;
        mLastDisplayedCycle = null;

        // kick off loader for network history
@@ -234,16 +224,6 @@ public class DataUsageList extends DataUsageBaseFragment
        mDataStateListener.stop();

        getLoaderManager().destroyLoader(LOADER_CHART_DATA);
        getLoaderManager().destroyLoader(LOADER_SUMMARY);
    }

    @Override
    public void onDestroy() {
        if (mUidDetailProvider != null) {
            mUidDetailProvider.clearCache();
            mUidDetailProvider = null;
        }
        super.onDestroy();
    }

    @Override
@@ -352,6 +332,7 @@ public class DataUsageList extends DataUsageBaseFragment
        if (mCycleData != null) {
            mCycleAdapter.updateCycleList(mCycleData);
        }
        mDataUsageListAppsController.setCycleData(mCycleData);
        updateSelectedCycle();
    }

@@ -402,8 +383,11 @@ public class DataUsageList extends DataUsageBaseFragment
        if (LOGD) Log.d(TAG, "updateDetailData()");

        // kick off loader for detailed stats
        getLoaderManager().restartLoader(LOADER_SUMMARY, null /* args */,
                mNetworkStatsDetailCallbacks);
        mDataUsageListAppsController.update(
                mSubscriptionInfoEntity == null ? null : mSubscriptionInfoEntity.carrierId,
                mChart.getInspectStart(),
                mChart.getInspectEnd()
        );

        final long totalBytes = mCycleData != null && !mCycleData.isEmpty()
                ? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0;
@@ -411,58 +395,6 @@ public class DataUsageList extends DataUsageBaseFragment
        mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
    }

    /**
     * Bind the given buckets.
     */
    private void bindStats(List<AppDataUsageRepository.Bucket> buckets) {
        mApps.removeAll();
        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(),
                    itemPercentPair.getFirst(), itemPercentPair.getSecond(), mUidDetailProvider);
            preference.setOnPreferenceClickListener(p -> {
                AppDataUsagePreference pref = (AppDataUsagePreference) p;
                startAppDataUsage(pref.getItem());
                return true;
            });
            mApps.addPreference(preference);
        }
    }

    @VisibleForTesting
    void startAppDataUsage(AppItem item) {
        if (mCycleData == null) {
            return;
        }
        final Bundle args = new Bundle();
        args.putParcelable(AppDataUsage.ARG_APP_ITEM, item);
        args.putParcelable(AppDataUsage.ARG_NETWORK_TEMPLATE, mTemplate);
        if (mCycles == null) {
            mCycles = new ArrayList<>();
            for (NetworkCycleChartData data : mCycleData) {
                if (mCycles.isEmpty()) {
                    mCycles.add(data.getEndTime());
                }
                mCycles.add(data.getStartTime());
            }
        }
        args.putSerializable(AppDataUsage.ARG_NETWORK_CYCLES, mCycles);
        args.putLong(AppDataUsage.ARG_SELECTED_CYCLE,
            mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getEndTime());

        new SubSettingLauncher(getContext())
                .setDestination(AppDataUsage.class.getName())
                .setTitleRes(R.string.data_usage_app_summary_title)
                .setArguments(args)
                .setSourceMetricsCategory(getMetricsCategory())
                .launch();
    }

    private final OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
@@ -502,44 +434,6 @@ public class DataUsageList extends DataUsageBaseFragment
                }
            };

    private final LoaderCallbacks<NetworkStats> mNetworkStatsDetailCallbacks =
            new LoaderCallbacks<>() {
                @Override
                @NonNull
                public Loader<NetworkStats> onCreateLoader(int id, Bundle args) {
                    return new NetworkStatsSummaryLoader.Builder(getContext())
                            .setStartTime(mChart.getInspectStart())
                            .setEndTime(mChart.getInspectEnd())
                            .setNetworkTemplate(mTemplate)
                            .build();
                }

                @Override
                public void onLoadFinished(
                        @NonNull Loader<NetworkStats> loader, NetworkStats data) {
                    bindStats(AppDataUsageRepository.Companion.convertToBuckets(data));
                    updateEmptyVisible();
                }

                @Override
                public void onLoaderReset(@NonNull Loader<NetworkStats> loader) {
                    mApps.removeAll();
                    updateEmptyVisible();
                }

                private void updateEmptyVisible() {
                    if ((mApps.getPreferenceCount() != 0)
                            != (getPreferenceScreen().getPreferenceCount() != 0)) {
                        if (mApps.getPreferenceCount() != 0) {
                            getPreferenceScreen().addPreference(mUsageAmount);
                            getPreferenceScreen().addPreference(mApps);
                        } else {
                            getPreferenceScreen().removeAll();
                        }
                    }
                }
            };

    private static boolean isGuestUser(Context context) {
        if (context == null) return false;
        final UserManager userManager = context.getSystemService(UserManager.class);
+114 −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

import android.app.ActivityManager
import android.content.Context
import android.net.NetworkTemplate
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settings.core.BasePreferenceController
import com.android.settings.core.SubSettingLauncher
import com.android.settings.datausage.lib.AppDataUsageRepository
import com.android.settingslib.AppItem
import com.android.settingslib.net.NetworkCycleChartData
import com.android.settingslib.net.UidDetailProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class DataUsageListAppsController(context: Context, preferenceKey: String) :
    BasePreferenceController(context, preferenceKey) {

    private val uidDetailProvider = UidDetailProvider(context)
    private lateinit var template: NetworkTemplate
    private lateinit var repository: AppDataUsageRepository
    private lateinit var preference: PreferenceGroup
    private lateinit var lifecycleScope: LifecycleCoroutineScope

    private var cycleData: List<NetworkCycleChartData>? = null

    fun init(template: NetworkTemplate) {
        this.template = template
        repository = AppDataUsageRepository(
            context = mContext,
            currentUserId = ActivityManager.getCurrentUser(),
            template = template,
        ) { appItem: AppItem -> uidDetailProvider.getUidDetail(appItem.key, true).packageName }
    }

    override fun getAvailabilityStatus() = AVAILABLE

    override fun displayPreference(screen: PreferenceScreen) {
        super.displayPreference(screen)
        preference = screen.findPreference(preferenceKey)!!
    }

    override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
        lifecycleScope = viewLifecycleOwner.lifecycleScope
    }

    fun setCycleData(cycleData: List<NetworkCycleChartData>?) {
        this.cycleData = cycleData
    }

    fun update(carrierId: Int?, startTime: Long, endTime: Long) = lifecycleScope.launch {
        val apps = withContext(Dispatchers.Default) {
            repository.getAppPercent(carrierId, startTime, endTime).map { (appItem, percent) ->
                AppDataUsagePreference(mContext, appItem, percent, uidDetailProvider).apply {
                    setOnPreferenceClickListener {
                        startAppDataUsage(appItem, endTime)
                        true
                    }
                }
            }
        }
        preference.removeAll()
        for (app in apps) {
            preference.addPreference(app)
        }
    }

    @VisibleForTesting
    fun startAppDataUsage(item: AppItem, endTime: Long) {
        val cycleData = cycleData ?: return
        val args = Bundle().apply {
            putParcelable(AppDataUsage.ARG_APP_ITEM, item)
            putParcelable(AppDataUsage.ARG_NETWORK_TEMPLATE, template)
            val cycles = ArrayList<Long>().apply {
                for (data in cycleData) {
                    if (isEmpty()) add(data.endTime)
                    add(data.startTime)
                }
            }
            putSerializable(AppDataUsage.ARG_NETWORK_CYCLES, cycles)
            putLong(AppDataUsage.ARG_SELECTED_CYCLE, endTime)
        }
        SubSettingLauncher(mContext).apply {
            setDestination(AppDataUsage::class.java.name)
            setTitleRes(R.string.data_usage_app_summary_title)
            setArguments(args)
            setSourceMetricsCategory(metricsCategory)
        }.launch()
    }
}
+32 −10
Original line number Diff line number Diff line
@@ -17,11 +17,15 @@
package com.android.settings.datausage.lib

import android.app.usage.NetworkStats
import android.app.usage.NetworkStatsManager
import android.content.Context
import android.net.NetworkPolicyManager
import android.net.NetworkTemplate
import android.os.Process
import android.os.UserHandle
import android.util.Log
import android.util.SparseArray
import androidx.annotation.VisibleForTesting
import com.android.settings.R
import com.android.settingslib.AppItem
import com.android.settingslib.net.UidDetailProvider
@@ -30,15 +34,18 @@ 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,
    private val template: NetworkTemplate,
    private val getPackageName: (AppItem) -> String?,
) {
    data class Bucket(
        val uid: Int,
        val bytes: Long,
    )
    private val networkStatsManager = context.getSystemService(NetworkStatsManager::class.java)!!

    fun getAppPercent(buckets: List<Bucket>): List<Pair<AppItem, Int>> {
    fun getAppPercent(carrierId: Int?, startTime: Long, endTime: Long): List<Pair<AppItem, Int>> {
        val networkStats = querySummary(startTime, endTime) ?: return emptyList()
        return getAppPercent(carrierId, convertToBuckets(networkStats))
    }

    @VisibleForTesting
    fun getAppPercent(carrierId: Int?, buckets: List<Bucket>): List<Pair<AppItem, Int>> {
        val items = ArrayList<AppItem>()
        val knownItems = SparseArray<AppItem>()
        val profiles = context.userManager.userProfiles
@@ -61,7 +68,7 @@ class AppDataUsageRepository(
            item.restricted = true
        }

        val filteredItems = filterItems(items).sorted()
        val filteredItems = filterItems(carrierId, 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
@@ -69,7 +76,14 @@ class AppDataUsageRepository(
        }
    }

    private fun filterItems(items: List<AppItem>): List<AppItem> {
    private fun querySummary(startTime: Long, endTime: Long): NetworkStats? = try {
        networkStatsManager.querySummary(template, startTime, endTime)
    } catch (e: RuntimeException) {
        Log.e(TAG, "Exception querying network detail.", e)
        null
    }

    private fun filterItems(carrierId: Int?, 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(
@@ -178,7 +192,15 @@ class AppDataUsageRepository(
    }

    companion object {
        fun convertToBuckets(stats: NetworkStats): List<Bucket> {
        private const val TAG = "AppDataUsageRepository"

        @VisibleForTesting
        data class Bucket(
            val uid: Int,
            val bytes: Long,
        )

        private fun convertToBuckets(stats: NetworkStats): List<Bucket> {
            val buckets = mutableListOf<Bucket>()
            stats.use {
                val bucket = NetworkStats.Bucket()
Loading