diff --git a/res/layout/data_usage_summary_preference.xml b/res/layout/data_usage_summary_preference.xml index 24399ac5953..4cbd9581865 100644 --- a/res/layout/data_usage_summary_preference.xml +++ b/res/layout/data_usage_summary_preference.xml @@ -18,8 +18,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="22dp" - android:paddingBottom="32dp" + android:paddingTop="8dp" + android:paddingBottom="16dp" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:orientation="vertical" @@ -99,6 +99,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" + android:minHeight="54dp" android:orientation="vertical"> ): DataPlanInfo +} + +class DataPlanRepositoryImpl( + private val networkCycleDataRepository: INetworkCycleDataRepository, +) : DataPlanRepository { + override fun getDataPlanInfo( + policy: NetworkPolicy, + plans: List, + ): DataPlanInfo { + getPrimaryPlan(plans)?.let { primaryPlan -> + val dataPlanSize = when (primaryPlan.dataLimitBytes) { + SubscriptionPlan.BYTES_UNLIMITED -> SubscriptionPlan.BYTES_UNKNOWN + else -> primaryPlan.dataLimitBytes + } + return DataPlanInfo( + dataPlanCount = plans.size, + dataPlanSize = dataPlanSize, + dataBarSize = dataPlanSize, + dataPlanUse = primaryPlan.dataUsageBytes, + cycleEnd = primaryPlan.cycleRule.end?.toInstant()?.toEpochMilli(), + snapshotTime = primaryPlan.dataUsageTime, + ) + } + + val cycle = policy.getCycles().firstOrNull() + val dataUsage = networkCycleDataRepository.queryUsage( + cycle ?: NetworkStatsRepository.AllTimeRange + ).usage + return DataPlanInfo( + dataPlanCount = 0, + dataPlanSize = SubscriptionPlan.BYTES_UNKNOWN, + dataBarSize = maxOf(dataUsage, policy.limitBytes, policy.warningBytes), + dataPlanUse = dataUsage, + cycleEnd = cycle?.upper, + snapshotTime = SubscriptionPlan.TIME_UNKNOWN, + ) + } + + companion object { + private const val PETA = 1_000_000_000_000_000L + + private fun getPrimaryPlan(plans: List): SubscriptionPlan? = + plans.firstOrNull()?.takeIf { plan -> + plan.dataLimitBytes > 0 && validSize(plan.dataUsageBytes) && plan.cycleRule != null + } + + private fun validSize(value: Long): Boolean = value in 0L until PETA + } +} diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreference.java b/src/com/android/settings/datausage/DataUsageSummaryPreference.java index f2fcddd6eb9..93d930c5211 100644 --- a/src/com/android/settings/datausage/DataUsageSummaryPreference.java +++ b/src/com/android/settings/datausage/DataUsageSummaryPreference.java @@ -20,7 +20,6 @@ import android.annotation.AttrRes; import android.content.Context; import android.graphics.Typeface; import android.icu.text.MessageFormat; -import android.net.NetworkTemplate; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; @@ -32,13 +31,14 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; +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.settingslib.Utils; -import com.android.settingslib.net.DataUsageController; import com.android.settingslib.utils.StringUtil; import java.util.HashMap; @@ -62,10 +62,9 @@ public class DataUsageSummaryPreference extends Preference { private CharSequence mEndLabel; private int mNumPlans; - /** The specified un-initialized value for cycle time */ - private static final long CYCLE_TIME_UNINITIAL_VALUE = 0; /** The ending time of the billing cycle in milliseconds since epoch. */ - private long mCycleEndTimeMs; + @Nullable + private Long mCycleEndTimeMs; /** The time of the last update in standard milliseconds since the epoch */ private long mSnapshotTimeMs; /** Name of carrier, or null if not available */ @@ -74,7 +73,6 @@ public class DataUsageSummaryPreference extends Preference { /** Progress to display on ProgressBar */ private float mProgress; - private boolean mHasMobileData; /** * The size of the first registered plan if one exists or the size of the warning if it is set. @@ -102,7 +100,10 @@ public class DataUsageSummaryPreference extends Preference { notifyChanged(); } - public void setUsageInfo(long cycleEnd, long snapshotTime, CharSequence carrierName, + /** + * Sets the usage info. + */ + public void setUsageInfo(@Nullable Long cycleEnd, long snapshotTime, CharSequence carrierName, int numPlans) { mCycleEndTimeMs = cycleEnd; mSnapshotTimeMs = snapshotTime; @@ -124,15 +125,17 @@ public class DataUsageSummaryPreference extends Preference { notifyChanged(); } - void setUsageNumbers(long used, long dataPlanSize, boolean hasMobileData) { + /** + * Sets the usage numbers. + */ + public void setUsageNumbers(long used, long dataPlanSize) { mDataplanUse = used; mDataplanSize = dataPlanSize; - mHasMobileData = hasMobileData; notifyChanged(); } @Override - public void onBindViewHolder(PreferenceViewHolder holder) { + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); ProgressBar bar = getProgressBar(holder); @@ -178,7 +181,7 @@ public class DataUsageSummaryPreference extends Preference { final MeasurableLinearLayout layout = getLayout(holder); - if (mHasMobileData && mNumPlans >= 0 && mDataplanSize > 0L) { + if (mDataplanSize > 0L) { TextView usageRemainingField = getDataRemaining(holder); long dataRemaining = mDataplanSize - mDataplanUse; if (dataRemaining >= 0) { @@ -204,7 +207,7 @@ public class DataUsageSummaryPreference extends Preference { TextView cycleTime = getCycleTime(holder); // Takes zero as a special case which value is never set. - if (mCycleEndTimeMs == CYCLE_TIME_UNINITIAL_VALUE) { + if (mCycleEndTimeMs == null) { cycleTime.setVisibility(View.GONE); return; } @@ -228,7 +231,7 @@ public class DataUsageSummaryPreference extends Preference { private void updateCarrierInfo(TextView carrierInfo) { - if (mNumPlans > 0 && mSnapshotTimeMs >= 0L) { + if (mSnapshotTimeMs >= 0L) { carrierInfo.setVisibility(View.VISIBLE); long updateAgeMillis = calculateTruncatedUpdateAge(); @@ -293,13 +296,6 @@ public class DataUsageSummaryPreference extends Preference { carrierInfo.setTypeface(typeface); } - @VisibleForTesting - protected long getHistoricalUsageLevel() { - final DataUsageController controller = new DataUsageController(getContext()); - return controller.getHistoricalUsageLevel( - new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()); - } - @VisibleForTesting protected TextView getUsageTitle(PreferenceViewHolder holder) { return (TextView) holder.findViewById(R.id.usage_title); diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java deleted file mode 100644 index 35f5931c00d..00000000000 --- a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java +++ /dev/null @@ -1,273 +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.content.Context; -import android.net.NetworkTemplate; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionPlan; -import android.text.TextUtils; -import android.util.Log; -import android.util.RecurrenceRule; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; - -import com.android.internal.util.CollectionUtils; -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.datausage.lib.DataUsageLib; -import com.android.settings.network.ProxySubscriptionManager; -import com.android.settings.network.telephony.TelephonyBasePreferenceController; -import com.android.settingslib.net.DataUsageController; -import com.android.settingslib.utils.ThreadUtils; - -import java.util.List; -import java.util.concurrent.Future; - -/** - * This is the controller for a data usage header that retrieves carrier data from the new - * subscriptions framework API if available. The controller reads subscription information from the - * framework and falls back to legacy usage data if none are available. - */ -public class DataUsageSummaryPreferenceController extends TelephonyBasePreferenceController - implements PreferenceControllerMixin { - - private static final String TAG = "DataUsageController"; - private static final String KEY = "status_header"; - private static final long PETA = 1000000000000000L; - - protected DataUsageController mDataUsageController; - protected DataUsageInfoController mDataInfoController; - private NetworkTemplate mDefaultTemplate; - private boolean mHasMobileData; - - /** Name of the carrier, or null if not available */ - private CharSequence mCarrierName; - - /** The number of registered plans, [0,N] */ - private int mDataplanCount; - - /** The time of the last update in milliseconds since the epoch, or -1 if unknown */ - private long mSnapshotTime; - - /** - * The size of the first registered plan if one exists or the size of the warning if it is set. - * -1 if no information is available. - */ - private long mDataplanSize; - /** The "size" of the data usage bar, i.e. the amount of data its rhs end represents */ - private long mDataBarSize; - /** The number of bytes used since the start of the cycle. */ - private long mDataplanUse; - /** The ending time of the billing cycle in ms since the epoch */ - private long mCycleEnd; - - private Future mHistoricalUsageLevel; - - public DataUsageSummaryPreferenceController(Activity activity, int subscriptionId) { - super(activity, KEY); - - init(subscriptionId); - } - - /** - * Initialize based on subscription ID provided - * @param subscriptionId is the target subscriptionId - */ - public void init(int subscriptionId) { - mSubId = subscriptionId; - mHasMobileData = DataUsageUtils.hasMobileData(mContext); - mDataUsageController = null; - } - - protected void updateConfiguration(Context context, - int subscriptionId, SubscriptionInfo subInfo) { - mDataUsageController = createDataUsageController(context); - mDataUsageController.setSubscriptionId(subscriptionId); - mDataInfoController = new DataUsageInfoController(); - - if (subInfo != null) { - mDefaultTemplate = DataUsageLib.getMobileTemplate(context, subscriptionId); - } - } - - @VisibleForTesting - DataUsageController createDataUsageController(Context context) { - return new DataUsageController(context); - } - - @VisibleForTesting - DataUsageSummaryPreferenceController( - DataUsageController dataUsageController, - DataUsageInfoController dataInfoController, - NetworkTemplate defaultTemplate, - Activity activity, - int subscriptionId) { - super(activity, KEY); - mDataUsageController = dataUsageController; - mDataInfoController = dataInfoController; - mDefaultTemplate = defaultTemplate; - mHasMobileData = true; - mSubId = subscriptionId; - } - - @VisibleForTesting - List getSubscriptionPlans(int subscriptionId) { - return ProxySubscriptionManager.getInstance(mContext).get() - .getSubscriptionPlans(subscriptionId); - } - - protected SubscriptionInfo getSubscriptionInfo(int subscriptionId) { - if (!mHasMobileData) { - return null; - } - return ProxySubscriptionManager.getInstance(mContext) - .getAccessibleSubscriptionInfo(subscriptionId); - } - - @Override - public int getAvailabilityStatus(int subId) { - return getSubscriptionInfo(subId) != null ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; - } - - @Override - public void updateState(Preference preference) { - DataUsageSummaryPreference summaryPreference = (DataUsageSummaryPreference) preference; - - final SubscriptionInfo subInfo = getSubscriptionInfo(mSubId); - if (subInfo == null) { - return; - } - if (mDataUsageController == null) { - updateConfiguration(mContext, mSubId, subInfo); - } - - mHistoricalUsageLevel = ThreadUtils.postOnBackgroundThread(() -> - mDataUsageController.getHistoricalUsageLevel(mDefaultTemplate)); - - final DataUsageController.DataUsageInfo info = - mDataUsageController.getDataUsageInfo(mDefaultTemplate); - - long usageLevel = info.usageLevel; - - refreshDataplanInfo(info, subInfo); - - if (info.warningLevel > 0 && info.limitLevel > 0) { - summaryPreference.setLimitInfo(TextUtils.expandTemplate( - mContext.getText(R.string.cell_data_warning_and_limit), - DataUsageUtils.formatDataUsage(mContext, info.warningLevel), - DataUsageUtils.formatDataUsage(mContext, info.limitLevel))); - } else if (info.warningLevel > 0) { - summaryPreference.setLimitInfo(TextUtils.expandTemplate( - mContext.getText(R.string.cell_data_warning), - DataUsageUtils.formatDataUsage(mContext, info.warningLevel))); - } else if (info.limitLevel > 0) { - summaryPreference.setLimitInfo(TextUtils.expandTemplate( - mContext.getText(R.string.cell_data_limit), - DataUsageUtils.formatDataUsage(mContext, info.limitLevel))); - } else { - summaryPreference.setLimitInfo(null); - } - - if ((mDataplanUse <= 0L) && (mSnapshotTime < 0)) { - Log.d(TAG, "Display data usage from history"); - mDataplanUse = displayUsageLevel(usageLevel); - mSnapshotTime = -1L; - } - - summaryPreference.setUsageNumbers(mDataplanUse, mDataplanSize, mHasMobileData); - - if (mDataBarSize <= 0) { - summaryPreference.setChartEnabled(false); - } else { - summaryPreference.setChartEnabled(true); - summaryPreference.setLabels(DataUsageUtils.formatDataUsage(mContext, 0 /* sizeBytes */), - DataUsageUtils.formatDataUsage(mContext, mDataBarSize)); - summaryPreference.setProgress(mDataplanUse / (float) mDataBarSize); - } - summaryPreference.setUsageInfo(mCycleEnd, mSnapshotTime, mCarrierName, mDataplanCount); - } - - private long displayUsageLevel(long usageLevel) { - if (usageLevel > 0) { - return usageLevel; - } - try { - usageLevel = mHistoricalUsageLevel.get(); - } catch (Exception ex) { - } - return usageLevel; - } - - // TODO(b/70950124) add test for this method once the robolectric shadow run script is - // completed (b/3526807) - private void refreshDataplanInfo(DataUsageController.DataUsageInfo info, - SubscriptionInfo subInfo) { - // reset data before overwriting - mCarrierName = null; - mDataplanCount = 0; - mDataplanSize = -1L; - mDataBarSize = mDataInfoController.getSummaryLimit(info); - mDataplanUse = info.usageLevel; - mCycleEnd = info.cycleEnd; - mSnapshotTime = -1L; - - if (subInfo != null && mHasMobileData) { - mCarrierName = subInfo.getCarrierName(); - final List plans = getSubscriptionPlans(mSubId); - final SubscriptionPlan primaryPlan = getPrimaryPlan(plans); - - if (primaryPlan != null) { - mDataplanCount = plans.size(); - mDataplanSize = primaryPlan.getDataLimitBytes(); - if (unlimited(mDataplanSize)) { - mDataplanSize = -1L; - } - mDataBarSize = mDataplanSize; - mDataplanUse = primaryPlan.getDataUsageBytes(); - - RecurrenceRule rule = primaryPlan.getCycleRule(); - if (rule != null && rule.start != null && rule.end != null) { - mCycleEnd = rule.end.toEpochSecond() * 1000L; - } - mSnapshotTime = primaryPlan.getDataUsageTime(); - } - } - Log.i(TAG, "Have " + mDataplanCount + " plans, dflt sub-id " + mSubId); - } - - private static SubscriptionPlan getPrimaryPlan(List plans) { - if (CollectionUtils.isEmpty(plans)) { - return null; - } - // First plan in the list is the primary plan - SubscriptionPlan plan = plans.get(0); - return plan.getDataLimitBytes() > 0 - && validSize(plan.getDataUsageBytes()) - && plan.getCycleRule() != null ? plan : null; - } - - private static boolean validSize(long value) { - return value >= 0L && value < PETA; - } - - public static boolean unlimited(long size) { - return size == SubscriptionPlan.BYTES_UNLIMITED; - } -} diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt new file mode 100644 index 00000000000..8b31f6755c1 --- /dev/null +++ b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2024 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.NetworkPolicy +import android.net.NetworkTemplate +import android.text.TextUtils +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.datausage.lib.DataUsageLib.getMobileTemplate +import com.android.settings.datausage.lib.INetworkCycleDataRepository +import com.android.settings.datausage.lib.NetworkCycleDataRepository +import com.android.settings.network.ProxySubscriptionManager +import com.android.settings.network.telephony.TelephonyBasePreferenceController +import kotlin.math.max +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * This is the controller for a data usage header that retrieves carrier data from the new + * subscriptions framework API if available. The controller reads subscription information from the + * framework and falls back to legacy usage data if none are available. + */ +open class DataUsageSummaryPreferenceController @JvmOverloads constructor( + context: Context, + subId: Int, + private val proxySubscriptionManager: ProxySubscriptionManager = + ProxySubscriptionManager.getInstance(context), + private val networkCycleDataRepositoryFactory: ( + template: NetworkTemplate, + ) -> INetworkCycleDataRepository = { NetworkCycleDataRepository(context, it) }, + private val dataPlanRepositoryFactory: ( + networkCycleDataRepository: INetworkCycleDataRepository, + ) -> DataPlanRepository = { DataPlanRepositoryImpl(it) } +) : TelephonyBasePreferenceController(context, KEY) { + + init { + mSubId = subId + } + + private val subInfo by lazy { + if (DataUsageUtils.hasMobileData(mContext)) { + proxySubscriptionManager.getAccessibleSubscriptionInfo(mSubId) + } else null + } + private val networkCycleDataRepository by lazy { + networkCycleDataRepositoryFactory(getMobileTemplate(mContext, mSubId)) + } + private val policy by lazy { networkCycleDataRepository.getPolicy() } + private lateinit var preference: DataUsageSummaryPreference + + override fun getAvailabilityStatus(subId: Int) = + if (subInfo != null && policy != null) AVAILABLE else CONDITIONALLY_UNAVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey)!! + policy?.let { + preference.setLimitInfo(it.getLimitInfo()) + val dataBarSize = max(it.limitBytes, it.warningBytes) + if (dataBarSize > NetworkPolicy.WARNING_DISABLED) { + setDataBarSize(dataBarSize) + } + } + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + update() + } + } + } + + private suspend fun update() { + val policy = policy ?: return + val dataPlanInfo = withContext(Dispatchers.Default) { + dataPlanRepositoryFactory(networkCycleDataRepository).getDataPlanInfo( + policy = policy, + plans = proxySubscriptionManager.get().getSubscriptionPlans(mSubId), + ) + } + Log.d(TAG, "dataPlanInfo: $dataPlanInfo") + preference.setUsageNumbers(dataPlanInfo.dataPlanUse, dataPlanInfo.dataPlanSize) + if (dataPlanInfo.dataBarSize > 0) { + preference.setChartEnabled(true) + setDataBarSize(dataPlanInfo.dataBarSize) + preference.setProgress(dataPlanInfo.dataPlanUse / dataPlanInfo.dataBarSize.toFloat()) + } else { + preference.setChartEnabled(false) + } + + preference.setUsageInfo( + dataPlanInfo.cycleEnd, + dataPlanInfo.snapshotTime, + subInfo?.carrierName, + dataPlanInfo.dataPlanCount, + ) + } + + private fun setDataBarSize(dataBarSize: Long) { + preference.setLabels( + DataUsageUtils.formatDataUsage(mContext, /* byteValue = */ 0), + DataUsageUtils.formatDataUsage(mContext, dataBarSize) + ) + } + + private fun NetworkPolicy.getLimitInfo(): CharSequence? = when { + warningBytes > 0 && limitBytes > 0 -> { + TextUtils.expandTemplate( + mContext.getText(R.string.cell_data_warning_and_limit), + DataUsageUtils.formatDataUsage(mContext, warningBytes), + DataUsageUtils.formatDataUsage(mContext, limitBytes), + ) + } + + warningBytes > 0 -> { + TextUtils.expandTemplate( + mContext.getText(R.string.cell_data_warning), + DataUsageUtils.formatDataUsage(mContext, warningBytes), + ) + } + + limitBytes > 0 -> { + TextUtils.expandTemplate( + mContext.getText(R.string.cell_data_limit), + DataUsageUtils.formatDataUsage(mContext, limitBytes), + ) + } + + else -> null + } + + companion object { + private const val TAG = "DataUsageSummaryPC" + private const val KEY = "status_header" + } +} diff --git a/src/com/android/settings/datausage/lib/NetworkCycleBucketRepository.kt b/src/com/android/settings/datausage/lib/NetworkCycleBucketRepository.kt index 652919e0a7a..7e3e1832592 100644 --- a/src/com/android/settings/datausage/lib/NetworkCycleBucketRepository.kt +++ b/src/com/android/settings/datausage/lib/NetworkCycleBucketRepository.kt @@ -21,6 +21,7 @@ import android.net.NetworkTemplate import android.text.format.DateUtils import android.util.Range import com.android.settings.datausage.lib.NetworkCycleDataRepository.Companion.bucketRange +import com.android.settings.datausage.lib.NetworkCycleDataRepository.Companion.getCycles import com.android.settings.datausage.lib.NetworkCycleDataRepository.Companion.reverseBucketRange import com.android.settings.datausage.lib.NetworkStatsRepository.Companion.Bucket import com.android.settings.datausage.lib.NetworkStatsRepository.Companion.aggregate @@ -37,12 +38,8 @@ class NetworkCycleBucketRepository( fun loadCycles(): List = getCycles().map { aggregateUsage(it) }.filter { it.usage > 0 } - private fun getCycles(): List> { - val policy = networkCycleDataRepository.getPolicy() ?: return queryCyclesAsFourWeeks() - return policy.cycleIterator().asSequence().map { - Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli()) - }.toList() - } + private fun getCycles(): List> = + networkCycleDataRepository.getPolicy()?.getCycles() ?: queryCyclesAsFourWeeks() private fun queryCyclesAsFourWeeks(): List> { val timeRange = buckets.aggregate()?.timeRange ?: return emptyList() diff --git a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt index cde64dff02a..31052ef8763 100644 --- a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt +++ b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt @@ -27,6 +27,7 @@ import com.android.settingslib.NetworkPolicyEditor interface INetworkCycleDataRepository { fun getCycles(): List> fun getPolicy(): NetworkPolicy? + fun queryUsage(range: Range): NetworkUsageData } class NetworkCycleDataRepository( @@ -40,12 +41,8 @@ class NetworkCycleDataRepository( fun loadFirstCycle(): NetworkUsageData? = getCycles().firstOrNull()?.let { queryUsage(it) } - override fun getCycles(): List> { - val policy = getPolicy() ?: return queryCyclesAsFourWeeks() - return policy.cycleIterator().asSequence().map { - Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli()) - }.toList() - } + override fun getCycles(): List> = + getPolicy()?.getCycles() ?: queryCyclesAsFourWeeks() private fun queryCyclesAsFourWeeks(): List> { val timeRange = networkStatsRepository.getTimeRange() ?: return emptyList() @@ -63,13 +60,17 @@ class NetworkCycleDataRepository( } - fun queryUsage(range: Range) = NetworkUsageData( + override fun queryUsage(range: Range) = NetworkUsageData( startTime = range.lower, endTime = range.upper, usage = networkStatsRepository.querySummaryForDevice(range.lower, range.upper), ) companion object { + fun NetworkPolicy.getCycles() = cycleIterator().asSequence().map { + Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli()) + }.toList() + fun bucketRange(startTime: Long, endTime: Long, step: Long): List> = (startTime..endTime step step).zipWithNext(::Range) diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 16b04aa356d..b4b40efae9b 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -172,7 +172,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme }); return Arrays.asList( - new DataUsageSummaryPreferenceController(getActivity(), mSubId), + new DataUsageSummaryPreferenceController(context, mSubId), new RoamingPreferenceController(context, KEY_ROAMING_PREF, getSettingsLifecycle(), this, mSubId), new CallsDefaultSubscriptionController(context, KEY_CALLS_PREF, @@ -229,11 +229,6 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme } - final DataUsageSummaryPreferenceController dataUsageSummaryPreferenceController = - use(DataUsageSummaryPreferenceController.class); - if (dataUsageSummaryPreferenceController != null) { - dataUsageSummaryPreferenceController.init(mSubId); - } use(MobileNetworkSwitchController.class).init(mSubId); use(CarrierSettingsVersionPreferenceController.class).init(mSubId); use(BillingCyclePreferenceController.class).init(mSubId); diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.java deleted file mode 100644 index a9298737f4d..00000000000 --- a/tests/robotests/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.java +++ /dev/null @@ -1,352 +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.android.settings.core.BasePreferenceController.AVAILABLE; -import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.net.NetworkTemplate; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.SubscriptionPlan; -import android.telephony.TelephonyManager; -import android.util.RecurrenceRule; - -import androidx.fragment.app.FragmentActivity; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.testutils.FakeFeatureFactory; -import com.android.settings.testutils.shadow.ShadowEntityHeaderController; -import com.android.settings.widget.EntityHeaderController; -import com.android.settingslib.net.DataUsageController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -import java.time.Instant; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -@RunWith(RobolectricTestRunner.class) -@Config(shadows = ShadowEntityHeaderController.class) -public class DataUsageSummaryPreferenceControllerTest { - - private static final long UPDATE_BACKOFF_MS = TimeUnit.MINUTES.toMillis(13); - private static final long CYCLE_BACKOFF_MS = TimeUnit.DAYS.toMillis(6); - private static final long CYCLE_LENGTH_MS = TimeUnit.DAYS.toMillis(30); - private static final long USAGE1 = 373 * BillingCycleSettings.MIB_IN_BYTES; - private static final long LIMIT1 = BillingCycleSettings.GIB_IN_BYTES; - private static final String CARRIER_NAME = "z-mobile"; - private static final String PERIOD = "Feb"; - - @Mock - private DataUsageController mDataUsageController; - @Mock - private DataUsageSummaryPreference mSummaryPreference; - @Mock - private NetworkTemplate mNetworkTemplate; - @Mock - private SubscriptionInfo mSubscriptionInfo; - @Mock - private SubscriptionPlan mSubscriptionPlan; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private EntityHeaderController mHeaderController; - @Mock - private TelephonyManager mTelephonyManager; - @Mock - private PackageManager mPm; - - private DataUsageInfoController mDataInfoController; - - private FakeFeatureFactory mFactory; - private FragmentActivity mActivity; - private Context mContext; - private DataUsageSummaryPreferenceController mController; - private int mDefaultSubscriptionId; - private List mSubscriptionPlans; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); - - doReturn("%1$s %2%s").when(mContext) - .getString(com.android.internal.R.string.fileSizeSuffix); - - mDefaultSubscriptionId = 1234; - mSubscriptionPlans = new ArrayList(); - - mFactory = FakeFeatureFactory.setupForTest(); - when(mFactory.metricsFeatureProvider.getMetricsCategory(any(Object.class))) - .thenReturn(MetricsProto.MetricsEvent.SETTINGS_APP_NOTIF_CATEGORY); - ShadowEntityHeaderController.setUseMock(mHeaderController); - - mDataInfoController = spy(new DataUsageInfoController()); - doReturn(-1L).when(mDataInfoController).getSummaryLimit(any()); - - mActivity = spy(Robolectric.buildActivity(FragmentActivity.class).get()); - doReturn(mTelephonyManager).when(mActivity).getSystemService(TelephonyManager.class); - doReturn(mTelephonyManager).when(mTelephonyManager) - .createForSubscriptionId(mDefaultSubscriptionId); - doReturn(mPm).when(mActivity).getPackageManager(); - doReturn(TelephonyManager.SIM_STATE_READY).when(mTelephonyManager).getSimState(); - - mController = spy(new DataUsageSummaryPreferenceController( - mDataUsageController, - mDataInfoController, - mNetworkTemplate, - mActivity, mDefaultSubscriptionId)); - doReturn(null).when(mController).getSubscriptionInfo( - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - doReturn(null).when(mController).getSubscriptionPlans( - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - - doReturn(CARRIER_NAME).when(mSubscriptionInfo).getCarrierName(); - doReturn(mSubscriptionInfo).when(mController).getSubscriptionInfo(mDefaultSubscriptionId); - doReturn(mSubscriptionPlans).when(mController).getSubscriptionPlans(mDefaultSubscriptionId); - } - - @After - public void tearDown() { - ShadowEntityHeaderController.reset(); - } - - @Test - public void testSummaryUpdate_onePlan_basic() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - setupTestDataUsage(LIMIT1, USAGE1, now - UPDATE_BACKOFF_MS); - createTestDataPlan(info.cycleStart, info.cycleEnd); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("512 MB data warning / 1.00 GB data limit"); - - // TODO (b/170330084): return intent instead of null for mSummaryPreference - verify(mSummaryPreference).setUsageInfo((info.cycleEnd / 1000) * 1000, - now - UPDATE_BACKOFF_MS, - CARRIER_NAME, 1 /* numPlans */); - verify(mSummaryPreference).setChartEnabled(true); - } - - @Test - public void testSummaryUpdate_noPlan_basic() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - setupTestDataUsage(LIMIT1, USAGE1, now - UPDATE_BACKOFF_MS); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("512 MB data warning / 1.00 GB data limit"); - - verify(mSummaryPreference).setUsageInfo( - info.cycleEnd, - -1L /* snapshotTime */, - CARRIER_NAME, - 0 /* numPlans */); - verify(mSummaryPreference).setChartEnabled(true); - } - - @Test - public void testSummaryUpdate_noCarrier_basic() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - doReturn(null).when(mSubscriptionInfo).getCarrierName(); - setupTestDataUsage(LIMIT1, USAGE1, -1L /* snapshotTime */); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("512 MB data warning / 1.00 GB data limit"); - - verify(mSummaryPreference).setUsageInfo( - info.cycleEnd, - -1L /* snapshotTime */, - null /* carrierName */, - 0 /* numPlans */); - verify(mSummaryPreference).setChartEnabled(true); - } - - @Test - public void testSummaryUpdate_noPlanData_basic() { - final long now = System.currentTimeMillis(); - - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - doReturn(null).when(mSubscriptionInfo).getCarrierName(); - setupTestDataUsage(-1L /* dataPlanSize */, USAGE1, -1L /* snapshotTime */); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("512 MB data warning / 1.00 GB data limit"); - verify(mSummaryPreference).setUsageInfo( - info.cycleEnd, - -1L /* snapshotTime */, - null /* carrierName */, - 0 /* numPlans */); - verify(mSummaryPreference).setChartEnabled(false); - } - - @Test - public void testSummaryUpdate_noLimitNoWarning() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - info.warningLevel = 0L; - info.limitLevel = 0L; - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - setupTestDataUsage(LIMIT1, USAGE1, now - UPDATE_BACKOFF_MS); - - mController.updateState(mSummaryPreference); - verify(mSummaryPreference).setLimitInfo(null); - } - - @Test - public void testSummaryUpdate_warningOnly() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - info.warningLevel = BillingCycleSettings.MIB_IN_BYTES; - info.limitLevel = 0L; - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - setupTestDataUsage(LIMIT1, USAGE1, now - UPDATE_BACKOFF_MS); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("1.00 MB data warning"); - } - - @Test - public void testSummaryUpdate_limitOnly() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - info.warningLevel = 0L; - info.limitLevel = BillingCycleSettings.MIB_IN_BYTES; - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - setupTestDataUsage(LIMIT1, USAGE1, now - UPDATE_BACKOFF_MS); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("1.00 MB data limit"); - } - - @Test - public void testSummaryUpdate_limitAndWarning() { - final long now = System.currentTimeMillis(); - final DataUsageController.DataUsageInfo info = createTestDataUsageInfo(now); - info.warningLevel = BillingCycleSettings.MIB_IN_BYTES; - info.limitLevel = BillingCycleSettings.MIB_IN_BYTES; - - doReturn(info).when(mDataUsageController).getDataUsageInfo(any()); - setupTestDataUsage(LIMIT1, USAGE1, now - UPDATE_BACKOFF_MS); - - mController.updateState(mSummaryPreference); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); - verify(mSummaryPreference).setLimitInfo(captor.capture()); - CharSequence value = captor.getValue(); - assertThat(value.toString()).isEqualTo("1.00 MB data warning / 1.00 MB data limit"); - } - - @Test - public void testMobileData_preferenceAvailable() { - assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); - } - - @Test - public void testMobileData_noSim_preferenceDisabled() { - final int subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; - mController.init(subscriptionId); - mController.mDataUsageController = mDataUsageController; - assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); - } - - private DataUsageController.DataUsageInfo createTestDataUsageInfo(long now) { - DataUsageController.DataUsageInfo info = new DataUsageController.DataUsageInfo(); - info.carrier = CARRIER_NAME; - info.period = PERIOD; - info.startDate = now; - info.limitLevel = LIMIT1; - info.warningLevel = LIMIT1 >> 1; - info.usageLevel = USAGE1; - info.cycleStart = now - CYCLE_BACKOFF_MS; - info.cycleEnd = info.cycleStart + CYCLE_LENGTH_MS; - return info; - } - - private void setupTestDataUsage(long dataPlanSize, long dataUsageSize, long snapshotTime) { - doReturn(dataPlanSize).when(mSubscriptionPlan).getDataLimitBytes(); - doReturn(dataUsageSize).when(mSubscriptionPlan).getDataUsageBytes(); - doReturn(snapshotTime).when(mSubscriptionPlan).getDataUsageTime(); - - doReturn(dataPlanSize).when(mDataInfoController).getSummaryLimit(any()); - } - - private void createTestDataPlan(long startTime, long endTime) { - final RecurrenceRule recurrenceRule = new RecurrenceRule( - Instant.ofEpochMilli(startTime).atZone(ZoneId.systemDefault()), - Instant.ofEpochMilli(endTime).atZone(ZoneId.systemDefault()), - null); - doReturn(recurrenceRule).when(mSubscriptionPlan).getCycleRule(); - mSubscriptionPlans.add(mSubscriptionPlan); - } -} diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataPlanRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataPlanRepositoryTest.kt new file mode 100644 index 00000000000..5ed14861b93 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/DataPlanRepositoryTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 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.net.NetworkPolicy +import android.telephony.SubscriptionPlan +import android.util.Range +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.datausage.lib.INetworkCycleDataRepository +import com.android.settings.datausage.lib.NetworkUsageData +import com.android.settings.testutils.zonedDateTime +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class DataPlanRepositoryTest { + + private object FakeNetworkCycleDataRepository : INetworkCycleDataRepository { + override fun getCycles(): List> = emptyList() + override fun getPolicy() = null + + override fun queryUsage(range: Range) = NetworkUsageData( + startTime = CYCLE_CYCLE_START_TIME, + endTime = CYCLE_CYCLE_END_TIME, + usage = CYCLE_BYTES, + ) + } + + private val repository = DataPlanRepositoryImpl(FakeNetworkCycleDataRepository) + + private val policy = mock { + on { cycleIterator() } doReturn listOf( + Range(zonedDateTime(CYCLE_CYCLE_START_TIME), zonedDateTime(CYCLE_CYCLE_END_TIME)), + ).iterator() + } + + @Test + fun getDataPlanInfo_hasSubscriptionPlan() { + val dataPlanInfo = repository.getDataPlanInfo(policy, listOf(SUBSCRIPTION_PLAN)) + + assertThat(dataPlanInfo).isEqualTo( + DataPlanInfo( + dataPlanCount = 1, + dataPlanSize = DATA_LIMIT_BYTES, + dataBarSize = DATA_LIMIT_BYTES, + dataPlanUse = DATA_USAGE_BYTES, + cycleEnd = PLAN_CYCLE_END_TIME, + snapshotTime = DATA_USAGE_TIME, + ) + ) + } + + @Test + fun getDataPlanInfo_noSubscriptionPlan() { + val dataPlanInfo = repository.getDataPlanInfo(policy, emptyList()) + + assertThat(dataPlanInfo).isEqualTo( + DataPlanInfo( + dataPlanCount = 0, + dataPlanSize = SubscriptionPlan.BYTES_UNKNOWN, + dataBarSize = CYCLE_BYTES, + dataPlanUse = CYCLE_BYTES, + cycleEnd = CYCLE_CYCLE_END_TIME, + snapshotTime = SubscriptionPlan.TIME_UNKNOWN, + ) + ) + } + + private companion object { + const val CYCLE_CYCLE_START_TIME = 1L + const val CYCLE_CYCLE_END_TIME = 2L + const val CYCLE_BYTES = 11L + + const val PLAN_CYCLE_START_TIME = 100L + const val PLAN_CYCLE_END_TIME = 200L + const val DATA_LIMIT_BYTES = 300L + const val DATA_USAGE_BYTES = 400L + const val DATA_USAGE_TIME = 500L + + val SUBSCRIPTION_PLAN: SubscriptionPlan = SubscriptionPlan.Builder.createNonrecurring( + zonedDateTime(PLAN_CYCLE_START_TIME), + zonedDateTime(PLAN_CYCLE_END_TIME), + ).apply { + setDataLimit(DATA_LIMIT_BYTES, SubscriptionPlan.LIMIT_BEHAVIOR_DISABLED) + setDataUsage(DATA_USAGE_BYTES, DATA_USAGE_TIME) + }.build() + } +} diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt new file mode 100644 index 00000000000..8ebb9c530ed --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2024 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.NetworkPolicy +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.SubscriptionPlan +import android.telephony.TelephonyManager +import android.util.Range +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.core.BasePreferenceController.AVAILABLE +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.datausage.lib.INetworkCycleDataRepository +import com.android.settings.datausage.lib.NetworkUsageData +import com.android.settings.network.ProxySubscriptionManager +import com.google.common.truth.Truth.assertThat +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.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class DataUsageSummaryPreferenceControllerTest { + + private var policy: NetworkPolicy? = mock() + + private val mockTelephonyManager = mock { + on { isDataCapable } doReturn true + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + } + + private val mockSubscriptionManager = mock { + on { getSubscriptionPlans(any()) } doReturn emptyList() + } + + private val mockProxySubscriptionManager = mock { + on { get() } doReturn mockSubscriptionManager + } + + private val fakeNetworkCycleDataRepository = object : INetworkCycleDataRepository { + override fun getCycles(): List> = emptyList() + override fun getPolicy() = policy + override fun queryUsage(range: Range) = NetworkUsageData.AllZero + } + + private var dataPlanInfo = EMPTY_DATA_PLAN_INFO + + private val fakeDataPlanRepository = object : DataPlanRepository { + override fun getDataPlanInfo(policy: NetworkPolicy, plans: List) = + dataPlanInfo + } + + private val controller = DataUsageSummaryPreferenceController( + context = context, + subId = SUB_ID, + proxySubscriptionManager = mockProxySubscriptionManager, + networkCycleDataRepositoryFactory = { fakeNetworkCycleDataRepository }, + dataPlanRepositoryFactory = { fakeDataPlanRepository }, + ) + + private val preference = mock { + on { key } doReturn controller.preferenceKey + } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + @Before + fun setUp() { + preferenceScreen.addPreference(preference) + } + + @Test + fun getAvailabilityStatus_noMobileData_conditionallyUnavailable() { + mockTelephonyManager.stub { + on { isDataCapable } doReturn false + } + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun getAvailabilityStatus_hasSubInfoAndPolicy_available() { + mockProxySubscriptionManager.stub { + on { getAccessibleSubscriptionInfo(SUB_ID) } doReturn SubscriptionInfo.Builder().build() + } + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(AVAILABLE) + } + + @Test + fun getAvailabilityStatus_noSubInfo_conditionallyUnavailable() { + mockProxySubscriptionManager.stub { + on { getAccessibleSubscriptionInfo(SUB_ID) } doReturn null + } + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun getAvailabilityStatus_noPolicy_conditionallyUnavailable() { + policy = null + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun displayPreference_policyHasNoLimitInfo() { + policy = mock().apply { + warningBytes = NetworkPolicy.WARNING_DISABLED + limitBytes = NetworkPolicy.LIMIT_DISABLED + } + + controller.displayPreference(preferenceScreen) + + verify(preference).setLimitInfo(null) + verify(preference, never()).setLabels(any(), any()) + } + + @Test + fun displayPreference_policyWarningOnly() { + policy = mock().apply { + warningBytes = 1L + limitBytes = NetworkPolicy.LIMIT_DISABLED + } + + controller.displayPreference(preferenceScreen) + + val limitInfo = argumentCaptor { + verify(preference).setLimitInfo(capture()) + }.firstValue.toString() + assertThat(limitInfo).isEqualTo("1 B data warning") + verify(preference).setLabels("0 B", "1 B") + } + + @Test + fun displayPreference_policyLimitOnly() { + policy = mock().apply { + warningBytes = NetworkPolicy.WARNING_DISABLED + limitBytes = 1L + } + + controller.displayPreference(preferenceScreen) + + val limitInfo = argumentCaptor { + verify(preference).setLimitInfo(capture()) + }.firstValue.toString() + assertThat(limitInfo).isEqualTo("1 B data limit") + verify(preference).setLabels("0 B", "1 B") + } + + @Test + fun displayPreference_policyHasWarningAndLimit() { + policy = mock().apply { + warningBytes = BillingCycleSettings.GIB_IN_BYTES / 2 + limitBytes = BillingCycleSettings.GIB_IN_BYTES + } + + controller.displayPreference(preferenceScreen) + + val limitInfo = argumentCaptor { + verify(preference).setLimitInfo(capture()) + }.firstValue.toString() + assertThat(limitInfo).isEqualTo("512 MB data warning / 1.00 GB data limit") + verify(preference).setLabels("0 B", "1.00 GB") + } + + @Test + fun onViewCreated_emptyDataPlanInfo() = runBlocking { + dataPlanInfo = EMPTY_DATA_PLAN_INFO + controller.displayPreference(preferenceScreen) + clearInvocations(preference) + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + verify(preference).setUsageNumbers( + EMPTY_DATA_PLAN_INFO.dataPlanUse, + EMPTY_DATA_PLAN_INFO.dataPlanSize, + ) + verify(preference).setChartEnabled(false) + verify(preference).setUsageInfo( + EMPTY_DATA_PLAN_INFO.cycleEnd, + EMPTY_DATA_PLAN_INFO.snapshotTime, + null, + EMPTY_DATA_PLAN_INFO.dataPlanCount, + ) + } + + @Test + fun onViewCreated_positiveDataPlanInfo() = runBlocking { + dataPlanInfo = POSITIVE_DATA_PLAN_INFO + controller.displayPreference(preferenceScreen) + clearInvocations(preference) + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + verify(preference).setUsageNumbers( + POSITIVE_DATA_PLAN_INFO.dataPlanUse, + POSITIVE_DATA_PLAN_INFO.dataPlanSize, + ) + verify(preference).setChartEnabled(true) + verify(preference).setLabels("0 B", "9 B") + val progress = argumentCaptor { + verify(preference).setProgress(capture()) + }.firstValue + assertThat(progress).isEqualTo(0.8888889f) + verify(preference).setUsageInfo( + POSITIVE_DATA_PLAN_INFO.cycleEnd, + POSITIVE_DATA_PLAN_INFO.snapshotTime, + null, + POSITIVE_DATA_PLAN_INFO.dataPlanCount, + ) + } + + private companion object { + const val SUB_ID = 1234 + val EMPTY_DATA_PLAN_INFO = DataPlanInfo( + dataPlanCount = 0, + dataPlanSize = SubscriptionPlan.BYTES_UNKNOWN, + dataBarSize = SubscriptionPlan.BYTES_UNKNOWN, + dataPlanUse = 0, + cycleEnd = null, + snapshotTime = SubscriptionPlan.TIME_UNKNOWN, + ) + val POSITIVE_DATA_PLAN_INFO = DataPlanInfo( + dataPlanCount = 0, + dataPlanSize = 10L, + dataBarSize = 9L, + dataPlanUse = 8L, + cycleEnd = 7L, + snapshotTime = 6L, + ) + } +} 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 index f0a5309ed12..77fe843d8b3 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt @@ -23,10 +23,8 @@ import android.text.format.DateUtils import android.util.Range import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.testutils.zonedDateTime 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 @@ -96,9 +94,6 @@ class NetworkCycleDataRepositoryTest { ) } - 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 diff --git a/tests/spa_unit/src/com/android/settings/testutils/DateTimeTestUtil.kt b/tests/spa_unit/src/com/android/settings/testutils/DateTimeTestUtil.kt new file mode 100644 index 00000000000..2a5f137028b --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/testutils/DateTimeTestUtil.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 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.testutils + +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime + +fun zonedDateTime(epochMilli: Long): ZonedDateTime? = + ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneId.systemDefault()) diff --git a/tests/unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceTest.java b/tests/unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceTest.java index fe7759da400..d80c16ab2cc 100644 --- a/tests/unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceTest.java +++ b/tests/unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceTest.java @@ -17,6 +17,7 @@ package com.android.settings.datausage; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -90,7 +91,7 @@ public class DataUsageSummaryPreferenceTest { @Test public void testSetUsageInfo_withNoDataPlans_carrierInfoNotShown() { - mSummaryPreference.setUsageInfo(mCycleEnd, mUpdateTime, FAKE_CARRIER, 0 /* numPlans */); + mSummaryPreference.setUsageInfo(mCycleEnd, -1, FAKE_CARRIER, 0 /* numPlans */); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getCarrierInfo(mHolder).getVisibility()) @@ -197,7 +198,7 @@ public class DataUsageSummaryPreferenceTest { @Test public void testSetUsageInfo_withNoDataPlans_usageTitleNotShown() { - mSummaryPreference.setUsageInfo(mCycleEnd, mUpdateTime, FAKE_CARRIER, 0 /* numPlans */); + mSummaryPreference.setUsageInfo(mCycleEnd, -1, FAKE_CARRIER, 0 /* numPlans */); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getUsageTitle(mHolder).getVisibility()).isEqualTo(View.GONE); @@ -216,7 +217,7 @@ public class DataUsageSummaryPreferenceTest { public void testSetUsageInfo_cycleRemainingTimeIsLessOneDay() { // just under one day final long cycleEnd = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(23); - mSummaryPreference.setUsageInfo(cycleEnd, mUpdateTime, FAKE_CARRIER, 0 /* numPlans */); + mSummaryPreference.setUsageInfo(cycleEnd, -1, FAKE_CARRIER, 0 /* numPlans */); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getCycleTime(mHolder).getVisibility()) @@ -229,7 +230,7 @@ public class DataUsageSummaryPreferenceTest { @Test public void testSetUsageInfo_cycleRemainingTimeNegativeDaysLeft_shouldDisplayNoneLeft() { final long cycleEnd = System.currentTimeMillis() - 1L; - mSummaryPreference.setUsageInfo(cycleEnd, mUpdateTime, FAKE_CARRIER, 0 /* numPlans */); + mSummaryPreference.setUsageInfo(cycleEnd, -1, FAKE_CARRIER, 0 /* numPlans */); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getCycleTime(mHolder).getVisibility()) @@ -243,7 +244,7 @@ public class DataUsageSummaryPreferenceTest { final int daysLeft = 3; final long cycleEnd = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(daysLeft) + TimeUnit.HOURS.toMillis(1); - mSummaryPreference.setUsageInfo(cycleEnd, mUpdateTime, FAKE_CARRIER, 0 /* numPlans */); + mSummaryPreference.setUsageInfo(cycleEnd, -1, FAKE_CARRIER, 0 /* numPlans */); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getCycleTime(mHolder).getVisibility()) @@ -329,8 +330,7 @@ public class DataUsageSummaryPreferenceTest { mSummaryPreference.setUsageInfo(mCycleEnd, mUpdateTime, FAKE_CARRIER, 1 /* numPlans */); mSummaryPreference.setUsageNumbers( BillingCycleSettings.MIB_IN_BYTES, - 10 * BillingCycleSettings.MIB_IN_BYTES, - true /* hasMobileData */); + 10 * BillingCycleSettings.MIB_IN_BYTES); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getDataUsed(mHolder).getText().toString()) @@ -349,8 +349,7 @@ public class DataUsageSummaryPreferenceTest { mSummaryPreference.setUsageInfo(mCycleEnd, mUpdateTime, FAKE_CARRIER, 1 /* numPlans */); mSummaryPreference.setUsageNumbers( 11 * BillingCycleSettings.MIB_IN_BYTES, - 10 * BillingCycleSettings.MIB_IN_BYTES, - true /* hasMobileData */); + 10 * BillingCycleSettings.MIB_IN_BYTES); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getDataUsed(mHolder).getText().toString()) @@ -364,9 +363,9 @@ public class DataUsageSummaryPreferenceTest { @Test public void testSetUsageInfo_withUsageInfo_dataUsageShown() { - mSummaryPreference.setUsageInfo(mCycleEnd, mUpdateTime, FAKE_CARRIER, 0 /* numPlans */); + mSummaryPreference.setUsageInfo(mCycleEnd, -1, FAKE_CARRIER, 0 /* numPlans */); mSummaryPreference.setUsageNumbers( - BillingCycleSettings.MIB_IN_BYTES, -1L, true /* hasMobileData */); + BillingCycleSettings.MIB_IN_BYTES, -1L); mSummaryPreference.onBindViewHolder(mHolder); assertThat(mSummaryPreference.getDataUsed(mHolder).getText().toString()) @@ -383,8 +382,7 @@ public class DataUsageSummaryPreferenceTest { mSummaryPreference.setUsageInfo(mCycleEnd, mUpdateTime, FAKE_CARRIER, 1 /* numPlans */); mSummaryPreference.setUsageNumbers( BillingCycleSettings.MIB_IN_BYTES, - 10 * BillingCycleSettings.MIB_IN_BYTES, - true /* hasMobileData */); + 10 * BillingCycleSettings.MIB_IN_BYTES); int data_used_formatted_id = ResourcesUtils.getResourcesId( mContext, "string", "data_used_formatted");