diff --git a/res/xml/data_usage_list.xml b/res/xml/data_usage_list.xml index 791fc86647d..62456ed64a4 100644 --- a/res/xml/data_usage_list.xml +++ b/res/xml/data_usage_list.xml @@ -22,7 +22,9 @@ android:title="@string/summary_placeholder"> + android:key="chart_data" + settings:controller="com.android.settings.datausage.ChartDataUsagePreferenceController" + /> 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; diff --git a/src/com/android/settings/datausage/ChartDataUsagePreference.java b/src/com/android/settings/datausage/ChartDataUsagePreference.java index fa467d2a8cc..e5a7307996f 100644 --- a/src/com/android/settings/datausage/ChartDataUsagePreference.java +++ b/src/com/android/settings/datausage/ChartDataUsagePreference.java @@ -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 usageSummary) { - if (usageSummary == null) { - return; - } + void calcPoints(UsageView chart, @NonNull List 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 usageSummary) { + private void setupContentDescription( + UsageView chart, @NonNull List 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 getDensedStatsData(List usageSummary) { + List getDensedStatsData(@NonNull List usageSummary) { final List 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(); } } diff --git a/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt b/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt new file mode 100644 index 00000000000..0479be4ad16 --- /dev/null +++ b/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt @@ -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) + } + } +} diff --git a/src/com/android/settings/datausage/CycleAdapter.java b/src/com/android/settings/datausage/CycleAdapter.java index 90a2035c476..7cff05e1e64 100644 --- a/src/com/android/settings/datausage/CycleAdapter.java +++ b/src/com/android/settings/datausage/CycleAdapter.java @@ -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 * Rebuild list based on network data. Always selects the newest item, * updating the inspection range on chartData. */ - public void updateCycleList(List cycleData) { + public void updateCycleList(List> 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 cycle : cycleData) { + add(new CycleAdapter.CycleItem(context, cycle.getLower(), cycle.getUpper())); } // force pick the current cycle (first item) diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java deleted file mode 100644 index e7345ab02ee..00000000000 --- a/src/com/android/settings/datausage/DataUsageList.java +++ /dev/null @@ -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 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 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> mNetworkCycleDataCallbacks = - new LoaderCallbacks<>() { - @Override - @NonNull - public Loader> onCreateLoader(int id, Bundle args) { - return NetworkCycleChartDataLoader.builder(getContext()) - .setNetworkTemplate(mTemplate) - .build(); - } - - @Override - public void onLoadFinished(@NonNull Loader> loader, - List data) { - mLoadingViewController.showContent(false /* animate */); - mCycleData = data; - mDataUsageListHeaderController.updateCycleData(mCycleData); - updateConfigButtonVisibility(); - mDataUsageListAppsController.setCycleData(mCycleData); - } - - @Override - public void onLoaderReset(@NonNull Loader> 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(); - } -} diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt new file mode 100644 index 00000000000..9ac716185f0 --- /dev/null +++ b/src/com/android/settings/datausage/DataUsageList.kt @@ -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) { + 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" + } +} diff --git a/src/com/android/settings/datausage/DataUsageListAppsController.kt b/src/com/android/settings/datausage/DataUsageListAppsController.kt index c324407655a..93623f479e3 100644 --- a/src/com/android/settings/datausage/DataUsageListAppsController.kt +++ b/src/com/android/settings/datausage/DataUsageListAppsController.kt @@ -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? = null + private var cycleData: List? = 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?) { + fun updateCycles(cycleData: List) { this.cycleData = cycleData } diff --git a/src/com/android/settings/datausage/DataUsageListHeaderController.kt b/src/com/android/settings/datausage/DataUsageListHeaderController.kt index e295a4cff3b..58fc3b58ef6 100644 --- a/src/com/android/settings/datausage/DataUsageListHeaderController.kt +++ b/src/com/android/settings/datausage/DataUsageListHeaderController.kt @@ -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) -> 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 = 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) { + 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) } } diff --git a/src/com/android/settings/datausage/lib/NetworkCycleChartData.kt b/src/com/android/settings/datausage/lib/NetworkCycleChartData.kt new file mode 100644 index 00000000000..fd3c504e66f --- /dev/null +++ b/src/com/android/settings/datausage/lib/NetworkCycleChartData.kt @@ -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, +) { + companion object { + val BUCKET_DURATION = 1.days + } +} diff --git a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt new file mode 100644 index 00000000000..f10d506ec47 --- /dev/null +++ b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt @@ -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 + 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 = + getCycles().queryUsage().filter { it.usage > 0 } + + private fun getCycles(): List> { + val policy = getPolicy() ?: return queryCyclesAsFourWeeks() + return policy.cycleIterator().asSequence().map { + Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli()) + }.toList() + } + + private fun queryCyclesAsFourWeeks(): List> { + val timeRange = getTimeRange() + return reverseBucketRange( + startTime = timeRange.lower, + endTime = timeRange.upper, + bucketSize = DateUtils.WEEK_IN_MILLIS * 4, + ) + } + + @VisibleForTesting + fun getTimeRange(): Range = getTimeRangeOf( + networkStatsManager.queryDetailsForDevice(networkTemplate, Long.MIN_VALUE, Long.MAX_VALUE) + ) + + private fun getTimeRangeOf(stats: NetworkStats): Range { + 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>.queryUsage(): List = 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> { + val buckets = mutableListOf>() + 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> { + val buckets = mutableListOf>() + 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" + } +} diff --git a/src/com/android/settings/datausage/lib/NetworkUsageData.kt b/src/com/android/settings/datausage/lib/NetworkUsageData.kt new file mode 100644 index 00000000000..fc5db2beb75 --- /dev/null +++ b/src/com/android/settings/datausage/lib/NetworkUsageData.kt @@ -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, +) diff --git a/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java b/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java index aab67be1de9..9c936065679 100644 --- a/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java @@ -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 mNetworkCycleData; + private List mNetworkCycleData; private NetworkCycleChartData mNetworkCycleChartData; private ChartDataUsagePreference mPreference; private Activity mActivity; @@ -79,6 +79,9 @@ public class ChartDataUsagePreferenceTest { final ArgumentCaptor 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 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 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(); - } } diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java deleted file mode 100644 index 12680325614..00000000000 --- a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java +++ /dev/null @@ -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 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 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 use(Class clazz) { - return mock(clazz); - } - - @Override - public 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; - } - } -} diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt new file mode 100644 index 00000000000..90bb0487b3a --- /dev/null +++ b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt @@ -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::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 use(clazz: Class): T = mock(clazz) + + @Suppress("UNCHECKED_CAST") + override fun findPreference(key: CharSequence): T = + mock(Preference::class.java) as T + + public override fun getIntent() = Intent() + + override fun createBillingCycleRepository() = billingCycleRepository + + override fun isBillingCycleModifiable() = true + } +} diff --git a/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt new file mode 100644 index 00000000000..1748f07de6b --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt @@ -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() + + 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() + private val preferenceScreen = mock { + 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)) + } +} diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListAppsControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListAppsControllerTest.kt index af5dc893013..2646323c292 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListAppsControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListAppsControllerTest.kt @@ -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()) - 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 diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt index a1eebe79ade..35b70d6cc75 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt @@ -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() + + 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(), 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) } diff --git a/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt new file mode 100644 index 00000000000..fb5e8203f6b --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt @@ -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 { + 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() + + private val repository = spy(NetworkCycleDataRepository(context, template)) + + @Test + fun loadCycles_byPolicy() = runTest { + val policy = mock { + 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 { + on { rxBytes } doReturn 1 + on { txBytes } doReturn 10 + } + + const val CYCLE2_START_TIME = 1695555555000L + const val CYCLE2_END_TIME = 1695566666000L + val CYCLE2_BUCKET = mock { + 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 { + 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 { + on { rxBytes } doReturn 4 + on { txBytes } doReturn 40 + } + + val CYCLE3_AND_4_BUCKET = mock { + on { rxBytes } doReturn 7 + on { txBytes } doReturn 70 + } + } +}