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

@@ -22,7 +22,9 @@
android:title="@string/summary_placeholder">
<com.android.settings.datausage.ChartDataUsagePreference
android:key="chart_data" />
android:key="chart_data"
settings:controller="com.android.settings.datausage.ChartDataUsagePreferenceController"
/>
<Preference
android:key="non_carrier_data_usage_warning"

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,
)

View File

@@ -31,9 +31,9 @@ import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.datausage.ChartDataUsagePreference.DataUsageSummaryNode;
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 org.junit.Before;
import org.junit.Test;
@@ -55,7 +55,7 @@ public class ChartDataUsagePreferenceTest {
// Test bucket end date, 22 Mar 2018 00:00:00
private static final long TIMESTAMP_END = 1521676800000L;
private List<NetworkCycleData> mNetworkCycleData;
private List<NetworkUsageData> mNetworkCycleData;
private NetworkCycleChartData mNetworkCycleChartData;
private ChartDataUsagePreference mPreference;
private Activity mActivity;
@@ -79,6 +79,9 @@ public class ChartDataUsagePreferenceTest {
final ArgumentCaptor<SparseIntArray> pointsCaptor =
ArgumentCaptor.forClass(SparseIntArray.class);
createTestNetworkData();
mPreference.setTime(
mNetworkCycleChartData.getTotal().getStartTime(),
mNetworkCycleChartData.getTotal().getEndTime());
mPreference.setNetworkCycleData(mNetworkCycleChartData);
mPreference.calcPoints(usageView, mNetworkCycleData.subList(0, 5));
@@ -95,6 +98,9 @@ public class ChartDataUsagePreferenceTest {
final ArgumentCaptor<SparseIntArray> pointsCaptor =
ArgumentCaptor.forClass(SparseIntArray.class);
createTestNetworkData();
mPreference.setTime(
mNetworkCycleChartData.getTotal().getStartTime(),
mNetworkCycleChartData.getTotal().getEndTime());
mPreference.setNetworkCycleData(mNetworkCycleChartData);
mPreference.calcPoints(usageView, mNetworkCycleData.subList(2, 7));
@@ -110,39 +116,62 @@ public class ChartDataUsagePreferenceTest {
public void calcPoints_shouldNotDrawPointForFutureDate() {
final UsageView usageView = mock(UsageView.class);
final ArgumentCaptor<SparseIntArray> pointsCaptor =
ArgumentCaptor.forClass(SparseIntArray.class);
ArgumentCaptor.forClass(SparseIntArray.class);
final long tonight = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(12);
mNetworkCycleData = new ArrayList<>();
// add test usage data for last 5 days
mNetworkCycleData.add(createNetworkCycleData(
tonight - TimeUnit.DAYS.toMillis(5), tonight - TimeUnit.DAYS.toMillis(4), 743823454L));
mNetworkCycleData.add(createNetworkCycleData(
tonight - TimeUnit.DAYS.toMillis(4), tonight - TimeUnit.DAYS.toMillis(3), 64396L));
mNetworkCycleData.add(createNetworkCycleData(
tonight - TimeUnit.DAYS.toMillis(3), tonight - TimeUnit.DAYS.toMillis(2), 2832L));
mNetworkCycleData.add(createNetworkCycleData(
tonight - TimeUnit.DAYS.toMillis(2), tonight - TimeUnit.DAYS.toMillis(1), 83849690L));
mNetworkCycleData.add(createNetworkCycleData(
tonight - TimeUnit.DAYS.toMillis(1), tonight, 1883657L));
mNetworkCycleData.add(new NetworkUsageData(
tonight - TimeUnit.DAYS.toMillis(5),
tonight - TimeUnit.DAYS.toMillis(4),
743823454L));
mNetworkCycleData.add(new NetworkUsageData(
tonight - TimeUnit.DAYS.toMillis(4),
tonight - TimeUnit.DAYS.toMillis(3),
64396L));
mNetworkCycleData.add(new NetworkUsageData(
tonight - TimeUnit.DAYS.toMillis(3),
tonight - TimeUnit.DAYS.toMillis(2),
2832L));
mNetworkCycleData.add(new NetworkUsageData(
tonight - TimeUnit.DAYS.toMillis(2),
tonight - TimeUnit.DAYS.toMillis(1),
83849690L));
mNetworkCycleData.add(new NetworkUsageData(
tonight - TimeUnit.DAYS.toMillis(1), tonight, 1883657L));
// add test usage data for next 5 days
mNetworkCycleData.add(createNetworkCycleData(
tonight, tonight + TimeUnit.DAYS.toMillis(1), 0L));
mNetworkCycleData.add(createNetworkCycleData(
tonight + TimeUnit.DAYS.toMillis(1), tonight + TimeUnit.DAYS.toMillis(2), 0L));
mNetworkCycleData.add(createNetworkCycleData(
tonight + TimeUnit.DAYS.toMillis(2), tonight + TimeUnit.DAYS.toMillis(3), 0L));
mNetworkCycleData.add(createNetworkCycleData(
tonight + TimeUnit.DAYS.toMillis(3), tonight + TimeUnit.DAYS.toMillis(4), 0L));
mNetworkCycleData.add(createNetworkCycleData(
tonight + TimeUnit.DAYS.toMillis(4), tonight + TimeUnit.DAYS.toMillis(5), 0L));
mNetworkCycleData.add(createNetworkCycleData(
tonight + TimeUnit.DAYS.toMillis(5), tonight + TimeUnit.DAYS.toMillis(6), 0L));
mNetworkCycleData.add(new NetworkUsageData(
tonight, tonight + TimeUnit.DAYS.toMillis(1), 0L));
mNetworkCycleData.add(new NetworkUsageData(
tonight + TimeUnit.DAYS.toMillis(1),
tonight + TimeUnit.DAYS.toMillis(2),
0L));
mNetworkCycleData.add(new NetworkUsageData(
tonight + TimeUnit.DAYS.toMillis(2),
tonight + TimeUnit.DAYS.toMillis(3),
0L));
mNetworkCycleData.add(new NetworkUsageData(
tonight + TimeUnit.DAYS.toMillis(3),
tonight + TimeUnit.DAYS.toMillis(4),
0L));
mNetworkCycleData.add(new NetworkUsageData(
tonight + TimeUnit.DAYS.toMillis(4),
tonight + TimeUnit.DAYS.toMillis(5),
0L));
mNetworkCycleData.add(new NetworkUsageData(
tonight + TimeUnit.DAYS.toMillis(5),
tonight + TimeUnit.DAYS.toMillis(6),
0L));
final NetworkCycleChartData.Builder builder = new NetworkCycleChartData.Builder();
builder.setUsageBuckets(mNetworkCycleData)
.setStartTime(tonight - TimeUnit.DAYS.toMillis(5))
.setEndTime(tonight + TimeUnit.DAYS.toMillis(6));
mNetworkCycleChartData = builder.build();
mNetworkCycleChartData = new NetworkCycleChartData(
new NetworkUsageData(
tonight - TimeUnit.DAYS.toMillis(5),
tonight + TimeUnit.DAYS.toMillis(6),
0),
mNetworkCycleData
);
mPreference.setTime(
mNetworkCycleChartData.getTotal().getStartTime(),
mNetworkCycleChartData.getTotal().getEndTime());
mPreference.setNetworkCycleData(mNetworkCycleChartData);
mPreference.calcPoints(usageView, mNetworkCycleData);
@@ -170,6 +199,9 @@ public class ChartDataUsagePreferenceTest {
final TextView labelStart = (TextView) mHolder.findViewById(R.id.label_start);
final TextView labelEnd = (TextView) mHolder.findViewById(R.id.label_end);
createTestNetworkData();
mPreference.setTime(
mNetworkCycleChartData.getTotal().getStartTime(),
mNetworkCycleChartData.getTotal().getEndTime());
mPreference.setNetworkCycleData(mNetworkCycleChartData);
mPreference.onBindViewHolder(mHolder);
@@ -198,38 +230,33 @@ public class ChartDataUsagePreferenceTest {
private void createTestNetworkData() {
mNetworkCycleData = new ArrayList<>();
// create 10 arbitrary network data
mNetworkCycleData.add(createNetworkCycleData(1521583200000L, 1521586800000L, 743823454L));
mNetworkCycleData.add(createNetworkCycleData(1521586800000L, 1521590400000L, 64396L));
mNetworkCycleData.add(createNetworkCycleData(1521590400000L, 1521655200000L, 2832L));
mNetworkCycleData.add(createNetworkCycleData(1521655200000L, 1521658800000L, 83849690L));
mNetworkCycleData.add(createNetworkCycleData(1521658800000L, 1521662400000L, 1883657L));
mNetworkCycleData.add(createNetworkCycleData(1521662400000L, 1521666000000L, 705259L));
mNetworkCycleData.add(createNetworkCycleData(1521666000000L, 1521669600000L, 216169L));
mNetworkCycleData.add(createNetworkCycleData(1521669600000L, 1521673200000L, 6069175L));
mNetworkCycleData.add(createNetworkCycleData(1521673200000L, 1521676800000L, 120389L));
mNetworkCycleData.add(createNetworkCycleData(1521676800000L, 1521678800000L, 29947L));
mNetworkCycleData.add(new NetworkUsageData(1521583200000L, 1521586800000L, 743823454L));
mNetworkCycleData.add(new NetworkUsageData(1521586800000L, 1521590400000L, 64396L));
mNetworkCycleData.add(new NetworkUsageData(1521590400000L, 1521655200000L, 2832L));
mNetworkCycleData.add(new NetworkUsageData(1521655200000L, 1521658800000L, 83849690L));
mNetworkCycleData.add(new NetworkUsageData(1521658800000L, 1521662400000L, 1883657L));
mNetworkCycleData.add(new NetworkUsageData(1521662400000L, 1521666000000L, 705259L));
mNetworkCycleData.add(new NetworkUsageData(1521666000000L, 1521669600000L, 216169L));
mNetworkCycleData.add(new NetworkUsageData(1521669600000L, 1521673200000L, 6069175L));
mNetworkCycleData.add(new NetworkUsageData(1521673200000L, 1521676800000L, 120389L));
mNetworkCycleData.add(new NetworkUsageData(1521676800000L, 1521678800000L, 29947L));
final NetworkCycleChartData.Builder builder = new NetworkCycleChartData.Builder();
builder.setUsageBuckets(mNetworkCycleData)
.setStartTime(TIMESTAMP_START)
.setEndTime(TIMESTAMP_END);
mNetworkCycleChartData = builder.build();
mNetworkCycleChartData = new NetworkCycleChartData(
new NetworkUsageData(TIMESTAMP_START, TIMESTAMP_END, 0),
mNetworkCycleData
);
}
private void createSomeSamePercentageNetworkData() {
mNetworkCycleData = new ArrayList<>();
mNetworkCycleData.add(createNetworkCycleData(1521583200000L, 1521586800000L, 100));//33%
mNetworkCycleData.add(createNetworkCycleData(1521586800000L, 1521590400000L, 1)); //33%
mNetworkCycleData.add(createNetworkCycleData(1521590400000L, 1521655200000L, 0)); //33%
mNetworkCycleData.add(createNetworkCycleData(1521655200000L, 1521658800000L, 0)); //33%
mNetworkCycleData.add(createNetworkCycleData(1521658800000L, 1521662400000L, 200));//99%
mNetworkCycleData.add(createNetworkCycleData(1521662400000L, 1521666000000L, 1)); //99%
mNetworkCycleData.add(createNetworkCycleData(1521666000000L, 1521669600000L, 1)); //100
mNetworkCycleData.add(createNetworkCycleData(1521669600000L, 1521673200000L, 0)); //100%
mNetworkCycleData.add(new NetworkUsageData(1521583200000L, 1521586800000L, 100)); //33%
mNetworkCycleData.add(new NetworkUsageData(1521586800000L, 1521590400000L, 1)); //33%
mNetworkCycleData.add(new NetworkUsageData(1521590400000L, 1521655200000L, 0)); //33%
mNetworkCycleData.add(new NetworkUsageData(1521655200000L, 1521658800000L, 0)); //33%
mNetworkCycleData.add(new NetworkUsageData(1521658800000L, 1521662400000L, 200)); //99%
mNetworkCycleData.add(new NetworkUsageData(1521662400000L, 1521666000000L, 1)); //99%
mNetworkCycleData.add(new NetworkUsageData(1521666000000L, 1521669600000L, 1)); //100
mNetworkCycleData.add(new NetworkUsageData(1521669600000L, 1521673200000L, 0)); //100%
}
private NetworkCycleData createNetworkCycleData(long start, long end, long usage) {
return new NetworkCycleData.Builder()
.setStartTime(start).setEndTime(end).setTotalUsage(usage).build();
}
}

View File

@@ -1,249 +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 static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Intent;
import android.net.NetworkTemplate;
import android.os.Bundle;
import android.os.UserManager;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.loader.app.LoaderManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.android.settings.datausage.lib.BillingCycleRepository;
import com.android.settings.network.MobileDataEnabledListener;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.NetworkPolicyEditor;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
import com.android.settingslib.net.NetworkCycleChartData;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
import java.util.Collections;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = DataUsageListTest.ShadowDataUsageBaseFragment.class)
public class DataUsageListTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
private MobileDataEnabledListener mMobileDataEnabledListener;
@Mock
private TemplatePreference.NetworkServices mNetworkServices;
@Mock
private LoaderManager mLoaderManager;
@Mock
private UserManager mUserManager;
@Mock
private BillingCycleRepository mBillingCycleRepository;
@Mock
private DataUsageListHeaderController mDataUsageListHeaderController;
private Activity mActivity;
@Spy
private TestDataUsageList mDataUsageList;
@Before
public void setUp() {
FakeFeatureFactory.setupForTest();
final ActivityController<Activity> mActivityController =
Robolectric.buildActivity(Activity.class);
mActivity = spy(mActivityController.get());
mNetworkServices.mPolicyEditor = mock(NetworkPolicyEditor.class);
mDataUsageList.mDataStateListener = mMobileDataEnabledListener;
doReturn(mActivity).when(mDataUsageList).getContext();
doReturn(mUserManager).when(mActivity).getSystemService(UserManager.class);
doReturn(false).when(mUserManager).isGuestUser();
ReflectionHelpers.setField(mDataUsageList, "mDataStateListener",
mMobileDataEnabledListener);
ReflectionHelpers.setField(mDataUsageList, "services", mNetworkServices);
doReturn(mLoaderManager).when(mDataUsageList).getLoaderManager();
mDataUsageList.mLoadingViewController = mock(LoadingViewController.class);
doNothing().when(mDataUsageList).updateSubscriptionInfoEntity();
when(mBillingCycleRepository.isBandwidthControlEnabled()).thenReturn(true);
mDataUsageList.mDataUsageListHeaderController = mDataUsageListHeaderController;
}
@Test
public void onCreate_isNotGuestUser_shouldNotFinish() {
mDataUsageList.mTemplate = mock(NetworkTemplate.class);
doReturn(false).when(mUserManager).isGuestUser();
doNothing().when(mDataUsageList).processArgument();
mDataUsageList.onCreate(null);
verify(mDataUsageList, never()).finish();
}
@Test
public void onCreate_isGuestUser_shouldFinish() {
doReturn(true).when(mUserManager).isGuestUser();
mDataUsageList.onCreate(null);
verify(mDataUsageList).finish();
}
@Test
public void resume_shouldListenDataStateChange() {
mDataUsageList.onCreate(null);
ReflectionHelpers.setField(
mDataUsageList, "mVisibilityLoggerMixin", mock(VisibilityLoggerMixin.class));
ReflectionHelpers.setField(
mDataUsageList, "mPreferenceManager", mock(PreferenceManager.class));
mDataUsageList.onResume();
verify(mMobileDataEnabledListener).start(anyInt());
mDataUsageList.onPause();
}
@Test
public void pause_shouldUnlistenDataStateChange() {
mDataUsageList.onCreate(null);
ReflectionHelpers.setField(
mDataUsageList, "mVisibilityLoggerMixin", mock(VisibilityLoggerMixin.class));
ReflectionHelpers.setField(
mDataUsageList, "mPreferenceManager", mock(PreferenceManager.class));
mDataUsageList.onResume();
mDataUsageList.onPause();
verify(mMobileDataEnabledListener).stop();
}
@Test
public void processArgument_shouldGetTemplateFromArgument() {
final Bundle args = new Bundle();
args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mock(NetworkTemplate.class));
args.putInt(DataUsageList.EXTRA_SUB_ID, 3);
mDataUsageList.setArguments(args);
mDataUsageList.processArgument();
assertThat(mDataUsageList.mTemplate).isNotNull();
assertThat(mDataUsageList.mSubId).isEqualTo(3);
}
@Test
public void processArgument_fromIntent_shouldGetTemplateFromIntent() {
final Intent intent = new Intent();
intent.putExtra(Settings.EXTRA_NETWORK_TEMPLATE, mock(NetworkTemplate.class));
intent.putExtra(Settings.EXTRA_SUB_ID, 3);
doReturn(intent).when(mDataUsageList).getIntent();
mDataUsageList.processArgument();
assertThat(mDataUsageList.mTemplate).isNotNull();
assertThat(mDataUsageList.mSubId).isEqualTo(3);
}
@Test
public void onLoadFinished_networkCycleDataCallback_shouldShowCycleSpinner() {
mDataUsageList.mTemplate = mock(NetworkTemplate.class);
mDataUsageList.onCreate(null);
mDataUsageList.updatePolicy();
List<NetworkCycleChartData> mockData = Collections.emptyList();
mDataUsageList.mNetworkCycleDataCallbacks.onLoadFinished(null, mockData);
verify(mDataUsageListHeaderController).updateCycleData(mockData);
verify(mDataUsageListHeaderController).setConfigButtonVisible(true);
}
@Test
public void onPause_shouldDestroyLoaders() {
mDataUsageList.onPause();
verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_CHART_DATA);
}
@Implements(DataUsageBaseFragment.class)
public static class ShadowDataUsageBaseFragment {
@Implementation
public void onCreate(Bundle icicle) {
// do nothing
}
}
public class TestDataUsageList extends DataUsageList {
@Override
protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {
return mock(clazz);
}
@Override
public <T extends Preference> T findPreference(CharSequence key) {
if (key.toString().equals("chart_data")) {
return (T) mock(ChartDataUsagePreference.class);
}
return (T) mock(Preference.class);
}
@Override
public Intent getIntent() {
return new Intent();
}
@NonNull
@Override
BillingCycleRepository createBillingCycleRepository() {
return mBillingCycleRepository;
}
@Override
boolean isBillingCycleModifiable() {
return true;
}
}
}

View File

@@ -0,0 +1,216 @@
/*
* 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 android.os.Bundle
import android.os.UserManager
import android.provider.Settings
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import com.android.settings.datausage.DataUsageListTest.ShadowDataUsageBaseFragment
import com.android.settings.datausage.TemplatePreference.NetworkServices
import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.network.MobileDataEnabledListener
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.NetworkPolicyEditor
import com.android.settingslib.core.AbstractPreferenceController
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
import org.robolectric.util.ReflectionHelpers
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowDataUsageBaseFragment::class])
class DataUsageListTest {
@get:Rule
val mockito: MockitoRule = MockitoJUnit.rule()
@Mock
private lateinit var mobileDataEnabledListener: MobileDataEnabledListener
@Mock
private lateinit var networkServices: NetworkServices
@Mock
private lateinit var userManager: UserManager
@Mock
private lateinit var billingCycleRepository: BillingCycleRepository
@Mock
private lateinit var dataUsageListHeaderController: DataUsageListHeaderController
@Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@Spy
private val dataUsageList = TestDataUsageList()
@Before
fun setUp() {
FakeFeatureFactory.setupForTest()
networkServices.mPolicyEditor = mock(NetworkPolicyEditor::class.java)
dataUsageList.dataStateListener = mobileDataEnabledListener
doReturn(context).`when`(dataUsageList).context
doReturn(userManager).`when`(context).getSystemService(UserManager::class.java)
doReturn(false).`when`(userManager).isGuestUser
ReflectionHelpers.setField(dataUsageList, "services", networkServices)
doNothing().`when`(dataUsageList).updateSubscriptionInfoEntity()
`when`(billingCycleRepository.isBandwidthControlEnabled()).thenReturn(true)
dataUsageList.dataUsageListHeaderController = dataUsageListHeaderController
}
@Test
fun onCreate_isNotGuestUser_shouldNotFinish() {
dataUsageList.template = mock<NetworkTemplate>(NetworkTemplate::class.java)
doReturn(false).`when`(userManager).isGuestUser
doNothing().`when`(dataUsageList).processArgument()
dataUsageList.onCreate(null)
verify(dataUsageList, never()).finish()
}
@Test
fun onCreate_isGuestUser_shouldFinish() {
doReturn(true).`when`(userManager).isGuestUser
dataUsageList.onCreate(null)
verify(dataUsageList).finish()
}
@Test
fun resume_shouldListenDataStateChange() {
dataUsageList.template = mock(NetworkTemplate::class.java)
dataUsageList.onCreate(null)
dataUsageList.dataStateListener = mobileDataEnabledListener
ReflectionHelpers.setField(
dataUsageList,
"mVisibilityLoggerMixin",
mock(VisibilityLoggerMixin::class.java),
)
ReflectionHelpers.setField(
dataUsageList,
"mPreferenceManager",
mock(PreferenceManager::class.java),
)
dataUsageList.onResume()
verify(mobileDataEnabledListener).start(ArgumentMatchers.anyInt())
dataUsageList.onPause()
}
@Test
fun pause_shouldUnlistenDataStateChange() {
dataUsageList.template = mock(NetworkTemplate::class.java)
dataUsageList.onCreate(null)
dataUsageList.dataStateListener = mobileDataEnabledListener
ReflectionHelpers.setField(
dataUsageList, "mVisibilityLoggerMixin", mock(
VisibilityLoggerMixin::class.java
)
)
ReflectionHelpers.setField(
dataUsageList, "mPreferenceManager", mock(
PreferenceManager::class.java
)
)
dataUsageList.onResume()
dataUsageList.onPause()
verify(mobileDataEnabledListener).stop()
}
@Test
fun processArgument_shouldGetTemplateFromArgument() {
val args = Bundle()
args.putParcelable(
DataUsageList.EXTRA_NETWORK_TEMPLATE, mock(
NetworkTemplate::class.java
)
)
args.putInt(DataUsageList.EXTRA_SUB_ID, 3)
dataUsageList.arguments = args
dataUsageList.processArgument()
assertThat(dataUsageList.template).isNotNull()
assertThat(dataUsageList.subId).isEqualTo(3)
}
@Test
fun processArgument_fromIntent_shouldGetTemplateFromIntent() {
val intent = Intent()
intent.putExtra(
Settings.EXTRA_NETWORK_TEMPLATE, mock(
NetworkTemplate::class.java
)
)
intent.putExtra(Settings.EXTRA_SUB_ID, 3)
doReturn(intent).`when`(dataUsageList).intent
dataUsageList.processArgument()
assertThat(dataUsageList.template).isNotNull()
assertThat(dataUsageList.subId).isEqualTo(3)
}
@Test
fun updatePolicy_setConfigButtonVisible() {
dataUsageList.template = mock(NetworkTemplate::class.java)
dataUsageList.onCreate(null)
dataUsageList.updatePolicy()
verify(dataUsageListHeaderController).setConfigButtonVisible(true)
}
@Implements(DataUsageBaseFragment::class)
class ShadowDataUsageBaseFragment {
@Implementation
fun onCreate(@Suppress("UNUSED_PARAMETER") icicle: Bundle?) {
// do nothing
}
}
open inner class TestDataUsageList : DataUsageList() {
override fun <T : AbstractPreferenceController?> use(clazz: Class<T>): T = mock(clazz)
@Suppress("UNCHECKED_CAST")
override fun <T : Preference?> findPreference(key: CharSequence): T =
mock(Preference::class.java) as T
public override fun getIntent() = Intent()
override fun createBillingCycleRepository() = billingCycleRepository
override fun isBillingCycleModifiable() = true
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.PreferenceScreen
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.datausage.lib.INetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkCycleChartData
import com.android.settings.datausage.lib.NetworkUsageData
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class ChartDataUsagePreferenceControllerTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val repository = object : INetworkCycleDataRepository {
override suspend fun loadCycles() = emptyList<NetworkUsageData>()
override fun getPolicy() = null
override suspend fun querySummary(startTime: Long, endTime: Long) = when {
startTime == START_TIME && endTime == END_TIME -> CycleChartDate
else -> null
}
}
private val preference = mock<ChartDataUsagePreference>()
private val preferenceScreen = mock<PreferenceScreen> {
onGeneric { findPreference(KEY) } doReturn preference
}
private val controller = ChartDataUsagePreferenceController(context, KEY)
@Before
fun setUp() {
controller.init(repository)
controller.displayPreference(preferenceScreen)
controller.onViewCreated(TestLifecycleOwner())
}
@Test
fun update() = runBlocking {
controller.update(START_TIME, END_TIME)
delay(100L)
verify(preference).setTime(START_TIME, END_TIME)
verify(preference).setNetworkCycleData(CycleChartDate)
}
private companion object {
const val KEY = "test_key"
const val START_TIME = 1L
const val END_TIME = 2L
val UsageData = NetworkUsageData(startTime = START_TIME, endTime = END_TIME, usage = 10)
val CycleChartDate =
NetworkCycleChartData(total = UsageData, dailyUsage = listOf(UsageData))
}
}

View File

@@ -22,8 +22,8 @@ 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.settings.datausage.lib.NetworkUsageData
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
@@ -48,11 +48,8 @@ class DataUsageListAppsControllerTest {
@Before
fun setUp() {
controller.init(mock<NetworkTemplate>())
val data = NetworkCycleChartData.Builder().apply {
setStartTime(START_TIME)
setEndTime(END_TIME)
}.build()
controller.setCycleData(listOf(data))
val data = NetworkUsageData(START_TIME, END_TIME, 0)
controller.updateCycles(listOf(data))
}
@Test

View File

@@ -21,10 +21,16 @@ import android.net.NetworkTemplate
import android.view.LayoutInflater
import android.view.View
import android.widget.Spinner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.datausage.lib.INetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkUsageData
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -40,6 +46,14 @@ class DataUsageListHeaderControllerTest {
doNothing().whenever(mock).startActivity(any())
}
private val repository = object : INetworkCycleDataRepository {
override suspend fun loadCycles() = emptyList<NetworkUsageData>()
override fun getPolicy() = null
override suspend fun querySummary(startTime: Long, endTime: Long) = null
}
private val header =
LayoutInflater.from(context).inflate(R.layout.apps_filter_spinner, null, false)
@@ -47,11 +61,16 @@ class DataUsageListHeaderControllerTest {
private val spinner: Spinner = header.requireViewById(R.id.filter_spinner)
private val testLifecycleOwner = TestLifecycleOwner(initialState = Lifecycle.State.CREATED)
private val controller = DataUsageListHeaderController(
header = header,
template = mock<NetworkTemplate>(),
sourceMetricsCategory = 0,
onItemSelected = { _, _ -> },
viewLifecycleOwner = testLifecycleOwner,
onCyclesLoad = {},
onItemSelected = {},
repository = repository,
)
@Test
@@ -60,8 +79,9 @@ class DataUsageListHeaderControllerTest {
}
@Test
fun updateCycleData_shouldShowCycleSpinner() {
controller.updateCycleData(emptyList())
fun updateCycleData_shouldShowCycleSpinner() = runBlocking {
testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
delay(100)
assertThat(spinner.visibility).isEqualTo(View.VISIBLE)
}

View File

@@ -0,0 +1,168 @@
/*
* 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.Bucket
import android.app.usage.NetworkStatsManager
import android.content.Context
import android.net.NetworkPolicy
import android.net.NetworkTemplate
import android.text.format.DateUtils
import android.util.Range
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class NetworkCycleDataRepositoryTest {
private val mockNetworkStatsManager = mock<NetworkStatsManager> {
on { querySummaryForDevice(any(), eq(CYCLE1_START_TIME), eq(CYCLE1_END_TIME)) } doReturn
CYCLE1_BUCKET
on {
querySummaryForDevice(
any(),
eq(CYCLE2_END_TIME - DateUtils.WEEK_IN_MILLIS * 4),
eq(CYCLE2_END_TIME),
)
} doReturn CYCLE2_BUCKET
on { querySummaryForDevice(any(), eq(CYCLE3_START_TIME), eq(CYCLE4_END_TIME)) } doReturn
CYCLE3_AND_4_BUCKET
on { querySummaryForDevice(any(), eq(CYCLE3_START_TIME), eq(CYCLE3_END_TIME)) } doReturn
CYCLE3_BUCKET
on { querySummaryForDevice(any(), eq(CYCLE4_START_TIME), eq(CYCLE4_END_TIME)) } doReturn
CYCLE4_BUCKET
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { getSystemService(NetworkStatsManager::class.java) } doReturn mockNetworkStatsManager
}
private val template = mock<NetworkTemplate>()
private val repository = spy(NetworkCycleDataRepository(context, template))
@Test
fun loadCycles_byPolicy() = runTest {
val policy = mock<NetworkPolicy> {
on { cycleIterator() } doReturn listOf(
Range(zonedDateTime(CYCLE1_START_TIME), zonedDateTime(CYCLE1_END_TIME))
).iterator()
}
doReturn(policy).whenever(repository).getPolicy()
val cycles = repository.loadCycles()
assertThat(cycles).containsExactly(NetworkUsageData(startTime = 1, endTime = 2, usage = 11))
}
@Test
fun loadCycles_asFourWeeks() = runTest {
doReturn(null).whenever(repository).getPolicy()
doReturn(Range(CYCLE2_START_TIME, CYCLE2_END_TIME)).whenever(repository).getTimeRange()
val cycles = repository.loadCycles()
assertThat(cycles).containsExactly(
NetworkUsageData(
startTime = CYCLE2_END_TIME - DateUtils.WEEK_IN_MILLIS * 4,
endTime = CYCLE2_END_TIME,
usage = 22,
),
)
}
@Test
fun querySummary() = runTest {
val summary = repository.querySummary(CYCLE3_START_TIME, CYCLE4_END_TIME)
assertThat(summary).isEqualTo(
NetworkCycleChartData(
total = NetworkUsageData(
startTime = CYCLE3_START_TIME,
endTime = CYCLE4_END_TIME,
usage = 77,
),
dailyUsage = listOf(
NetworkUsageData(
startTime = CYCLE3_START_TIME,
endTime = CYCLE3_END_TIME,
usage = 33,
),
NetworkUsageData(
startTime = CYCLE4_START_TIME,
endTime = CYCLE4_END_TIME,
usage = 44,
),
),
)
)
}
private fun zonedDateTime(epochMilli: Long): ZonedDateTime? =
ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneId.systemDefault())
private companion object {
const val CYCLE1_START_TIME = 1L
const val CYCLE1_END_TIME = 2L
val CYCLE1_BUCKET = mock<Bucket> {
on { rxBytes } doReturn 1
on { txBytes } doReturn 10
}
const val CYCLE2_START_TIME = 1695555555000L
const val CYCLE2_END_TIME = 1695566666000L
val CYCLE2_BUCKET = mock<Bucket> {
on { rxBytes } doReturn 2
on { txBytes } doReturn 20
}
const val CYCLE3_START_TIME = 1695555555000L
const val CYCLE3_END_TIME = CYCLE3_START_TIME + DateUtils.DAY_IN_MILLIS
val CYCLE3_BUCKET = mock<Bucket> {
on { rxBytes } doReturn 3
on { txBytes } doReturn 30
}
const val CYCLE4_START_TIME = CYCLE3_END_TIME
const val CYCLE4_END_TIME = CYCLE4_START_TIME + DateUtils.DAY_IN_MILLIS
val CYCLE4_BUCKET = mock<Bucket> {
on { rxBytes } doReturn 4
on { txBytes } doReturn 40
}
val CYCLE3_AND_4_BUCKET = mock<Bucket> {
on { rxBytes } doReturn 7
on { txBytes } doReturn 70
}
}
}