Improve the latency of DataUsageList

Up to 30x faster.

Currently it load the usage detail for every day at the beginning, so
it's quite slow.

To fix,
- Not load the usage detail for every day at the beginning
- Load only the cycles first
- And only load the daily detail for the selected month

Fix: 290856342
Test: manual - on DataUsageList (cell & wifi)
Test: unit tests
Change-Id: Ie18fa68f801743389bd6b6a28e236dcf1fea00e4
This commit is contained in:
Chaohui Wang
2023-09-15 13:50:11 +08:00
parent db9fdb0de8
commit 2205762482
19 changed files with 1185 additions and 727 deletions

View File

@@ -31,6 +31,7 @@ import android.telephony.SubscriptionManager;
import android.util.ArraySet;
import android.util.IconDrawableFactory;
import android.util.Log;
import android.util.Range;
import android.view.View;
import android.widget.AdapterView;
@@ -472,7 +473,9 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
List<NetworkCycleDataForUid> data) {
mUsageData = data;
mCycle.setOnItemSelectedListener(mCycleListener);
mCycleAdapter.updateCycleList(data);
mCycleAdapter.updateCycleList(data.stream()
.map(cycle -> new Range<>(cycle.getStartTime(), cycle.getEndTime()))
.toList());
if (mSelectedCycle > 0L) {
final int numCycles = data.size();
int position = 0;

View File

@@ -26,15 +26,17 @@ import android.util.AttributeSet;
import android.util.DataUnit;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.datausage.lib.NetworkCycleChartData;
import com.android.settings.datausage.lib.NetworkUsageData;
import com.android.settings.widget.UsageView;
import com.android.settingslib.net.NetworkCycleChartData;
import com.android.settingslib.net.NetworkCycleData;
import java.util.ArrayList;
import java.util.Comparator;
@@ -51,8 +53,8 @@ public class ChartDataUsagePreference extends Preference {
private final int mWarningColor;
private final int mLimitColor;
private Resources mResources;
private NetworkPolicy mPolicy;
private final Resources mResources;
@Nullable private NetworkPolicy mPolicy;
private long mStart;
private long mEnd;
private NetworkCycleChartData mNetworkCycleChartData;
@@ -67,18 +69,16 @@ public class ChartDataUsagePreference extends Preference {
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final UsageView chart = (UsageView) holder.findViewById(R.id.data_usage);
if (mNetworkCycleChartData == null) {
return;
}
final UsageView chart = holder.itemView.requireViewById(R.id.data_usage);
final int top = getTop();
chart.clearPaths();
chart.configureGraph(toInt(mEnd - mStart), top);
calcPoints(chart, mNetworkCycleChartData.getUsageBuckets());
setupContentDescription(chart, mNetworkCycleChartData.getUsageBuckets());
if (mNetworkCycleChartData != null) {
calcPoints(chart, mNetworkCycleChartData.getDailyUsage());
setupContentDescription(chart, mNetworkCycleChartData.getDailyUsage());
}
chart.setBottomLabels(new CharSequence[] {
Utils.formatDateRange(getContext(), mStart, mStart),
Utils.formatDateRange(getContext(), mEnd, mEnd),
@@ -88,23 +88,21 @@ public class ChartDataUsagePreference extends Preference {
}
public int getTop() {
final long totalData = mNetworkCycleChartData.getTotalUsage();
final long totalData =
mNetworkCycleChartData != null ? mNetworkCycleChartData.getTotal().getUsage() : 0;
final long policyMax =
mPolicy != null ? Math.max(mPolicy.limitBytes, mPolicy.warningBytes) : 0;
return (int) (Math.max(totalData, policyMax) / RESOLUTION);
}
@VisibleForTesting
void calcPoints(UsageView chart, List<NetworkCycleData> usageSummary) {
if (usageSummary == null) {
return;
}
void calcPoints(UsageView chart, @NonNull List<NetworkUsageData> usageSummary) {
final SparseIntArray points = new SparseIntArray();
points.put(0, 0);
final long now = System.currentTimeMillis();
long totalData = 0;
for (NetworkCycleData data : usageSummary) {
for (NetworkUsageData data : usageSummary) {
final long startTime = data.getStartTime();
if (startTime > now) {
break;
@@ -112,7 +110,7 @@ public class ChartDataUsagePreference extends Preference {
final long endTime = data.getEndTime();
// increment by current bucket total
totalData += data.getTotalUsage();
totalData += data.getUsage();
if (points.size() == 1) {
points.put(toInt(startTime - mStart) - 1, -1);
@@ -125,7 +123,8 @@ public class ChartDataUsagePreference extends Preference {
}
}
private void setupContentDescription(UsageView chart, List<NetworkCycleData> usageSummary) {
private void setupContentDescription(
UsageView chart, @NonNull List<NetworkUsageData> usageSummary) {
final Context context = getContext();
final StringBuilder contentDescription = new StringBuilder();
final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
@@ -137,7 +136,7 @@ public class ChartDataUsagePreference extends Preference {
.getString(R.string.data_usage_chart_brief_content_description, startDate, endDate);
contentDescription.append(briefContentDescription);
if (usageSummary == null || usageSummary.isEmpty()) {
if (usageSummary.isEmpty()) {
final String noDataContentDescription = mResources
.getString(R.string.data_usage_chart_no_data_content_description);
contentDescription.append(noDataContentDescription);
@@ -170,17 +169,17 @@ public class ChartDataUsagePreference extends Preference {
* Collect the date of the same percentage, e.g., Aug 2 to Aug 22: 0%; Aug 23: 2%.
*/
@VisibleForTesting
List<DataUsageSummaryNode> getDensedStatsData(List<NetworkCycleData> usageSummary) {
List<DataUsageSummaryNode> getDensedStatsData(@NonNull List<NetworkUsageData> usageSummary) {
final List<DataUsageSummaryNode> dataUsageSummaryNodes = new ArrayList<>();
final long overallDataUsage = Math.max(1L, usageSummary.stream()
.mapToLong(NetworkCycleData::getTotalUsage).sum());
.mapToLong(NetworkUsageData::getUsage).sum());
long cumulatedDataUsage = 0L;
int cumulatedDataUsagePercentage = 0;
// Collect List of DataUsageSummaryNode for data usage percentage information.
for (NetworkCycleData data : usageSummary) {
cumulatedDataUsage += data.getTotalUsage();
cumulatedDataUsagePercentage = (int) ((cumulatedDataUsage * 100) / overallDataUsage);
for (NetworkUsageData data : usageSummary) {
cumulatedDataUsage += data.getUsage();
int cumulatedDataUsagePercentage =
(int) ((cumulatedDataUsage * 100) / overallDataUsage);
final DataUsageSummaryNode node = new DataUsageSummaryNode(data.getStartTime(),
data.getEndTime(), cumulatedDataUsagePercentage);
@@ -268,8 +267,9 @@ public class ChartDataUsagePreference extends Preference {
}
if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
chart.setDividerLoc((int) (policy.warningBytes / RESOLUTION));
float weight = policy.warningBytes / RESOLUTION / (float) top;
int dividerLoc = (int) (policy.warningBytes / RESOLUTION);
chart.setDividerLoc(dividerLoc);
float weight = dividerLoc / (float) top;
float above = 1 - weight;
chart.setSideLabelWeights(above, weight);
middleVisibility = mWarningColor;
@@ -289,15 +289,21 @@ public class ChartDataUsagePreference extends Preference {
return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0);
}
public void setNetworkPolicy(NetworkPolicy policy) {
/** Sets network policy. */
public void setNetworkPolicy(@Nullable NetworkPolicy policy) {
mPolicy = policy;
notifyChanged();
}
/** Sets time. */
public void setTime(long start, long end) {
mStart = start;
mEnd = end;
notifyChanged();
}
public void setNetworkCycleData(NetworkCycleChartData data) {
mNetworkCycleChartData = data;
mStart = data.getStartTime();
mEnd = data.getEndTime();
notifyChanged();
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.net.NetworkTemplate
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceScreen
import com.android.settings.core.BasePreferenceController
import com.android.settings.datausage.lib.INetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkCycleDataRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OpenForTesting
open class ChartDataUsagePreferenceController(context: Context, preferenceKey: String) :
BasePreferenceController(context, preferenceKey) {
private lateinit var repository: INetworkCycleDataRepository
private lateinit var preference: ChartDataUsagePreference
private lateinit var lifecycleScope: LifecycleCoroutineScope
open fun init(template: NetworkTemplate) {
this.repository = NetworkCycleDataRepository(mContext, template)
}
@VisibleForTesting
fun init(repository: INetworkCycleDataRepository) {
this.repository = repository
}
override fun getAvailabilityStatus() = AVAILABLE
override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen)
preference = screen.findPreference(preferenceKey)!!
}
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
lifecycleScope = viewLifecycleOwner.lifecycleScope
}
/**
* Sets whether billing cycle modifiable.
*
* Don't bind warning / limit sweeps if not modifiable.
*/
open fun setBillingCycleModifiable(isModifiable: Boolean) {
preference.setNetworkPolicy(
if (isModifiable) repository.getPolicy() else null
)
}
fun update(startTime: Long, endTime: Long) {
preference.setTime(startTime, endTime)
lifecycleScope.launch {
val chartData = withContext(Dispatchers.Default) {
repository.querySummary(startTime, endTime)
}
preference.setNetworkCycleData(chartData)
}
}
}

View File

@@ -14,9 +14,9 @@
package com.android.settings.datausage;
import android.content.Context;
import android.util.Range;
import com.android.settings.Utils;
import com.android.settingslib.net.NetworkCycleData;
import com.android.settingslib.widget.SettingsSpinnerAdapter;
import java.util.List;
@@ -62,15 +62,15 @@ public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem>
* Rebuild list based on network data. Always selects the newest item,
* updating the inspection range on chartData.
*/
public void updateCycleList(List<? extends NetworkCycleData> cycleData) {
public void updateCycleList(List<Range<Long>> cycleData) {
// stash away currently selected cycle to try restoring below
final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem)
mSpinner.getSelectedItem();
clear();
final Context context = getContext();
for (NetworkCycleData data : cycleData) {
add(new CycleAdapter.CycleItem(context, data.getStartTime(), data.getEndTime()));
for (Range<Long> cycle : cycleData) {
add(new CycleAdapter.CycleItem(context, cycle.getLower(), cycle.getUpper()));
}
// force pick the current cycle (first item)

View File

@@ -1,358 +0,0 @@
/*
* Copyright (C) 2018 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.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.net.NetworkPolicy;
import android.net.NetworkTemplate;
import android.os.Bundle;
import android.os.UserManager;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
import android.util.EventLog;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.datausage.lib.BillingCycleRepository;
import com.android.settings.network.MobileDataEnabledListener;
import com.android.settings.network.MobileNetworkRepository;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
import com.android.settingslib.net.NetworkCycleChartData;
import com.android.settingslib.net.NetworkCycleChartDataLoader;
import com.android.settingslib.utils.ThreadUtils;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import kotlin.Unit;
/**
* Panel showing data usage history across various networks, including options
* to inspect based on usage cycle and control through {@link NetworkPolicy}.
*/
public class DataUsageList extends DataUsageBaseFragment
implements MobileDataEnabledListener.Client {
static final String EXTRA_SUB_ID = "sub_id";
static final String EXTRA_NETWORK_TEMPLATE = "network_template";
private static final String TAG = "DataUsageList";
private static final boolean LOGD = false;
private static final String KEY_USAGE_AMOUNT = "usage_amount";
private static final String KEY_CHART_DATA = "chart_data";
private static final String KEY_TEMPLATE = "template";
private static final String KEY_APP = "app";
@VisibleForTesting
static final int LOADER_CHART_DATA = 2;
@VisibleForTesting
MobileDataEnabledListener mDataStateListener;
@VisibleForTesting
NetworkTemplate mTemplate;
@VisibleForTesting
int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@VisibleForTesting
LoadingViewController mLoadingViewController;
private ChartDataUsagePreference mChart;
@Nullable
private List<NetworkCycleChartData> mCycleData;
// 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 Preference mUsageAmount;
private MobileNetworkRepository mMobileNetworkRepository;
private SubscriptionInfoEntity mSubscriptionInfoEntity;
private DataUsageListAppsController mDataUsageListAppsController;
private BillingCycleRepository mBillingCycleRepository;
@VisibleForTesting
DataUsageListHeaderController mDataUsageListHeaderController;
private boolean mIsBillingCycleModifiable = false;
@Override
public int getMetricsCategory() {
return SettingsEnums.DATA_USAGE_LIST;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (isGuestUser(getContext())) {
Log.e(TAG, "This setting isn't available for guest user");
EventLog.writeEvent(0x534e4554, "262741858", -1 /* UID */, "Guest user");
finish();
return;
}
final Activity activity = getActivity();
mBillingCycleRepository = createBillingCycleRepository();
if (!mBillingCycleRepository.isBandwidthControlEnabled()) {
Log.w(TAG, "No bandwidth control; leaving");
activity.finish();
return;
}
mUsageAmount = findPreference(KEY_USAGE_AMOUNT);
mChart = findPreference(KEY_CHART_DATA);
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);
}
@VisibleForTesting
@NonNull
BillingCycleRepository createBillingCycleRepository() {
return new BillingCycleRepository(requireContext());
}
@Override
public void onViewCreated(@NonNull View v, Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
mDataUsageListHeaderController = new DataUsageListHeaderController(
setPinnedHeaderView(R.layout.apps_filter_spinner),
mTemplate,
getMetricsCategory(),
(cycle, position) -> {
updateSelectedCycle(cycle, position);
return Unit.INSTANCE;
}
);
mLoadingViewController = new LoadingViewController(
getView().findViewById(R.id.loading_container), getListView());
}
@Override
public void onResume() {
super.onResume();
mLoadingViewController.showLoadingViewDelayed();
mDataStateListener.start(mSubId);
mLastDisplayedCycle = null;
updatePolicy();
// kick off loader for network history
// TODO: consider chaining two loaders together instead of reloading
// network history when showing app detail.
getLoaderManager().restartLoader(LOADER_CHART_DATA,
buildArgs(mTemplate), mNetworkCycleDataCallbacks);
}
@Override
public void onPause() {
super.onPause();
mDataStateListener.stop();
getLoaderManager().destroyLoader(LOADER_CHART_DATA);
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.data_usage_list;
}
@Override
protected String getLogTag() {
return TAG;
}
void processArgument() {
final Bundle args = getArguments();
if (args != null) {
mSubId = args.getInt(EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mTemplate = args.getParcelable(EXTRA_NETWORK_TEMPLATE);
}
if (mTemplate == null && mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
final Intent intent = getIntent();
mSubId = intent.getIntExtra(Settings.EXTRA_SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mTemplate = intent.getParcelableExtra(Settings.EXTRA_NETWORK_TEMPLATE);
if (mTemplate == null) {
Optional<NetworkTemplate> mobileNetworkTemplateFromSim =
DataUsageUtils.getMobileNetworkTemplateFromSubId(getContext(), getIntent());
if (mobileNetworkTemplateFromSim.isPresent()) {
mTemplate = mobileNetworkTemplateFromSim.get();
}
}
}
}
@VisibleForTesting
void updateSubscriptionInfoEntity() {
mMobileNetworkRepository = MobileNetworkRepository.getInstance(getContext());
ThreadUtils.postOnBackgroundThread(() -> {
mSubscriptionInfoEntity = mMobileNetworkRepository.getSubInfoById(
String.valueOf(mSubId));
});
}
/**
* Implementation of {@code MobileDataEnabledListener.Client}
*/
public void onMobileDataEnabledChange() {
updatePolicy();
}
private Bundle buildArgs(NetworkTemplate template) {
final Bundle args = new Bundle();
args.putParcelable(KEY_TEMPLATE, template);
args.putParcelable(KEY_APP, null);
return args;
}
/**
* Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for
* current {@link #mTemplate}.
*/
@VisibleForTesting
void updatePolicy() {
mIsBillingCycleModifiable = isBillingCycleModifiable();
if (mIsBillingCycleModifiable) {
mChart.setNetworkPolicy(services.mPolicyEditor.getPolicy(mTemplate));
} else {
mChart.setNetworkPolicy(null); // don't bind warning / limit sweeps
}
updateConfigButtonVisibility();
}
@VisibleForTesting
boolean isBillingCycleModifiable() {
return mBillingCycleRepository.isModifiable(mSubId)
&& SubscriptionManager.from(requireContext())
.getActiveSubscriptionInfo(mSubId) != null;
}
private void updateConfigButtonVisibility() {
mDataUsageListHeaderController.setConfigButtonVisible(
mIsBillingCycleModifiable && mCycleData != null);
}
/**
* Updates the chart and detail data when initial loaded or selected cycle changed.
*/
private void updateSelectedCycle(CycleAdapter.CycleItem cycle, int position) {
// Avoid from updating UI after #onStop.
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
return;
}
// Avoid from updating UI when async query still on-going.
// This could happen when a request from #onMobileDataEnabledChange.
if (mCycleData == null) {
return;
}
if (Objects.equals(cycle, mLastDisplayedCycle)) {
// Avoid duplicate update to avoid page flash.
return;
}
mLastDisplayedCycle = cycle;
if (LOGD) {
Log.d(TAG, "showing cycle " + cycle + ", [start=" + cycle.start + ", end="
+ cycle.end + "]");
}
// update chart to show selected cycle, and update detail data
// to match updated sweep bounds.
NetworkCycleChartData cycleChartData = mCycleData.get(position);
mChart.setNetworkCycleData(cycleChartData);
updateDetailData(cycleChartData);
}
/**
* Update details based on {@link #mChart} inspection range depending on
* current mode. Updates {@link #mAdapter} with sorted list
* of applications data usage.
*/
private void updateDetailData(NetworkCycleChartData cycleChartData) {
if (LOGD) Log.d(TAG, "updateDetailData()");
// kick off loader for detailed stats
mDataUsageListAppsController.update(
mSubscriptionInfoEntity == null ? null : mSubscriptionInfoEntity.carrierId,
cycleChartData.getStartTime(),
cycleChartData.getEndTime()
);
final long totalBytes = cycleChartData.getTotalUsage();
final CharSequence totalPhrase = DataUsageUtils.formatDataUsage(getActivity(), totalBytes);
mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
}
@VisibleForTesting
final LoaderCallbacks<List<NetworkCycleChartData>> mNetworkCycleDataCallbacks =
new LoaderCallbacks<>() {
@Override
@NonNull
public Loader<List<NetworkCycleChartData>> onCreateLoader(int id, Bundle args) {
return NetworkCycleChartDataLoader.builder(getContext())
.setNetworkTemplate(mTemplate)
.build();
}
@Override
public void onLoadFinished(@NonNull Loader<List<NetworkCycleChartData>> loader,
List<NetworkCycleChartData> data) {
mLoadingViewController.showContent(false /* animate */);
mCycleData = data;
mDataUsageListHeaderController.updateCycleData(mCycleData);
updateConfigButtonVisibility();
mDataUsageListAppsController.setCycleData(mCycleData);
}
@Override
public void onLoaderReset(@NonNull Loader<List<NetworkCycleChartData>> loader) {
mCycleData = null;
}
};
private static boolean isGuestUser(Context context) {
if (context == null) return false;
final UserManager userManager = context.getSystemService(UserManager.class);
if (userManager == null) return false;
return userManager.isGuestUser();
}
}

View File

@@ -0,0 +1,229 @@
/*
* 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.settings.SettingsEnums
import android.net.NetworkPolicy
import android.net.NetworkTemplate
import android.os.Bundle
import android.provider.Settings
import android.telephony.SubscriptionManager
import android.util.EventLog
import android.util.Log
import android.view.View
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.datausage.lib.NetworkUsageData
import com.android.settings.network.MobileDataEnabledListener
import com.android.settings.network.MobileNetworkRepository
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.utils.ThreadUtils
import kotlin.jvm.optionals.getOrNull
/**
* Panel showing data usage history across various networks, including options
* to inspect based on usage cycle and control through [NetworkPolicy].
*/
@OpenForTesting
open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Client {
@VisibleForTesting
lateinit var dataStateListener: MobileDataEnabledListener
@JvmField
@VisibleForTesting
var template: NetworkTemplate? = null
@JvmField
@VisibleForTesting
var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID
// Spinner will keep the selected cycle even after paused, this only keeps the displayed cycle,
// which need be cleared when resumed.
private var lastDisplayedUsageData: NetworkUsageData? = null
private lateinit var usageAmount: Preference
private var subscriptionInfoEntity: SubscriptionInfoEntity? = null
private lateinit var dataUsageListAppsController: DataUsageListAppsController
private lateinit var chartDataUsagePreferenceController: ChartDataUsagePreferenceController
private lateinit var billingCycleRepository: BillingCycleRepository
@VisibleForTesting
var dataUsageListHeaderController: DataUsageListHeaderController? = null
override fun getMetricsCategory() = SettingsEnums.DATA_USAGE_LIST
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (requireContext().userManager.isGuestUser) {
Log.e(TAG, "This setting isn't available for guest user")
EventLog.writeEvent(0x534e4554, "262741858", -1 /* UID */, "Guest user")
finish()
return
}
billingCycleRepository = createBillingCycleRepository();
if (!billingCycleRepository.isBandwidthControlEnabled()) {
Log.w(TAG, "No bandwidth control; leaving")
finish()
return
}
usageAmount = findPreference(KEY_USAGE_AMOUNT)!!
processArgument()
val template = template
if (template == null) {
Log.e(TAG, "No template; leaving")
finish()
return
}
updateSubscriptionInfoEntity()
dataStateListener = MobileDataEnabledListener(activity, this)
dataUsageListAppsController = use(DataUsageListAppsController::class.java).apply {
init(template)
}
chartDataUsagePreferenceController = use(ChartDataUsagePreferenceController::class.java)
chartDataUsagePreferenceController.init(template)
}
@VisibleForTesting
open fun createBillingCycleRepository() = BillingCycleRepository(requireContext())
override fun onViewCreated(v: View, savedInstanceState: Bundle?) {
super.onViewCreated(v, savedInstanceState)
val template = template ?: return
dataUsageListHeaderController = DataUsageListHeaderController(
setPinnedHeaderView(R.layout.apps_filter_spinner),
template,
metricsCategory,
viewLifecycleOwner,
::onCyclesLoad,
::updateSelectedCycle,
)
}
override fun onResume() {
super.onResume()
dataStateListener.start(subId)
lastDisplayedUsageData = null
updatePolicy()
}
override fun onPause() {
super.onPause()
dataStateListener.stop()
}
override fun getPreferenceScreenResId() = R.xml.data_usage_list
override fun getLogTag() = TAG
fun processArgument() {
arguments?.let {
subId = it.getInt(EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID)
template = it.getParcelable(EXTRA_NETWORK_TEMPLATE, NetworkTemplate::class.java)
}
if (template == null && subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
subId = intent.getIntExtra(
Settings.EXTRA_SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID,
)
template = intent.getParcelableExtra(
Settings.EXTRA_NETWORK_TEMPLATE,
NetworkTemplate::class.java,
) ?: DataUsageUtils.getMobileNetworkTemplateFromSubId(context, intent).getOrNull()
}
}
@VisibleForTesting
open fun updateSubscriptionInfoEntity() {
ThreadUtils.postOnBackgroundThread {
subscriptionInfoEntity =
MobileNetworkRepository.getInstance(context).getSubInfoById(subId.toString())
}
}
/**
* Implementation of `MobileDataEnabledListener.Client`
*/
override fun onMobileDataEnabledChange() {
updatePolicy()
}
/** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */
@VisibleForTesting
fun updatePolicy() {
val isBillingCycleModifiable = isBillingCycleModifiable()
dataUsageListHeaderController?.setConfigButtonVisible(isBillingCycleModifiable)
chartDataUsagePreferenceController.setBillingCycleModifiable(isBillingCycleModifiable)
}
@VisibleForTesting
open fun isBillingCycleModifiable(): Boolean {
return (billingCycleRepository.isModifiable(subId) &&
requireContext().getSystemService(SubscriptionManager::class.java)!!
.getActiveSubscriptionInfo(subId) != null)
}
private fun onCyclesLoad(networkUsageData: List<NetworkUsageData>) {
dataUsageListAppsController.updateCycles(networkUsageData)
}
/**
* Updates the chart and detail data when initial loaded or selected cycle changed.
*/
private fun updateSelectedCycle(usageData: NetworkUsageData) {
if (usageData == lastDisplayedUsageData) {
// Avoid duplicate update to avoid page flash.
return
}
lastDisplayedUsageData = usageData
Log.d(TAG, "showing cycle $usageData")
val totalPhrase = DataUsageUtils.formatDataUsage(requireContext(), usageData.usage)
usageAmount.title = getString(R.string.data_used_template, totalPhrase)
updateChart(usageData)
updateApps(usageData)
}
/** Updates chart to show selected cycle. */
private fun updateChart(usageData: NetworkUsageData) {
chartDataUsagePreferenceController.update(
startTime = usageData.startTime,
endTime = usageData.endTime,
)
}
/** Updates applications data usage. */
private fun updateApps(usageData: NetworkUsageData) {
dataUsageListAppsController.update(
carrierId = subscriptionInfoEntity?.carrierId,
startTime = usageData.startTime,
endTime = usageData.endTime,
)
}
companion object {
const val EXTRA_SUB_ID = "sub_id"
const val EXTRA_NETWORK_TEMPLATE = "network_template"
private const val TAG = "DataUsageList"
private const val KEY_USAGE_AMOUNT = "usage_amount"
}
}

View File

@@ -31,8 +31,8 @@ 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.settings.datausage.lib.NetworkUsageData
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
@@ -48,7 +48,7 @@ open class DataUsageListAppsController(context: Context, preferenceKey: String)
private lateinit var preference: PreferenceGroup
private lateinit var lifecycleScope: LifecycleCoroutineScope
private var cycleData: List<NetworkCycleChartData>? = null
private var cycleData: List<NetworkUsageData>? = null
open fun init(template: NetworkTemplate) {
this.template = template
@@ -70,7 +70,7 @@ open class DataUsageListAppsController(context: Context, preferenceKey: String)
lifecycleScope = viewLifecycleOwner.lifecycleScope
}
fun setCycleData(cycleData: List<NetworkCycleChartData>?) {
fun updateCycles(cycleData: List<NetworkUsageData>) {
this.cycleData = cycleData
}

View File

@@ -18,25 +18,39 @@ package com.android.settings.datausage
import android.net.NetworkTemplate
import android.os.Bundle
import android.util.Range
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.widget.AdapterView
import android.widget.Spinner
import androidx.annotation.OpenForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.settings.R
import com.android.settings.core.SubSettingLauncher
import com.android.settings.datausage.CycleAdapter.CycleItem
import com.android.settings.datausage.CycleAdapter.SpinnerInterface
import com.android.settingslib.net.NetworkCycleChartData
import com.android.settings.datausage.lib.INetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkUsageData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OpenForTesting
open class DataUsageListHeaderController(
header: View,
template: NetworkTemplate,
sourceMetricsCategory: Int,
private val onItemSelected: (cycleItem: CycleItem, position: Int) -> Unit,
viewLifecycleOwner: LifecycleOwner,
private val onCyclesLoad: (usageDataList: List<NetworkUsageData>) -> Unit,
private val onItemSelected: (usageData: NetworkUsageData) -> Unit,
private val repository: INetworkCycleDataRepository =
NetworkCycleDataRepository(header.context, template),
) {
private val context = header.context
private val configureButton: View = header.requireViewById(R.id.filter_settings)
private val cycleSpinner: Spinner = header.requireViewById(R.id.filter_spinner)
private val cycleAdapter = CycleAdapter(context, object : SpinnerInterface {
@@ -50,13 +64,12 @@ open class DataUsageListHeaderController(
cycleSpinner.setSelection(position)
}
})
private var cycles: List<NetworkUsageData> = emptyList()
private val cycleListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (0 <= position && position < cycleAdapter.count) {
cycleAdapter.getItem(position)?.let { cycleItem ->
onItemSelected(cycleItem, position)
}
cycles.getOrNull(position)?.let(onItemSelected)
}
}
@@ -80,24 +93,32 @@ open class DataUsageListHeaderController(
cycleSpinner.visibility = View.GONE
cycleSpinner.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun sendAccessibilityEvent(host: View, eventType: Int) {
if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
// Ignore TYPE_VIEW_SELECTED or TalkBack will speak for it at onResume.
return
}
// Ignore TYPE_VIEW_SELECTED or TalkBack will speak for it at onResume.
if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) return
super.sendAccessibilityEvent(host, eventType)
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
cycles = withContext(Dispatchers.Default) {
repository.loadCycles()
}
updateCycleData()
}
}
}
open fun setConfigButtonVisible(visible: Boolean) {
configureButton.visibility = if (visible) View.VISIBLE else View.GONE
}
open fun updateCycleData(cycleData: List<NetworkCycleChartData>) {
private fun updateCycleData() {
cycleSpinner.onItemSelectedListener = cycleListener
// calculate policy cycles based on available data
// generate cycle list based on policy and available history
cycleAdapter.updateCycleList(cycleData)
cycleAdapter.updateCycleList(cycles.map { Range(it.startTime, it.endTime) })
cycleSpinner.visibility = View.VISIBLE
onCyclesLoad(cycles)
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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 kotlin.time.Duration.Companion.days
/**
* Usage data in a billing cycle with daily data for plotting the usage chart.
*/
data class NetworkCycleChartData(
val total: NetworkUsageData,
val dailyUsage: List<NetworkUsageData>,
) {
companion object {
val BUCKET_DURATION = 1.days
}
}

View File

@@ -0,0 +1,154 @@
/*
* 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.app.usage.NetworkStatsManager
import android.content.Context
import android.net.NetworkPolicy
import android.net.NetworkPolicyManager
import android.net.NetworkTemplate
import android.text.format.DateUtils
import android.util.Log
import android.util.Range
import androidx.annotation.VisibleForTesting
import com.android.settingslib.NetworkPolicyEditor
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
interface INetworkCycleDataRepository {
suspend fun loadCycles(): List<NetworkUsageData>
fun getPolicy(): NetworkPolicy?
suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData?
}
class NetworkCycleDataRepository(
context: Context,
private val networkTemplate: NetworkTemplate,
) : INetworkCycleDataRepository {
private val networkStatsManager = context.getSystemService(NetworkStatsManager::class.java)!!
private val policyManager = context.getSystemService(NetworkPolicyManager::class.java)!!
override suspend fun loadCycles(): List<NetworkUsageData> =
getCycles().queryUsage().filter { it.usage > 0 }
private fun getCycles(): List<Range<Long>> {
val policy = getPolicy() ?: return queryCyclesAsFourWeeks()
return policy.cycleIterator().asSequence().map {
Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli())
}.toList()
}
private fun queryCyclesAsFourWeeks(): List<Range<Long>> {
val timeRange = getTimeRange()
return reverseBucketRange(
startTime = timeRange.lower,
endTime = timeRange.upper,
bucketSize = DateUtils.WEEK_IN_MILLIS * 4,
)
}
@VisibleForTesting
fun getTimeRange(): Range<Long> = getTimeRangeOf(
networkStatsManager.queryDetailsForDevice(networkTemplate, Long.MIN_VALUE, Long.MAX_VALUE)
)
private fun getTimeRangeOf(stats: NetworkStats): Range<Long> {
var start = Long.MAX_VALUE
var end = Long.MIN_VALUE
val bucket = NetworkStats.Bucket()
while (stats.getNextBucket(bucket)) {
start = start.coerceAtMost(bucket.startTimeStamp)
end = end.coerceAtLeast(bucket.endTimeStamp)
}
return Range(start, end)
}
override fun getPolicy(): NetworkPolicy? =
with(NetworkPolicyEditor(policyManager)) {
read()
getPolicy(networkTemplate)
}
override suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData? {
val usage = getUsage(startTime, endTime)
if (usage > 0L) {
return NetworkCycleChartData(
total = NetworkUsageData(startTime, endTime, usage),
dailyUsage = bucketRange(
startTime = startTime,
endTime = endTime,
bucketSize = NetworkCycleChartData.BUCKET_DURATION.inWholeMilliseconds,
).queryUsage(),
)
}
return null
}
private suspend fun List<Range<Long>>.queryUsage(): List<NetworkUsageData> = coroutineScope {
map { range ->
async {
NetworkUsageData(
startTime = range.lower,
endTime = range.upper,
usage = getUsage(range.lower, range.upper),
)
}
}.awaitAll()
}
private fun bucketRange(startTime: Long, endTime: Long, bucketSize: Long): List<Range<Long>> {
val buckets = mutableListOf<Range<Long>>()
var currentStart = startTime
while (currentStart < endTime) {
val bucketEnd = currentStart + bucketSize
buckets += Range(currentStart, bucketEnd)
currentStart = bucketEnd
}
return buckets
}
private fun reverseBucketRange(
startTime: Long,
endTime: Long,
bucketSize: Long,
): List<Range<Long>> {
val buckets = mutableListOf<Range<Long>>()
var currentEnd = endTime
while (currentEnd > startTime) {
val bucketStart = currentEnd - bucketSize
buckets += Range(bucketStart, currentEnd)
currentEnd = bucketStart
}
return buckets
}
private fun getUsage(start: Long, end: Long): Long = try {
networkStatsManager.querySummaryForDevice(networkTemplate, start, end).let {
it.rxBytes + it.txBytes
}
} catch (e: Exception) {
Log.e(TAG, "Exception querying network detail.", e)
0
}
companion object {
private const val TAG = "NetworkCycleDataRepository"
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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
/**
* Base data structure representing usage data in a period.
*/
data class NetworkUsageData(
val startTime: Long,
val endTime: Long,
val usage: Long,
)