From 741979bc0221e256deea46dec9232f2cfad56ca0 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Sun, 8 Oct 2023 19:46:32 +0800 Subject: [PATCH] Create AppDataUsageCycleController To improve performance and better organization and testings. Fix: 240931350 Test: manual - on AppDataUsage Test: unit test Change-Id: I277133b55378a3445aceb826d771b14c0fc91e4a --- res/xml/app_data_usage.xml | 3 +- .../settings/datausage/AppDataUsage.java | 144 ++++------------ .../datausage/AppDataUsageCycleController.kt | 114 +++++++++++++ .../datausage/AppDataUsageListController.kt | 5 +- .../settings/datausage/SpinnerPreference.java | 1 - .../lib/AppDataUsageDetailsRepository.kt | 97 +++++++++++ .../datausage/lib/AppDataUsageRepository.kt | 6 + .../lib/NetworkCycleDataRepository.kt | 3 +- .../datausage/lib/NetworkUsageDetailsData.kt | 38 +++++ .../settings/datausage/AppDataUsageTest.java | 156 ++---------------- .../AppDataUsageCycleControllerTest.kt | 113 +++++++++++++ .../AppDataUsageListControllerTest.kt | 10 +- .../ChartDataUsagePreferenceControllerTest.kt | 3 +- .../DataUsageListHeaderControllerTest.kt | 4 +- .../lib/AppDataUsageDetailsRepositoryTest.kt | 109 ++++++++++++ 15 files changed, 536 insertions(+), 270 deletions(-) create mode 100644 src/com/android/settings/datausage/AppDataUsageCycleController.kt create mode 100644 src/com/android/settings/datausage/lib/AppDataUsageDetailsRepository.kt create mode 100644 src/com/android/settings/datausage/lib/NetworkUsageDetailsData.kt create mode 100644 tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt diff --git a/res/xml/app_data_usage.xml b/res/xml/app_data_usage.xml index 7d4eaab369d..034015cbb72 100644 --- a/res/xml/app_data_usage.xml +++ b/res/xml/app_data_usage.xml @@ -21,7 +21,8 @@ android:title="@string/data_usage_app_summary_title"> + android:key="cycle" + settings:controller="com.android.settings.datausage.AppDataUsageCycleController" /> diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java index 67d33a72bf1..f74a5eacc99 100644 --- a/src/com/android/settings/datausage/AppDataUsage.java +++ b/src/com/android/settings/datausage/AppDataUsage.java @@ -17,6 +17,7 @@ package com.android.settings.datausage; import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; import static com.android.settings.datausage.lib.AppDataUsageRepository.getAppUid; +import static com.android.settings.datausage.lib.AppDataUsageRepository.getAppUidList; import android.app.Activity; import android.app.settings.SettingsEnums; @@ -32,15 +33,9 @@ import android.telephony.SubscriptionManager; import android.util.ArraySet; import android.util.IconDrawableFactory; import android.util.Log; -import android.util.Range; -import android.view.View; -import android.widget.AdapterView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.recyclerview.widget.DefaultItemAnimator; @@ -48,17 +43,19 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; import com.android.settings.applications.AppInfoBase; +import com.android.settings.datausage.lib.AppDataUsageDetailsRepository; +import com.android.settings.datausage.lib.NetworkUsageDetailsData; import com.android.settings.network.SubscriptionUtil; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.AppItem; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; -import com.android.settingslib.net.NetworkCycleDataForUid; -import com.android.settingslib.net.NetworkCycleDataForUidLoader; import com.android.settingslib.net.UidDetail; import com.android.settingslib.net.UidDetailProvider; +import kotlin.Unit; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -77,11 +74,8 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC private static final String KEY_FOREGROUND_USAGE = "foreground_usage"; private static final String KEY_BACKGROUND_USAGE = "background_usage"; private static final String KEY_RESTRICT_BACKGROUND = "restrict_background"; - private static final String KEY_CYCLE = "cycle"; private static final String KEY_UNRESTRICTED_DATA = "unrestricted_data_saver"; - private static final int LOADER_APP_USAGE_DATA = 2; - private PackageManager mPackageManager; private final ArraySet mPackages = new ArraySet<>(); private Preference mTotalUsage; @@ -94,14 +88,10 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC CharSequence mLabel; @VisibleForTesting String mPackageName; - private CycleAdapter mCycleAdapter; - @Nullable - private List mUsageData; @VisibleForTesting NetworkTemplate mTemplate; private AppItem mAppItem; - private SpinnerPreference mCycle; private RestrictedSwitchPreference mUnrestrictedData; private DataSaverBackend mDataSaverBackend; private Context mContext; @@ -160,7 +150,8 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC mForegroundUsage = findPreference(KEY_FOREGROUND_USAGE); mBackgroundUsage = findPreference(KEY_BACKGROUND_USAGE); - initCycle(); + final List uidList = getAppUidList(mAppItem.uids); + initCycle(uidList); final UidDetailProvider uidDetailProvider = getUidDetailProvider(); @@ -191,7 +182,7 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC } mDataSaverBackend = new DataSaverBackend(mContext); - use(AppDataUsageListController.class).init(mAppItem.uids); + use(AppDataUsageListController.class).init(uidList); } else { final Context context = getActivity(); final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true); @@ -207,11 +198,9 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC } @Override - public void onResume() { - super.onResume(); - // No animations will occur before: - // - LOADER_APP_USAGE_DATA initially updates the cycle - // - updatePrefs() initially updates the preference visibility + public void onStart() { + super.onStart(); + // No animations will occur before bindData() initially updates the cycle. // This is mainly for the cycle spinner, because when the page is entered from the // AppInfoDashboardFragment, there is no way to know whether the cycle data is available // before finished the async loading. @@ -219,11 +208,14 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC // setBackPreferenceListAnimatorIfLoaded(). mIsLoading = true; getListView().setItemAnimator(null); + } + + @Override + public void onResume() { + super.onResume(); if (mDataSaverBackend != null) { mDataSaverBackend.addListener(this); } - LoaderManager.getInstance(this).restartLoader(LOADER_APP_USAGE_DATA, null /* args */, - mUidDataCallbacks); updatePrefs(); } @@ -268,14 +260,16 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC return new UidDetailProvider(mContext); } - private void initCycle() { - mCycle = findPreference(KEY_CYCLE); - mCycleAdapter = new CycleAdapter(mContext, mCycle); + @VisibleForTesting + void initCycle(List uidList) { + var controller = use(AppDataUsageCycleController.class); + var repository = new AppDataUsageDetailsRepository(mContext, mTemplate, mCycles, uidList); + controller.init(repository, data -> { + bindData(data); + return Unit.INSTANCE; + }); if (mCycles != null) { - // If coming from a page like DataUsageList where already has a selected cycle, display - // that before loading to reduce flicker. - mCycleAdapter.setInitialCycleList(mCycles, mSelectedCycle); - mCycle.setHasCycles(true); + controller.setInitialCycles(mCycles, mSelectedCycle); } } @@ -326,22 +320,13 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC } @VisibleForTesting - void bindData(int position) { - final long backgroundBytes, foregroundBytes; - if (mUsageData == null || position >= mUsageData.size()) { - backgroundBytes = foregroundBytes = 0; - mCycle.setHasCycles(false); - } else { - mCycle.setHasCycles(true); - final NetworkCycleDataForUid data = mUsageData.get(position); - backgroundBytes = data.getBackgroudUsage(); - foregroundBytes = data.getForegroudUsage(); - } - final long totalBytes = backgroundBytes + foregroundBytes; - - mTotalUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, totalBytes)); - mForegroundUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, foregroundBytes)); - mBackgroundUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, backgroundBytes)); + void bindData(@NonNull NetworkUsageDetailsData data) { + mIsLoading = false; + mTotalUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, data.getTotalUsage())); + mForegroundUsage.setSummary( + DataUsageUtils.formatDataUsage(mContext, data.getForegroundUsage())); + mBackgroundUsage.setSummary( + DataUsageUtils.formatDataUsage(mContext, data.getBackgroundUsage())); } private boolean getAppRestrictBackground() { @@ -391,71 +376,6 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC return SettingsEnums.APP_DATA_USAGE; } - private final AdapterView.OnItemSelectedListener mCycleListener = - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - bindData(position); - } - - @Override - public void onNothingSelected(AdapterView parent) { - // ignored - } - }; - - @VisibleForTesting - final LoaderManager.LoaderCallbacks> mUidDataCallbacks = - new LoaderManager.LoaderCallbacks<>() { - @Override - @NonNull - public Loader> onCreateLoader(int id, Bundle args) { - final NetworkCycleDataForUidLoader.Builder builder = - NetworkCycleDataForUidLoader.builder(mContext); - builder.setRetrieveDetail(true) - .setNetworkTemplate(mTemplate); - for (int i = 0; i < mAppItem.uids.size(); i++) { - builder.addUid(mAppItem.uids.keyAt(i)); - } - if (mCycles != null) { - builder.setCycles(mCycles); - } - return builder.build(); - } - - @Override - public void onLoadFinished(@NonNull Loader> loader, - List data) { - mUsageData = data; - mCycle.setOnItemSelectedListener(mCycleListener); - mCycleAdapter.updateCycleList(data.stream() - .map(cycle -> new Range<>(cycle.getStartTime(), cycle.getEndTime())) - .toList()); - if (mSelectedCycle > 0L) { - final int numCycles = data.size(); - int position = 0; - for (int i = 0; i < numCycles; i++) { - final NetworkCycleDataForUid cycleData = data.get(i); - if (cycleData.getEndTime() == mSelectedCycle) { - position = i; - break; - } - } - if (position > 0) { - mCycle.setSelection(position); - } - bindData(position); - } else { - bindData(0 /* position */); - } - mIsLoading = false; - } - - @Override - public void onLoaderReset(@NonNull Loader> loader) { - } - }; - @Override public void onDataSaverChanged(boolean isDataSaving) { diff --git a/src/com/android/settings/datausage/AppDataUsageCycleController.kt b/src/com/android/settings/datausage/AppDataUsageCycleController.kt new file mode 100644 index 00000000000..b1a0e76757a --- /dev/null +++ b/src/com/android/settings/datausage/AppDataUsageCycleController.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datausage + +import android.content.Context +import android.view.View +import android.widget.AdapterView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.preference.PreferenceScreen +import com.android.settings.core.BasePreferenceController +import com.android.settings.datausage.lib.AppDataUsageDetailsRepository +import com.android.settings.datausage.lib.IAppDataUsageDetailsRepository +import com.android.settings.datausage.lib.NetworkUsageDetailsData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class AppDataUsageCycleController(context: Context, preferenceKey: String) : + BasePreferenceController(context, preferenceKey) { + + private lateinit var repository: IAppDataUsageDetailsRepository + private var onUsageDataUpdated: (NetworkUsageDetailsData) -> Unit = {} + private lateinit var preference: SpinnerPreference + private var cycleAdapter: CycleAdapter? = null + + private var initialCycles: List = emptyList() + private var initialSelectedEndTime: Long = -1 + + private var usageDetailsDataList: List = emptyList() + + fun init( + repository: IAppDataUsageDetailsRepository, + onUsageDataUpdated: (NetworkUsageDetailsData) -> Unit, + ) { + this.repository = repository + this.onUsageDataUpdated = onUsageDataUpdated + } + + /** + * Sets the initial cycles. + * + * If coming from a page like DataUsageList where already has a selected cycle, display that + * before loading to reduce flicker. + */ + fun setInitialCycles(initialCycles: List, initialSelectedEndTime: Long) { + this.initialCycles = initialCycles + this.initialSelectedEndTime = initialSelectedEndTime + } + + override fun getAvailabilityStatus() = AVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey)!! + if (cycleAdapter == null) { + cycleAdapter = CycleAdapter(mContext, preference).apply { + if (initialCycles.isNotEmpty()) { + setInitialCycleList(initialCycles, initialSelectedEndTime) + preference.setHasCycles(true) + } + } + } + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + update() + } + } + } + + private suspend fun update() { + usageDetailsDataList = withContext(Dispatchers.Default) { + repository.queryDetailsForCycles() + } + if (usageDetailsDataList.isEmpty()) { + preference.setHasCycles(false) + onUsageDataUpdated(NetworkUsageDetailsData.AllZero) + return + } + + preference.setHasCycles(true) + cycleAdapter?.updateCycleList(usageDetailsDataList.map { it.range }) + preference.setOnItemSelectedListener(cycleListener) + } + + private val cycleListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + usageDetailsDataList.getOrNull(position)?.let(onUsageDataUpdated) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // ignored + } + } +} diff --git a/src/com/android/settings/datausage/AppDataUsageListController.kt b/src/com/android/settings/datausage/AppDataUsageListController.kt index e39ed7e9166..85a6e920f53 100644 --- a/src/com/android/settings/datausage/AppDataUsageListController.kt +++ b/src/com/android/settings/datausage/AppDataUsageListController.kt @@ -28,6 +28,7 @@ import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen import com.android.settings.core.BasePreferenceController import com.android.settings.datausage.lib.AppDataUsageRepository.Companion.getAppUid +import com.android.settings.datausage.lib.AppDataUsageRepository.Companion.getAppUidList import com.android.settings.datausage.lib.AppPreferenceRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -43,8 +44,8 @@ open class AppDataUsageListController @JvmOverloads constructor( private var uids: List = emptyList() private lateinit var preference: PreferenceGroup - fun init(uids: SparseBooleanArray) { - this.uids = uids.keyIterator().asSequence().map { getAppUid(it) }.distinct().toList() + fun init(uids: List) { + this.uids = uids } override fun getAvailabilityStatus() = AVAILABLE diff --git a/src/com/android/settings/datausage/SpinnerPreference.java b/src/com/android/settings/datausage/SpinnerPreference.java index a705079b76b..c81ac550d97 100644 --- a/src/com/android/settings/datausage/SpinnerPreference.java +++ b/src/com/android/settings/datausage/SpinnerPreference.java @@ -94,7 +94,6 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne @Override public void onItemSelected( AdapterView parent, View view, int position, long id) { - if (mPosition == position) return; mPosition = position; mCurrentObject = mAdapter.getItem(position); if (mListener != null) { diff --git a/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepository.kt b/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepository.kt new file mode 100644 index 00000000000..7dc49bbfdb0 --- /dev/null +++ b/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepository.kt @@ -0,0 +1,97 @@ +/* + * 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.NetworkTemplate +import android.util.Log +import android.util.Range +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +interface IAppDataUsageDetailsRepository { + suspend fun queryDetailsForCycles(): List +} + +class AppDataUsageDetailsRepository @JvmOverloads constructor( + context: Context, + private val template: NetworkTemplate, + private val cycles: List?, + private val uids: List, + private val networkCycleDataRepository: INetworkCycleDataRepository = + NetworkCycleDataRepository(context, template) +) : IAppDataUsageDetailsRepository { + private val networkStatsManager = context.getSystemService(NetworkStatsManager::class.java)!! + + override suspend fun queryDetailsForCycles(): List = coroutineScope { + getCycles().map { + async { + queryDetails(it) + } + }.awaitAll().filter { it.totalUsage > 0 } + } + + private fun getCycles(): List> = + cycles?.zipWithNext { endTime, startTime -> Range(startTime, endTime) } + ?: networkCycleDataRepository.getCycles() + + private fun queryDetails(range: Range): NetworkUsageDetailsData { + var totalUsage = 0L + var foregroundUsage = 0L + for (uid in uids) { + val usage = getUsage(range, uid, NetworkStats.Bucket.STATE_ALL) + if (usage > 0L) { + totalUsage += usage + foregroundUsage += + getUsage(range, uid, NetworkStats.Bucket.STATE_FOREGROUND) + } + } + return NetworkUsageDetailsData( + range = range, + totalUsage = totalUsage, + foregroundUsage = foregroundUsage, + backgroundUsage = totalUsage - foregroundUsage, + ) + } + + @VisibleForTesting + fun getUsage(range: Range, uid: Int, state: Int): Long = try { + networkStatsManager.queryDetailsForUidTagState( + template, range.lower, range.upper, uid, NetworkStats.Bucket.TAG_NONE, state, + ).getTotalUsage() + } catch (e: Exception) { + Log.e(TAG, "Exception querying network detail.", e) + 0 + } + + private fun NetworkStats.getTotalUsage(): Long = use { + var bytes = 0L + val bucket = NetworkStats.Bucket() + while (getNextBucket(bucket)) { + bytes += bucket.rxBytes + bucket.txBytes + } + return bytes + } + + private companion object { + private const val TAG = "AppDataUsageDetailsRepo" + } +} diff --git a/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt b/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt index ccd3e60e13f..bde25ab676a 100644 --- a/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt +++ b/src/com/android/settings/datausage/lib/AppDataUsageRepository.kt @@ -25,7 +25,9 @@ import android.os.Process import android.os.UserHandle import android.util.Log import android.util.SparseArray +import android.util.SparseBooleanArray import androidx.annotation.VisibleForTesting +import androidx.core.util.keyIterator import com.android.settings.R import com.android.settingslib.AppItem import com.android.settingslib.net.UidDetailProvider @@ -195,6 +197,10 @@ class AppDataUsageRepository( val bytes: Long, ) + @JvmStatic + fun getAppUidList(uids: SparseBooleanArray) = + uids.keyIterator().asSequence().map { getAppUid(it) }.distinct().toList() + @JvmStatic fun getAppUid(uid: Int): Int { if (Process.isSdkSandboxUid(uid)) { diff --git a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt index f10d506ec47..cfd10536c08 100644 --- a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt +++ b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.coroutineScope interface INetworkCycleDataRepository { suspend fun loadCycles(): List + fun getCycles(): List> fun getPolicy(): NetworkPolicy? suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData? } @@ -48,7 +49,7 @@ class NetworkCycleDataRepository( override suspend fun loadCycles(): List = getCycles().queryUsage().filter { it.usage > 0 } - private fun getCycles(): List> { + override fun getCycles(): List> { val policy = getPolicy() ?: return queryCyclesAsFourWeeks() return policy.cycleIterator().asSequence().map { Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli()) diff --git a/src/com/android/settings/datausage/lib/NetworkUsageDetailsData.kt b/src/com/android/settings/datausage/lib/NetworkUsageDetailsData.kt new file mode 100644 index 00000000000..19ff81c4a90 --- /dev/null +++ b/src/com/android/settings/datausage/lib/NetworkUsageDetailsData.kt @@ -0,0 +1,38 @@ +/* + * 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.util.Range + +/** + * Details data structure representing usage data in a period. + */ +data class NetworkUsageDetailsData( + val range: Range, + val totalUsage: Long, + val foregroundUsage: Long, + val backgroundUsage: Long, +) { + companion object { + val AllZero = NetworkUsageDetailsData( + range = Range(0, 0), + totalUsage = 0, + foregroundUsage = 0, + backgroundUsage = 0, + ) + } +} diff --git a/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java b/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java index e4b91c6a795..2c8dbfd9ced 100644 --- a/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java +++ b/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java @@ -41,8 +41,8 @@ import android.net.NetworkTemplate; import android.os.Bundle; import android.os.Process; import android.telephony.SubscriptionManager; -import android.text.format.DateUtils; import android.util.ArraySet; +import android.util.Range; import androidx.fragment.app.FragmentActivity; import androidx.preference.Preference; @@ -51,6 +51,7 @@ import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.settings.applications.AppInfoBase; +import com.android.settings.datausage.lib.NetworkUsageDetailsData; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowDataUsageUtils; import com.android.settings.testutils.shadow.ShadowEntityHeaderController; @@ -61,8 +62,6 @@ import com.android.settingslib.AppItem; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.net.NetworkCycleDataForUid; -import com.android.settingslib.net.NetworkCycleDataForUidLoader; import com.android.settingslib.net.UidDetail; import com.android.settingslib.net.UidDetailProvider; @@ -80,7 +79,6 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowSubscriptionManager; import org.robolectric.util.ReflectionHelpers; -import java.util.ArrayList; import java.util.List; @RunWith(RobolectricTestRunner.class) @@ -254,163 +252,33 @@ public class AppDataUsageTest { } @Test - public void bindData_noAppUsageData_shouldHideCycleSpinner() { - mFragment = spy(new TestFragment()); - final SpinnerPreference cycle = mock(SpinnerPreference.class); - ReflectionHelpers.setField(mFragment, "mCycle", cycle); - final Preference preference = mock(Preference.class); - ReflectionHelpers.setField(mFragment, "mBackgroundUsage", preference); - ReflectionHelpers.setField(mFragment, "mForegroundUsage", preference); - ReflectionHelpers.setField(mFragment, "mTotalUsage", preference); - ReflectionHelpers.setField(mFragment, "mContext", RuntimeEnvironment.application); - - mFragment.bindData(0 /* position */); - - verify(cycle).setHasCycles(false); - } - - @Test - public void bindData_hasAppUsageData_shouldShowCycleSpinnerAndUpdateUsageSummary() { + public void bindData_shouldUpdateUsageSummary() { mFragment = spy(new TestFragment()); final Context context = RuntimeEnvironment.application; ReflectionHelpers.setField(mFragment, "mContext", context); final long backgroundBytes = 1234L; final long foregroundBytes = 5678L; - final List appUsage = new ArrayList<>(); - appUsage.add(new NetworkCycleDataForUid.Builder() - .setBackgroundUsage(backgroundBytes).setForegroundUsage(foregroundBytes).build()); - ReflectionHelpers.setField(mFragment, "mUsageData", appUsage); + final NetworkUsageDetailsData appUsage = new NetworkUsageDetailsData( + new Range<>(1L, 2L), + backgroundBytes + foregroundBytes, + foregroundBytes, + backgroundBytes + ); final Preference backgroundPref = mock(Preference.class); ReflectionHelpers.setField(mFragment, "mBackgroundUsage", backgroundPref); final Preference foregroundPref = mock(Preference.class); ReflectionHelpers.setField(mFragment, "mForegroundUsage", foregroundPref); final Preference totalPref = mock(Preference.class); ReflectionHelpers.setField(mFragment, "mTotalUsage", totalPref); - final SpinnerPreference cycle = mock(SpinnerPreference.class); - ReflectionHelpers.setField(mFragment, "mCycle", cycle); - mFragment.bindData(0 /* position */); + mFragment.bindData(appUsage); - verify(cycle).setHasCycles(true); verify(totalPref).setSummary( DataUsageUtils.formatDataUsage(context, backgroundBytes + foregroundBytes)); verify(backgroundPref).setSummary(DataUsageUtils.formatDataUsage(context, backgroundBytes)); verify(foregroundPref).setSummary(DataUsageUtils.formatDataUsage(context, foregroundBytes)); } - @Test - public void onCreateLoader_categoryApp_shouldQueryDataUsageUsingAppKey() { - mFragment = new TestFragment(); - final Context context = RuntimeEnvironment.application; - final int testUid = 123123; - final AppItem appItem = new AppItem(testUid); - appItem.addUid(testUid); - appItem.category = AppItem.CATEGORY_APP; - ReflectionHelpers.setField(mFragment, "mContext", context); - ReflectionHelpers.setField(mFragment, "mAppItem", appItem); - ReflectionHelpers.setField(mFragment, "mTemplate", - new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()); - final long end = System.currentTimeMillis(); - final long start = end - (DateUtils.WEEK_IN_MILLIS * 4); - - final NetworkCycleDataForUidLoader loader = (NetworkCycleDataForUidLoader) - mFragment.mUidDataCallbacks.onCreateLoader(0, Bundle.EMPTY); - - final List uids = loader.getUids(); - assertThat(uids).hasSize(1); - assertThat(uids.get(0)).isEqualTo(testUid); - } - - @Test - public void onCreateLoader_categoryUser_shouldQueryDataUsageUsingAssociatedUids() { - mFragment = new TestFragment(); - final Context context = RuntimeEnvironment.application; - final int testUserId = 11; - final AppItem appItem = new AppItem(testUserId); - appItem.category = AppItem.CATEGORY_USER; - appItem.addUid(123); - appItem.addUid(456); - appItem.addUid(789); - ReflectionHelpers.setField(mFragment, "mContext", context); - ReflectionHelpers.setField(mFragment, "mAppItem", appItem); - ReflectionHelpers.setField(mFragment, "mTemplate", - new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()); - final long end = System.currentTimeMillis(); - final long start = end - (DateUtils.WEEK_IN_MILLIS * 4); - - final NetworkCycleDataForUidLoader loader = (NetworkCycleDataForUidLoader) - mFragment.mUidDataCallbacks.onCreateLoader(0, Bundle.EMPTY); - - final List uids = loader.getUids(); - assertThat(uids).hasSize(3); - assertThat(uids.get(0)).isEqualTo(123); - assertThat(uids.get(1)).isEqualTo(456); - assertThat(uids.get(2)).isEqualTo(789); - } - - @Test - public void onCreateLoader_hasCyclesSpecified_shouldQueryDataUsageForSpecifiedCycles() { - final long startTime = 1521583200000L; - final long endTime = 1521676800000L; - ArrayList testCycles = new ArrayList<>(); - testCycles.add(endTime); - testCycles.add(startTime); - final int uid = 123; - final AppItem appItem = new AppItem(uid); - appItem.category = AppItem.CATEGORY_APP; - appItem.addUid(uid); - - mFragment = new TestFragment(); - ReflectionHelpers.setField(mFragment, "mContext", RuntimeEnvironment.application); - ReflectionHelpers.setField(mFragment, "mCycles", testCycles); - ReflectionHelpers.setField(mFragment, "mAppItem", appItem); - ReflectionHelpers.setField(mFragment, "mTemplate", - new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()); - - final NetworkCycleDataForUidLoader loader = (NetworkCycleDataForUidLoader) - mFragment.mUidDataCallbacks.onCreateLoader(0 /* id */, Bundle.EMPTY /* args */); - - final ArrayList cycles = loader.getCycles(); - assertThat(cycles).hasSize(2); - assertThat(cycles.get(0)).isEqualTo(endTime); - assertThat(cycles.get(1)).isEqualTo(startTime); - } - - @Test - public void onLoadFinished_hasSelectedCycleSpecified_shouldSelectSpecifiedCycle() { - final long now = System.currentTimeMillis(); - final long tenDaysAgo = now - (DateUtils.DAY_IN_MILLIS * 10); - final long twentyDaysAgo = now - (DateUtils.DAY_IN_MILLIS * 20); - final long thirtyDaysAgo = now - (DateUtils.DAY_IN_MILLIS * 30); - final List data = new ArrayList<>(); - NetworkCycleDataForUid.Builder builder = new NetworkCycleDataForUid.Builder(); - builder.setStartTime(thirtyDaysAgo).setEndTime(twentyDaysAgo).setTotalUsage(9876L); - data.add(builder.build()); - builder = new NetworkCycleDataForUid.Builder(); - builder.setStartTime(twentyDaysAgo).setEndTime(tenDaysAgo).setTotalUsage(5678L); - data.add(builder.build()); - builder = new NetworkCycleDataForUid.Builder(); - builder.setStartTime(tenDaysAgo).setEndTime(now).setTotalUsage(1234L); - data.add(builder.build()); - - mFragment = new TestFragment(); - ReflectionHelpers.setField(mFragment, "mContext", RuntimeEnvironment.application); - ReflectionHelpers.setField(mFragment, "mCycleAdapter", mock(CycleAdapter.class)); - ReflectionHelpers.setField(mFragment, "mSelectedCycle", tenDaysAgo); - final Preference backgroundPref = mock(Preference.class); - ReflectionHelpers.setField(mFragment, "mBackgroundUsage", backgroundPref); - final Preference foregroundPref = mock(Preference.class); - ReflectionHelpers.setField(mFragment, "mForegroundUsage", foregroundPref); - final Preference totalPref = mock(Preference.class); - ReflectionHelpers.setField(mFragment, "mTotalUsage", totalPref); - final SpinnerPreference cycle = mock(SpinnerPreference.class); - ReflectionHelpers.setField(mFragment, "mCycle", cycle); - - mFragment.mUidDataCallbacks.onLoadFinished(null /* loader */, data); - - verify(cycle).setSelection(1); - } - @Test @Config(shadows = {ShadowDataUsageUtils.class, ShadowSubscriptionManager.class, ShadowFragment.class}) @@ -447,6 +315,10 @@ public class AppDataUsageTest { return mock(clazz); } + @Override + void initCycle(List uidList) { + } + @Override public boolean isSimHardwareVisible(Context context) { return true; diff --git a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt new file mode 100644 index 00000000000..ea51f015237 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageCycleControllerTest.kt @@ -0,0 +1,113 @@ +/* + * 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.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.datausage.lib.AppDataUsageDetailsRepository +import com.android.settings.datausage.lib.IAppDataUsageDetailsRepository +import com.android.settings.datausage.lib.NetworkUsageDetailsData +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.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class AppDataUsageCycleControllerTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + private val controller = AppDataUsageCycleController(context, KEY) + + private val preference = spy(SpinnerPreference(context, null).apply { key = KEY }) + + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + private val onUsageDataUpdated: (NetworkUsageDetailsData) -> Unit = {} + + @Before + fun setUp() { + preferenceScreen.addPreference(preference) + } + + @Test + fun onViewCreated_noUsage_hidePreference(): Unit = runBlocking { + val repository = object : IAppDataUsageDetailsRepository { + override suspend fun queryDetailsForCycles() = emptyList() + } + controller.init(repository, onUsageDataUpdated) + controller.displayPreference(preferenceScreen) + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isVisible).isFalse() + } + + @Test + fun onViewCreated_hasUsage_showPreference(): Unit = runBlocking { + val detailsData = NetworkUsageDetailsData( + range = Range(1, 2), + totalUsage = 11, + foregroundUsage = 1, + backgroundUsage = 10, + ) + val repository = object : IAppDataUsageDetailsRepository { + override suspend fun queryDetailsForCycles() = listOf(detailsData) + } + controller.init(repository, onUsageDataUpdated) + controller.displayPreference(preferenceScreen) + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isVisible).isTrue() + } + + @Test + fun setInitialCycles() { + val repository = object : IAppDataUsageDetailsRepository { + override suspend fun queryDetailsForCycles() = emptyList() + } + controller.init(repository, onUsageDataUpdated) + controller.setInitialCycles( + initialCycles = listOf(CYCLE2_END_TIME, CYCLE1_END_TIME, CYCLE1_START_TIME), + initialSelectedEndTime = CYCLE1_END_TIME, + ) + + controller.displayPreference(preferenceScreen) + + verify(preference).setSelection(1) + } + + private companion object { + const val KEY = "test_key" + const val CYCLE1_START_TIME = 1694444444000L + const val CYCLE1_END_TIME = 1695555555000L + const val CYCLE2_END_TIME = 1695566666000L + } +} diff --git a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageListControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageListControllerTest.kt index 67272329192..4575a8d66aa 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageListControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageListControllerTest.kt @@ -17,7 +17,6 @@ package com.android.settings.datausage import android.content.Context -import android.util.SparseBooleanArray import androidx.lifecycle.testing.TestLifecycleOwner import androidx.preference.Preference import androidx.preference.PreferenceCategory @@ -63,9 +62,7 @@ class AppDataUsageListControllerTest { @Test fun onViewCreated_singleUid_hidePreference(): Unit = runBlocking { - controller.init(SparseBooleanArray().apply { - put(UID_0, true) - }) + controller.init(listOf(UID_0)) controller.displayPreference(preferenceScreen) controller.onViewCreated(TestLifecycleOwner()) @@ -76,10 +73,7 @@ class AppDataUsageListControllerTest { @Test fun onViewCreated_twoUid_showPreference(): Unit = runBlocking { - controller.init(SparseBooleanArray().apply { - put(UID_0, true) - put(UID_1, true) - }) + controller.init(listOf(UID_0, UID_1)) controller.displayPreference(preferenceScreen) controller.onViewCreated(TestLifecycleOwner()) diff --git a/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt index 1748f07de6b..e0eb789867c 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt @@ -17,6 +17,7 @@ package com.android.settings.datausage import android.content.Context +import android.util.Range import androidx.lifecycle.testing.TestLifecycleOwner import androidx.preference.PreferenceScreen import androidx.test.core.app.ApplicationProvider @@ -39,7 +40,7 @@ class ChartDataUsagePreferenceControllerTest { private val repository = object : INetworkCycleDataRepository { override suspend fun loadCycles() = emptyList() - + override fun getCycles() = emptyList>() override fun getPolicy() = null override suspend fun querySummary(startTime: Long, endTime: Long) = when { 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 35b70d6cc75..581f7ba3d66 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt @@ -18,6 +18,7 @@ package com.android.settings.datausage import android.content.Context import android.net.NetworkTemplate +import android.util.Range import android.view.LayoutInflater import android.view.View import android.widget.Spinner @@ -48,9 +49,8 @@ class DataUsageListHeaderControllerTest { private val repository = object : INetworkCycleDataRepository { override suspend fun loadCycles() = emptyList() - + override fun getCycles() = emptyList>() override fun getPolicy() = null - override suspend fun querySummary(startTime: Long, endTime: Long) = null } diff --git a/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt new file mode 100644 index 00000000000..7072b462057 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt @@ -0,0 +1,109 @@ +/* + * 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.content.Context +import android.net.NetworkTemplate +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 kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class AppDataUsageDetailsRepositoryTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + private val template = mock() + + private val networkCycleDataRepository = mock { + on { getCycles() } doReturn listOf(Range(CYCLE1_END_TIME, CYCLE2_END_TIME)) + } + + @Test + fun queryDetailsForCycles_hasCycles(): Unit = runBlocking { + val range = Range(CYCLE1_START_TIME, CYCLE1_END_TIME) + val repository = spy( + AppDataUsageDetailsRepository( + context = context, + cycles = listOf(CYCLE1_END_TIME, CYCLE1_START_TIME), + template = template, + uids = listOf(UID), + networkCycleDataRepository = networkCycleDataRepository, + ) + ) { + doReturn(ALL_USAGE).whenever(mock).getUsage(range, UID, Bucket.STATE_ALL) + doReturn(FOREGROUND_USAGE).whenever(mock).getUsage(range, UID, Bucket.STATE_FOREGROUND) + } + + val detailsForCycles = repository.queryDetailsForCycles() + + assertThat(detailsForCycles).containsExactly( + NetworkUsageDetailsData( + range = range, + totalUsage = ALL_USAGE, + foregroundUsage = FOREGROUND_USAGE, + backgroundUsage = ALL_USAGE - FOREGROUND_USAGE, + ) + ) + } + + @Test + fun queryDetailsForCycles_defaultCycles(): Unit = runBlocking { + val range = Range(CYCLE1_END_TIME, CYCLE2_END_TIME) + val repository = spy( + AppDataUsageDetailsRepository( + context = context, + cycles = null, + template = template, + uids = listOf(UID), + networkCycleDataRepository = networkCycleDataRepository, + ) + ) { + doReturn(ALL_USAGE).whenever(mock).getUsage(range, UID, Bucket.STATE_ALL) + doReturn(FOREGROUND_USAGE).whenever(mock).getUsage(range, UID, Bucket.STATE_FOREGROUND) + } + + val detailsForCycles = repository.queryDetailsForCycles() + + assertThat(detailsForCycles).containsExactly( + NetworkUsageDetailsData( + range = range, + totalUsage = ALL_USAGE, + foregroundUsage = FOREGROUND_USAGE, + backgroundUsage = ALL_USAGE - FOREGROUND_USAGE, + ) + ) + } + + private companion object { + const val CYCLE1_START_TIME = 1694444444000L + const val CYCLE1_END_TIME = 1695555555000L + const val CYCLE2_END_TIME = 1695566666000L + const val UID = 10000 + + const val ALL_USAGE = 10L + const val FOREGROUND_USAGE = 2L + } +}