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:
@@ -14,7 +14,8 @@
|
|||||||
limitations under the License.
|
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
|
<PreferenceCategory
|
||||||
android:key="usage_amount"
|
android:key="usage_amount"
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:key="apps_group"
|
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>
|
</PreferenceScreen>
|
||||||
|
@@ -38,6 +38,7 @@ public class AppDataUsagePreference extends AppPreference {
|
|||||||
public AppDataUsagePreference(Context context, AppItem item, int percent,
|
public AppDataUsagePreference(Context context, AppItem item, int percent,
|
||||||
UidDetailProvider provider) {
|
UidDetailProvider provider) {
|
||||||
super(context);
|
super(context);
|
||||||
|
setKey("app_data_usage_" + item.key);
|
||||||
mItem = item;
|
mItem = item;
|
||||||
mPercent = percent;
|
mPercent = percent;
|
||||||
|
|
||||||
|
@@ -15,9 +15,7 @@
|
|||||||
package com.android.settings.datausage;
|
package com.android.settings.datausage;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.app.settings.SettingsEnums;
|
import android.app.settings.SettingsEnums;
|
||||||
import android.app.usage.NetworkStats;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -46,25 +44,19 @@ import androidx.lifecycle.Lifecycle;
|
|||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceGroup;
|
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
import com.android.settings.core.SubSettingLauncher;
|
import com.android.settings.core.SubSettingLauncher;
|
||||||
import com.android.settings.datausage.CycleAdapter.SpinnerInterface;
|
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.MobileDataEnabledListener;
|
||||||
import com.android.settings.network.MobileNetworkRepository;
|
import com.android.settings.network.MobileNetworkRepository;
|
||||||
import com.android.settings.network.ProxySubscriptionManager;
|
import com.android.settings.network.ProxySubscriptionManager;
|
||||||
import com.android.settings.widget.LoadingViewController;
|
import com.android.settings.widget.LoadingViewController;
|
||||||
import com.android.settingslib.AppItem;
|
|
||||||
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
|
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
|
||||||
import com.android.settingslib.net.NetworkCycleChartData;
|
import com.android.settingslib.net.NetworkCycleChartData;
|
||||||
import com.android.settingslib.net.NetworkCycleChartDataLoader;
|
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 com.android.settingslib.utils.ThreadUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
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_USAGE_AMOUNT = "usage_amount";
|
||||||
private static final String KEY_CHART_DATA = "chart_data";
|
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_TEMPLATE = "template";
|
||||||
private static final String KEY_APP = "app";
|
private static final String KEY_APP = "app";
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int LOADER_CHART_DATA = 2;
|
static final int LOADER_CHART_DATA = 2;
|
||||||
@VisibleForTesting
|
|
||||||
static final int LOADER_SUMMARY = 3;
|
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
MobileDataEnabledListener mDataStateListener;
|
MobileDataEnabledListener mDataStateListener;
|
||||||
@@ -113,18 +102,15 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
@Nullable
|
@Nullable
|
||||||
private List<NetworkCycleChartData> mCycleData;
|
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,
|
// Spinner will keep the selected cycle even after paused, this only keeps the displayed cycle,
|
||||||
// which need be cleared when resumed.
|
// which need be cleared when resumed.
|
||||||
private CycleAdapter.CycleItem mLastDisplayedCycle;
|
private CycleAdapter.CycleItem mLastDisplayedCycle;
|
||||||
private UidDetailProvider mUidDetailProvider;
|
|
||||||
private CycleAdapter mCycleAdapter;
|
private CycleAdapter mCycleAdapter;
|
||||||
private Preference mUsageAmount;
|
private Preference mUsageAmount;
|
||||||
private PreferenceGroup mApps;
|
|
||||||
private View mHeader;
|
private View mHeader;
|
||||||
private MobileNetworkRepository mMobileNetworkRepository;
|
private MobileNetworkRepository mMobileNetworkRepository;
|
||||||
private SubscriptionInfoEntity mSubscriptionInfoEntity;
|
private SubscriptionInfoEntity mSubscriptionInfoEntity;
|
||||||
|
private DataUsageListAppsController mDataUsageListAppsController;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMetricsCategory() {
|
public int getMetricsCategory() {
|
||||||
@@ -148,14 +134,19 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mUidDetailProvider = new UidDetailProvider(activity);
|
|
||||||
mUsageAmount = findPreference(KEY_USAGE_AMOUNT);
|
mUsageAmount = findPreference(KEY_USAGE_AMOUNT);
|
||||||
mChart = findPreference(KEY_CHART_DATA);
|
mChart = findPreference(KEY_CHART_DATA);
|
||||||
mApps = findPreference(KEY_APPS_GROUP);
|
|
||||||
|
|
||||||
processArgument();
|
processArgument();
|
||||||
|
if (mTemplate == null) {
|
||||||
|
Log.e(TAG, "No template; leaving");
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
updateSubscriptionInfoEntity();
|
updateSubscriptionInfoEntity();
|
||||||
mDataStateListener = new MobileDataEnabledListener(activity, this);
|
mDataStateListener = new MobileDataEnabledListener(activity, this);
|
||||||
|
mDataUsageListAppsController = use(DataUsageListAppsController.class);
|
||||||
|
mDataUsageListAppsController.init(mTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -216,7 +207,6 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
super.onResume();
|
super.onResume();
|
||||||
mLoadingViewController.showLoadingViewDelayed();
|
mLoadingViewController.showLoadingViewDelayed();
|
||||||
mDataStateListener.start(mSubId);
|
mDataStateListener.start(mSubId);
|
||||||
mCycles = null;
|
|
||||||
mLastDisplayedCycle = null;
|
mLastDisplayedCycle = null;
|
||||||
|
|
||||||
// kick off loader for network history
|
// kick off loader for network history
|
||||||
@@ -234,16 +224,6 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
mDataStateListener.stop();
|
mDataStateListener.stop();
|
||||||
|
|
||||||
getLoaderManager().destroyLoader(LOADER_CHART_DATA);
|
getLoaderManager().destroyLoader(LOADER_CHART_DATA);
|
||||||
getLoaderManager().destroyLoader(LOADER_SUMMARY);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (mUidDetailProvider != null) {
|
|
||||||
mUidDetailProvider.clearCache();
|
|
||||||
mUidDetailProvider = null;
|
|
||||||
}
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -352,6 +332,7 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
if (mCycleData != null) {
|
if (mCycleData != null) {
|
||||||
mCycleAdapter.updateCycleList(mCycleData);
|
mCycleAdapter.updateCycleList(mCycleData);
|
||||||
}
|
}
|
||||||
|
mDataUsageListAppsController.setCycleData(mCycleData);
|
||||||
updateSelectedCycle();
|
updateSelectedCycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,8 +383,11 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
if (LOGD) Log.d(TAG, "updateDetailData()");
|
if (LOGD) Log.d(TAG, "updateDetailData()");
|
||||||
|
|
||||||
// kick off loader for detailed stats
|
// kick off loader for detailed stats
|
||||||
getLoaderManager().restartLoader(LOADER_SUMMARY, null /* args */,
|
mDataUsageListAppsController.update(
|
||||||
mNetworkStatsDetailCallbacks);
|
mSubscriptionInfoEntity == null ? null : mSubscriptionInfoEntity.carrierId,
|
||||||
|
mChart.getInspectStart(),
|
||||||
|
mChart.getInspectEnd()
|
||||||
|
);
|
||||||
|
|
||||||
final long totalBytes = mCycleData != null && !mCycleData.isEmpty()
|
final long totalBytes = mCycleData != null && !mCycleData.isEmpty()
|
||||||
? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0;
|
? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0;
|
||||||
@@ -411,58 +395,6 @@ public class DataUsageList extends DataUsageBaseFragment
|
|||||||
mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
|
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() {
|
private final OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
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) {
|
private static boolean isGuestUser(Context context) {
|
||||||
if (context == null) return false;
|
if (context == null) return false;
|
||||||
final UserManager userManager = context.getSystemService(UserManager.class);
|
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
|
package com.android.settings.datausage.lib
|
||||||
|
|
||||||
import android.app.usage.NetworkStats
|
import android.app.usage.NetworkStats
|
||||||
|
import android.app.usage.NetworkStatsManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.NetworkPolicyManager
|
import android.net.NetworkPolicyManager
|
||||||
|
import android.net.NetworkTemplate
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
|
import android.util.Log
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.android.settingslib.AppItem
|
import com.android.settingslib.AppItem
|
||||||
import com.android.settingslib.net.UidDetailProvider
|
import com.android.settingslib.net.UidDetailProvider
|
||||||
@@ -30,15 +34,18 @@ import com.android.settingslib.spaprivileged.framework.common.userManager
|
|||||||
class AppDataUsageRepository(
|
class AppDataUsageRepository(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val currentUserId: Int,
|
private val currentUserId: Int,
|
||||||
private val carrierId: Int?,
|
private val template: NetworkTemplate,
|
||||||
private val getPackageName: (AppItem) -> String,
|
private val getPackageName: (AppItem) -> String?,
|
||||||
) {
|
) {
|
||||||
data class Bucket(
|
private val networkStatsManager = context.getSystemService(NetworkStatsManager::class.java)!!
|
||||||
val uid: Int,
|
|
||||||
val bytes: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
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 items = ArrayList<AppItem>()
|
||||||
val knownItems = SparseArray<AppItem>()
|
val knownItems = SparseArray<AppItem>()
|
||||||
val profiles = context.userManager.userProfiles
|
val profiles = context.userManager.userProfiles
|
||||||
@@ -61,7 +68,7 @@ class AppDataUsageRepository(
|
|||||||
item.restricted = true
|
item.restricted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredItems = filterItems(items).sorted()
|
val filteredItems = filterItems(carrierId, items).sorted()
|
||||||
val largest: Long = filteredItems.maxOfOrNull { it.total } ?: 0
|
val largest: Long = filteredItems.maxOfOrNull { it.total } ?: 0
|
||||||
return filteredItems.map { item ->
|
return filteredItems.map { item ->
|
||||||
val percentTotal = if (largest > 0) (item.total * 100 / largest).toInt() else 0
|
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.
|
// 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.
|
// In this case, the carrier service package also needs to be hidden.
|
||||||
if (carrierId != null && carrierId !in context.resources.getIntArray(
|
if (carrierId != null && carrierId !in context.resources.getIntArray(
|
||||||
@@ -178,7 +192,15 @@ class AppDataUsageRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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>()
|
val buckets = mutableListOf<Bucket>()
|
||||||
stats.use {
|
stats.use {
|
||||||
val bucket = NetworkStats.Bucket()
|
val bucket = NetworkStats.Bucket()
|
||||||
|
@@ -17,7 +17,6 @@
|
|||||||
package com.android.settings.datausage;
|
package com.android.settings.datausage;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
@@ -44,19 +43,15 @@ import androidx.loader.app.LoaderManager;
|
|||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
import com.android.settings.SettingsActivity;
|
|
||||||
import com.android.settings.network.MobileDataEnabledListener;
|
import com.android.settings.network.MobileDataEnabledListener;
|
||||||
import com.android.settings.testutils.FakeFeatureFactory;
|
import com.android.settings.testutils.FakeFeatureFactory;
|
||||||
import com.android.settings.widget.LoadingViewController;
|
import com.android.settings.widget.LoadingViewController;
|
||||||
import com.android.settingslib.AppItem;
|
|
||||||
import com.android.settingslib.NetworkPolicyEditor;
|
import com.android.settingslib.NetworkPolicyEditor;
|
||||||
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
|
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
|
||||||
import com.android.settingslib.net.NetworkCycleChartData;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.Robolectric;
|
import org.robolectric.Robolectric;
|
||||||
@@ -67,9 +62,6 @@ import org.robolectric.annotation.Implementation;
|
|||||||
import org.robolectric.annotation.Implements;
|
import org.robolectric.annotation.Implements;
|
||||||
import org.robolectric.util.ReflectionHelpers;
|
import org.robolectric.util.ReflectionHelpers;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
public class DataUsageListTest {
|
public class DataUsageListTest {
|
||||||
|
|
||||||
@@ -195,34 +187,6 @@ public class DataUsageListTest {
|
|||||||
assertThat(mDataUsageList.mSubId).isEqualTo(3);
|
assertThat(mDataUsageList.mSubId).isEqualTo(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void startAppDataUsage_shouldAddCyclesInfoToLaunchArguments() {
|
|
||||||
final long startTime = 1521583200000L;
|
|
||||||
final long endTime = 1521676800000L;
|
|
||||||
final List<NetworkCycleChartData> data = new ArrayList<>();
|
|
||||||
final NetworkCycleChartData.Builder builder = new NetworkCycleChartData.Builder();
|
|
||||||
builder.setStartTime(startTime)
|
|
||||||
.setEndTime(endTime);
|
|
||||||
data.add(builder.build());
|
|
||||||
ReflectionHelpers.setField(mDataUsageList, "mCycleData", data);
|
|
||||||
final Spinner spinner = mock(Spinner.class);
|
|
||||||
when(spinner.getSelectedItemPosition()).thenReturn(0);
|
|
||||||
ReflectionHelpers.setField(mDataUsageList, "mCycleSpinner", spinner);
|
|
||||||
final ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
|
|
||||||
|
|
||||||
mDataUsageList.startAppDataUsage(new AppItem());
|
|
||||||
|
|
||||||
verify(mActivity).startActivity(intent.capture());
|
|
||||||
final Bundle arguments =
|
|
||||||
intent.getValue().getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
|
|
||||||
assertThat(arguments.getLong(AppDataUsage.ARG_SELECTED_CYCLE)).isEqualTo(endTime);
|
|
||||||
final ArrayList<Long> cycles =
|
|
||||||
(ArrayList) arguments.getSerializable(AppDataUsage.ARG_NETWORK_CYCLES);
|
|
||||||
assertThat(cycles).hasSize(2);
|
|
||||||
assertThat(cycles.get(0)).isEqualTo(endTime);
|
|
||||||
assertThat(cycles.get(1)).isEqualTo(startTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void onViewCreated_shouldHideCycleSpinner() {
|
public void onViewCreated_shouldHideCycleSpinner() {
|
||||||
final View view = new View(mActivity);
|
final View view = new View(mActivity);
|
||||||
@@ -255,7 +219,6 @@ public class DataUsageListTest {
|
|||||||
mDataUsageList.onPause();
|
mDataUsageList.onPause();
|
||||||
|
|
||||||
verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_CHART_DATA);
|
verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_CHART_DATA);
|
||||||
verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_SUMMARY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private View getHeader() {
|
private View getHeader() {
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.NetworkTemplate
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.android.settings.SettingsActivity
|
||||||
|
import com.android.settingslib.AppItem
|
||||||
|
import com.android.settingslib.net.NetworkCycleChartData
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.argumentCaptor
|
||||||
|
import org.mockito.kotlin.doNothing
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.spy
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DataUsageListAppsControllerTest {
|
||||||
|
|
||||||
|
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
|
||||||
|
doNothing().whenever(mock).startActivity(any())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val controller = DataUsageListAppsController(context, "test_key")
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
controller.init(mock<NetworkTemplate>())
|
||||||
|
val data = NetworkCycleChartData.Builder().apply {
|
||||||
|
setStartTime(START_TIME)
|
||||||
|
setEndTime(END_TIME)
|
||||||
|
}.build()
|
||||||
|
controller.setCycleData(listOf(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startAppDataUsage_shouldAddCyclesInfoToLaunchArguments() {
|
||||||
|
controller.startAppDataUsage(AppItem(), END_TIME)
|
||||||
|
|
||||||
|
val intent = argumentCaptor<Intent> {
|
||||||
|
verify(context).startActivity(capture())
|
||||||
|
}.firstValue
|
||||||
|
val arguments = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS)!!
|
||||||
|
assertThat(arguments.getLong(AppDataUsage.ARG_SELECTED_CYCLE)).isEqualTo(END_TIME)
|
||||||
|
assertThat(
|
||||||
|
arguments.getSerializable(AppDataUsage.ARG_NETWORK_CYCLES, ArrayList::class.java)
|
||||||
|
).containsExactly(END_TIME, START_TIME).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val START_TIME = 1521583200000L
|
||||||
|
const val END_TIME = 1521676800000L
|
||||||
|
}
|
||||||
|
}
|
@@ -20,12 +20,13 @@ import android.content.Context
|
|||||||
import android.content.pm.UserInfo
|
import android.content.pm.UserInfo
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.NetworkPolicyManager
|
import android.net.NetworkPolicyManager
|
||||||
|
import android.net.NetworkTemplate
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.android.settings.datausage.lib.AppDataUsageRepository.Bucket
|
import com.android.settings.datausage.lib.AppDataUsageRepository.Companion.Bucket
|
||||||
import com.android.settingslib.AppItem
|
import com.android.settingslib.AppItem
|
||||||
import com.android.settingslib.spaprivileged.framework.common.userManager
|
import com.android.settingslib.spaprivileged.framework.common.userManager
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
@@ -72,15 +73,15 @@ class AppDataUsageRepositoryTest {
|
|||||||
val repository = AppDataUsageRepository(
|
val repository = AppDataUsageRepository(
|
||||||
context = context,
|
context = context,
|
||||||
currentUserId = USER_ID,
|
currentUserId = USER_ID,
|
||||||
carrierId = null,
|
template = Template,
|
||||||
getPackageName = { "" },
|
getPackageName = { null },
|
||||||
)
|
)
|
||||||
val buckets = listOf(
|
val buckets = listOf(
|
||||||
Bucket(uid = APP_ID_1, bytes = 1),
|
Bucket(uid = APP_ID_1, bytes = 1),
|
||||||
Bucket(uid = APP_ID_2, bytes = 2),
|
Bucket(uid = APP_ID_2, bytes = 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
val appPercentList = repository.getAppPercent(buckets)
|
val appPercentList = repository.getAppPercent(null, buckets)
|
||||||
|
|
||||||
assertThat(appPercentList).hasSize(2)
|
assertThat(appPercentList).hasSize(2)
|
||||||
appPercentList[0].first.apply {
|
appPercentList[0].first.apply {
|
||||||
@@ -102,15 +103,15 @@ class AppDataUsageRepositoryTest {
|
|||||||
val repository = AppDataUsageRepository(
|
val repository = AppDataUsageRepository(
|
||||||
context = context,
|
context = context,
|
||||||
currentUserId = USER_ID,
|
currentUserId = USER_ID,
|
||||||
carrierId = HIDING_CARRIER_ID,
|
template = Template,
|
||||||
getPackageName = { if (it.key == APP_ID_1) HIDING_PACKAGE_NAME else "" },
|
getPackageName = { if (it.key == APP_ID_1) HIDING_PACKAGE_NAME else null },
|
||||||
)
|
)
|
||||||
val buckets = listOf(
|
val buckets = listOf(
|
||||||
Bucket(uid = APP_ID_1, bytes = 1),
|
Bucket(uid = APP_ID_1, bytes = 1),
|
||||||
Bucket(uid = APP_ID_2, bytes = 2),
|
Bucket(uid = APP_ID_2, bytes = 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
val appPercentList = repository.getAppPercent(buckets)
|
val appPercentList = repository.getAppPercent(HIDING_CARRIER_ID, buckets)
|
||||||
|
|
||||||
assertThat(appPercentList).hasSize(1)
|
assertThat(appPercentList).hasSize(1)
|
||||||
appPercentList[0].first.apply {
|
appPercentList[0].first.apply {
|
||||||
@@ -127,5 +128,7 @@ class AppDataUsageRepositoryTest {
|
|||||||
const val APP_ID_2 = 110002
|
const val APP_ID_2 = 110002
|
||||||
const val HIDING_CARRIER_ID = 4
|
const val HIDING_CARRIER_ID = 4
|
||||||
const val HIDING_PACKAGE_NAME = "hiding.package.name"
|
const val HIDING_PACKAGE_NAME = "hiding.package.name"
|
||||||
|
|
||||||
|
val Template: NetworkTemplate = mock<NetworkTemplate>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user