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:
@@ -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,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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user