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
This commit is contained in:
Chaohui Wang
2023-09-17 07:25:06 +08:00
parent 089318d92f
commit 0cb8d91e4e
8 changed files with 252 additions and 177 deletions

View File

@@ -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;

View File

@@ -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,67 +383,18 @@ 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;
? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0;
final CharSequence totalPhrase = DataUsageUtils.formatDataUsage(getActivity(), totalBytes);
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);

View File

@@ -0,0 +1,114 @@
/*
* 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()
}
}

View File

@@ -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()