Create AppDataUsageCycleController

To improve performance and better organization and testings.

Fix: 240931350
Test: manual - on AppDataUsage
Test: unit test
Change-Id: I277133b55378a3445aceb826d771b14c0fc91e4a
This commit is contained in:
Chaohui Wang
2023-10-08 19:46:32 +08:00
parent 0bcf5b79f8
commit 741979bc02
15 changed files with 536 additions and 270 deletions

View File

@@ -21,7 +21,8 @@
android:title="@string/data_usage_app_summary_title"> android:title="@string/data_usage_app_summary_title">
<com.android.settings.datausage.SpinnerPreference <com.android.settings.datausage.SpinnerPreference
android:key="cycle" /> android:key="cycle"
settings:controller="com.android.settings.datausage.AppDataUsageCycleController" />
<PreferenceCategory <PreferenceCategory
android:key="app_data_usage_summary_category"> android:key="app_data_usage_summary_category">

View File

@@ -17,6 +17,7 @@ package com.android.settings.datausage;
import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; 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.getAppUid;
import static com.android.settings.datausage.lib.AppDataUsageRepository.getAppUidList;
import android.app.Activity; import android.app.Activity;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
@@ -32,15 +33,9 @@ import android.telephony.SubscriptionManager;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.IconDrawableFactory; import android.util.IconDrawableFactory;
import android.util.Log; import android.util.Log;
import android.util.Range;
import android.view.View;
import android.widget.AdapterView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
@@ -48,17 +43,19 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.applications.AppInfoBase; 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.network.SubscriptionUtil;
import com.android.settings.widget.EntityHeaderController; import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.AppItem; import com.android.settingslib.AppItem;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedSwitchPreference; 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.UidDetail;
import com.android.settingslib.net.UidDetailProvider; import com.android.settingslib.net.UidDetailProvider;
import kotlin.Unit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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_FOREGROUND_USAGE = "foreground_usage";
private static final String KEY_BACKGROUND_USAGE = "background_usage"; private static final String KEY_BACKGROUND_USAGE = "background_usage";
private static final String KEY_RESTRICT_BACKGROUND = "restrict_background"; 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 String KEY_UNRESTRICTED_DATA = "unrestricted_data_saver";
private static final int LOADER_APP_USAGE_DATA = 2;
private PackageManager mPackageManager; private PackageManager mPackageManager;
private final ArraySet<String> mPackages = new ArraySet<>(); private final ArraySet<String> mPackages = new ArraySet<>();
private Preference mTotalUsage; private Preference mTotalUsage;
@@ -94,14 +88,10 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
CharSequence mLabel; CharSequence mLabel;
@VisibleForTesting @VisibleForTesting
String mPackageName; String mPackageName;
private CycleAdapter mCycleAdapter;
@Nullable
private List<NetworkCycleDataForUid> mUsageData;
@VisibleForTesting @VisibleForTesting
NetworkTemplate mTemplate; NetworkTemplate mTemplate;
private AppItem mAppItem; private AppItem mAppItem;
private SpinnerPreference mCycle;
private RestrictedSwitchPreference mUnrestrictedData; private RestrictedSwitchPreference mUnrestrictedData;
private DataSaverBackend mDataSaverBackend; private DataSaverBackend mDataSaverBackend;
private Context mContext; private Context mContext;
@@ -160,7 +150,8 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
mForegroundUsage = findPreference(KEY_FOREGROUND_USAGE); mForegroundUsage = findPreference(KEY_FOREGROUND_USAGE);
mBackgroundUsage = findPreference(KEY_BACKGROUND_USAGE); mBackgroundUsage = findPreference(KEY_BACKGROUND_USAGE);
initCycle(); final List<Integer> uidList = getAppUidList(mAppItem.uids);
initCycle(uidList);
final UidDetailProvider uidDetailProvider = getUidDetailProvider(); final UidDetailProvider uidDetailProvider = getUidDetailProvider();
@@ -191,7 +182,7 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
} }
mDataSaverBackend = new DataSaverBackend(mContext); mDataSaverBackend = new DataSaverBackend(mContext);
use(AppDataUsageListController.class).init(mAppItem.uids); use(AppDataUsageListController.class).init(uidList);
} else { } else {
final Context context = getActivity(); final Context context = getActivity();
final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true); final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true);
@@ -207,11 +198,9 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
} }
@Override @Override
public void onResume() { public void onStart() {
super.onResume(); super.onStart();
// No animations will occur before: // No animations will occur before bindData() initially updates the cycle.
// - LOADER_APP_USAGE_DATA initially updates the cycle
// - updatePrefs() initially updates the preference visibility
// This is mainly for the cycle spinner, because when the page is entered from the // 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 // AppInfoDashboardFragment, there is no way to know whether the cycle data is available
// before finished the async loading. // before finished the async loading.
@@ -219,11 +208,14 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
// setBackPreferenceListAnimatorIfLoaded(). // setBackPreferenceListAnimatorIfLoaded().
mIsLoading = true; mIsLoading = true;
getListView().setItemAnimator(null); getListView().setItemAnimator(null);
}
@Override
public void onResume() {
super.onResume();
if (mDataSaverBackend != null) { if (mDataSaverBackend != null) {
mDataSaverBackend.addListener(this); mDataSaverBackend.addListener(this);
} }
LoaderManager.getInstance(this).restartLoader(LOADER_APP_USAGE_DATA, null /* args */,
mUidDataCallbacks);
updatePrefs(); updatePrefs();
} }
@@ -268,14 +260,16 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
return new UidDetailProvider(mContext); return new UidDetailProvider(mContext);
} }
private void initCycle() { @VisibleForTesting
mCycle = findPreference(KEY_CYCLE); void initCycle(List<Integer> uidList) {
mCycleAdapter = new CycleAdapter(mContext, mCycle); 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 (mCycles != null) {
// If coming from a page like DataUsageList where already has a selected cycle, display controller.setInitialCycles(mCycles, mSelectedCycle);
// that before loading to reduce flicker.
mCycleAdapter.setInitialCycleList(mCycles, mSelectedCycle);
mCycle.setHasCycles(true);
} }
} }
@@ -326,22 +320,13 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
} }
@VisibleForTesting @VisibleForTesting
void bindData(int position) { void bindData(@NonNull NetworkUsageDetailsData data) {
final long backgroundBytes, foregroundBytes; mIsLoading = false;
if (mUsageData == null || position >= mUsageData.size()) { mTotalUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, data.getTotalUsage()));
backgroundBytes = foregroundBytes = 0; mForegroundUsage.setSummary(
mCycle.setHasCycles(false); DataUsageUtils.formatDataUsage(mContext, data.getForegroundUsage()));
} else { mBackgroundUsage.setSummary(
mCycle.setHasCycles(true); DataUsageUtils.formatDataUsage(mContext, data.getBackgroundUsage()));
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));
} }
private boolean getAppRestrictBackground() { private boolean getAppRestrictBackground() {
@@ -391,71 +376,6 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
return SettingsEnums.APP_DATA_USAGE; 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<List<NetworkCycleDataForUid>> mUidDataCallbacks =
new LoaderManager.LoaderCallbacks<>() {
@Override
@NonNull
public Loader<List<NetworkCycleDataForUid>> 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<List<NetworkCycleDataForUid>> loader,
List<NetworkCycleDataForUid> 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<List<NetworkCycleDataForUid>> loader) {
}
};
@Override @Override
public void onDataSaverChanged(boolean isDataSaving) { public void onDataSaverChanged(boolean isDataSaving) {

View File

@@ -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<Long> = emptyList()
private var initialSelectedEndTime: Long = -1
private var usageDetailsDataList: List<NetworkUsageDetailsData> = 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<Long>, 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
}
}
}

View File

@@ -28,6 +28,7 @@ import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.android.settings.core.BasePreferenceController import com.android.settings.core.BasePreferenceController
import com.android.settings.datausage.lib.AppDataUsageRepository.Companion.getAppUid 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 com.android.settings.datausage.lib.AppPreferenceRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -43,8 +44,8 @@ open class AppDataUsageListController @JvmOverloads constructor(
private var uids: List<Int> = emptyList() private var uids: List<Int> = emptyList()
private lateinit var preference: PreferenceGroup private lateinit var preference: PreferenceGroup
fun init(uids: SparseBooleanArray) { fun init(uids: List<Int>) {
this.uids = uids.keyIterator().asSequence().map { getAppUid(it) }.distinct().toList() this.uids = uids
} }
override fun getAvailabilityStatus() = AVAILABLE override fun getAvailabilityStatus() = AVAILABLE

View File

@@ -94,7 +94,6 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne
@Override @Override
public void onItemSelected( public void onItemSelected(
AdapterView<?> parent, View view, int position, long id) { AdapterView<?> parent, View view, int position, long id) {
if (mPosition == position) return;
mPosition = position; mPosition = position;
mCurrentObject = mAdapter.getItem(position); mCurrentObject = mAdapter.getItem(position);
if (mListener != null) { if (mListener != null) {

View File

@@ -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<NetworkUsageDetailsData>
}
class AppDataUsageDetailsRepository @JvmOverloads constructor(
context: Context,
private val template: NetworkTemplate,
private val cycles: List<Long>?,
private val uids: List<Int>,
private val networkCycleDataRepository: INetworkCycleDataRepository =
NetworkCycleDataRepository(context, template)
) : IAppDataUsageDetailsRepository {
private val networkStatsManager = context.getSystemService(NetworkStatsManager::class.java)!!
override suspend fun queryDetailsForCycles(): List<NetworkUsageDetailsData> = coroutineScope {
getCycles().map {
async {
queryDetails(it)
}
}.awaitAll().filter { it.totalUsage > 0 }
}
private fun getCycles(): List<Range<Long>> =
cycles?.zipWithNext { endTime, startTime -> Range(startTime, endTime) }
?: networkCycleDataRepository.getCycles()
private fun queryDetails(range: Range<Long>): 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<Long>, 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"
}
}

View File

@@ -25,7 +25,9 @@ import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseBooleanArray
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.util.keyIterator
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.AppItem import com.android.settingslib.AppItem
import com.android.settingslib.net.UidDetailProvider import com.android.settingslib.net.UidDetailProvider
@@ -195,6 +197,10 @@ class AppDataUsageRepository(
val bytes: Long, val bytes: Long,
) )
@JvmStatic
fun getAppUidList(uids: SparseBooleanArray) =
uids.keyIterator().asSequence().map { getAppUid(it) }.distinct().toList()
@JvmStatic @JvmStatic
fun getAppUid(uid: Int): Int { fun getAppUid(uid: Int): Int {
if (Process.isSdkSandboxUid(uid)) { if (Process.isSdkSandboxUid(uid)) {

View File

@@ -33,6 +33,7 @@ import kotlinx.coroutines.coroutineScope
interface INetworkCycleDataRepository { interface INetworkCycleDataRepository {
suspend fun loadCycles(): List<NetworkUsageData> suspend fun loadCycles(): List<NetworkUsageData>
fun getCycles(): List<Range<Long>>
fun getPolicy(): NetworkPolicy? fun getPolicy(): NetworkPolicy?
suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData? suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData?
} }
@@ -48,7 +49,7 @@ class NetworkCycleDataRepository(
override suspend fun loadCycles(): List<NetworkUsageData> = override suspend fun loadCycles(): List<NetworkUsageData> =
getCycles().queryUsage().filter { it.usage > 0 } getCycles().queryUsage().filter { it.usage > 0 }
private fun getCycles(): List<Range<Long>> { override fun getCycles(): List<Range<Long>> {
val policy = getPolicy() ?: return queryCyclesAsFourWeeks() val policy = getPolicy() ?: return queryCyclesAsFourWeeks()
return policy.cycleIterator().asSequence().map { return policy.cycleIterator().asSequence().map {
Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli()) Range(it.lower.toInstant().toEpochMilli(), it.upper.toInstant().toEpochMilli())

View File

@@ -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<Long>,
val totalUsage: Long,
val foregroundUsage: Long,
val backgroundUsage: Long,
) {
companion object {
val AllZero = NetworkUsageDetailsData(
range = Range(0, 0),
totalUsage = 0,
foregroundUsage = 0,
backgroundUsage = 0,
)
}
}

View File

@@ -41,8 +41,8 @@ import android.net.NetworkTemplate;
import android.os.Bundle; import android.os.Bundle;
import android.os.Process; import android.os.Process;
import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager;
import android.text.format.DateUtils;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Range;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference; import androidx.preference.Preference;
@@ -51,6 +51,7 @@ import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.applications.AppInfoBase; import com.android.settings.applications.AppInfoBase;
import com.android.settings.datausage.lib.NetworkUsageDetailsData;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowDataUsageUtils; import com.android.settings.testutils.shadow.ShadowDataUsageUtils;
import com.android.settings.testutils.shadow.ShadowEntityHeaderController; 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.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.RestrictedSwitchPreference;
import com.android.settingslib.core.AbstractPreferenceController; 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.UidDetail;
import com.android.settingslib.net.UidDetailProvider; import com.android.settingslib.net.UidDetailProvider;
@@ -80,7 +79,6 @@ import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowSubscriptionManager; import org.robolectric.shadows.ShadowSubscriptionManager;
import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@@ -254,163 +252,33 @@ public class AppDataUsageTest {
} }
@Test @Test
public void bindData_noAppUsageData_shouldHideCycleSpinner() { public void bindData_shouldUpdateUsageSummary() {
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() {
mFragment = spy(new TestFragment()); mFragment = spy(new TestFragment());
final Context context = RuntimeEnvironment.application; final Context context = RuntimeEnvironment.application;
ReflectionHelpers.setField(mFragment, "mContext", context); ReflectionHelpers.setField(mFragment, "mContext", context);
final long backgroundBytes = 1234L; final long backgroundBytes = 1234L;
final long foregroundBytes = 5678L; final long foregroundBytes = 5678L;
final List<NetworkCycleDataForUid> appUsage = new ArrayList<>(); final NetworkUsageDetailsData appUsage = new NetworkUsageDetailsData(
appUsage.add(new NetworkCycleDataForUid.Builder() new Range<>(1L, 2L),
.setBackgroundUsage(backgroundBytes).setForegroundUsage(foregroundBytes).build()); backgroundBytes + foregroundBytes,
ReflectionHelpers.setField(mFragment, "mUsageData", appUsage); foregroundBytes,
backgroundBytes
);
final Preference backgroundPref = mock(Preference.class); final Preference backgroundPref = mock(Preference.class);
ReflectionHelpers.setField(mFragment, "mBackgroundUsage", backgroundPref); ReflectionHelpers.setField(mFragment, "mBackgroundUsage", backgroundPref);
final Preference foregroundPref = mock(Preference.class); final Preference foregroundPref = mock(Preference.class);
ReflectionHelpers.setField(mFragment, "mForegroundUsage", foregroundPref); ReflectionHelpers.setField(mFragment, "mForegroundUsage", foregroundPref);
final Preference totalPref = mock(Preference.class); final Preference totalPref = mock(Preference.class);
ReflectionHelpers.setField(mFragment, "mTotalUsage", totalPref); 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( verify(totalPref).setSummary(
DataUsageUtils.formatDataUsage(context, backgroundBytes + foregroundBytes)); DataUsageUtils.formatDataUsage(context, backgroundBytes + foregroundBytes));
verify(backgroundPref).setSummary(DataUsageUtils.formatDataUsage(context, backgroundBytes)); verify(backgroundPref).setSummary(DataUsageUtils.formatDataUsage(context, backgroundBytes));
verify(foregroundPref).setSummary(DataUsageUtils.formatDataUsage(context, foregroundBytes)); 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<Integer> 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<Integer> 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<Long> 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<Long> 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<NetworkCycleDataForUid> 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 @Test
@Config(shadows = {ShadowDataUsageUtils.class, ShadowSubscriptionManager.class, @Config(shadows = {ShadowDataUsageUtils.class, ShadowSubscriptionManager.class,
ShadowFragment.class}) ShadowFragment.class})
@@ -447,6 +315,10 @@ public class AppDataUsageTest {
return mock(clazz); return mock(clazz);
} }
@Override
void initCycle(List<Integer> uidList) {
}
@Override @Override
public boolean isSimHardwareVisible(Context context) { public boolean isSimHardwareVisible(Context context) {
return true; return true;

View File

@@ -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<NetworkUsageDetailsData>()
}
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<NetworkUsageDetailsData>()
}
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
}
}

View File

@@ -17,7 +17,6 @@
package com.android.settings.datausage package com.android.settings.datausage
import android.content.Context import android.content.Context
import android.util.SparseBooleanArray
import androidx.lifecycle.testing.TestLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
@@ -63,9 +62,7 @@ class AppDataUsageListControllerTest {
@Test @Test
fun onViewCreated_singleUid_hidePreference(): Unit = runBlocking { fun onViewCreated_singleUid_hidePreference(): Unit = runBlocking {
controller.init(SparseBooleanArray().apply { controller.init(listOf(UID_0))
put(UID_0, true)
})
controller.displayPreference(preferenceScreen) controller.displayPreference(preferenceScreen)
controller.onViewCreated(TestLifecycleOwner()) controller.onViewCreated(TestLifecycleOwner())
@@ -76,10 +73,7 @@ class AppDataUsageListControllerTest {
@Test @Test
fun onViewCreated_twoUid_showPreference(): Unit = runBlocking { fun onViewCreated_twoUid_showPreference(): Unit = runBlocking {
controller.init(SparseBooleanArray().apply { controller.init(listOf(UID_0, UID_1))
put(UID_0, true)
put(UID_1, true)
})
controller.displayPreference(preferenceScreen) controller.displayPreference(preferenceScreen)
controller.onViewCreated(TestLifecycleOwner()) controller.onViewCreated(TestLifecycleOwner())

View File

@@ -17,6 +17,7 @@
package com.android.settings.datausage package com.android.settings.datausage
import android.content.Context import android.content.Context
import android.util.Range
import androidx.lifecycle.testing.TestLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
@@ -39,7 +40,7 @@ class ChartDataUsagePreferenceControllerTest {
private val repository = object : INetworkCycleDataRepository { private val repository = object : INetworkCycleDataRepository {
override suspend fun loadCycles() = emptyList<NetworkUsageData>() override suspend fun loadCycles() = emptyList<NetworkUsageData>()
override fun getCycles() = emptyList<Range<Long>>()
override fun getPolicy() = null override fun getPolicy() = null
override suspend fun querySummary(startTime: Long, endTime: Long) = when { override suspend fun querySummary(startTime: Long, endTime: Long) = when {

View File

@@ -18,6 +18,7 @@ package com.android.settings.datausage
import android.content.Context import android.content.Context
import android.net.NetworkTemplate import android.net.NetworkTemplate
import android.util.Range
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.Spinner import android.widget.Spinner
@@ -48,9 +49,8 @@ class DataUsageListHeaderControllerTest {
private val repository = object : INetworkCycleDataRepository { private val repository = object : INetworkCycleDataRepository {
override suspend fun loadCycles() = emptyList<NetworkUsageData>() override suspend fun loadCycles() = emptyList<NetworkUsageData>()
override fun getCycles() = emptyList<Range<Long>>()
override fun getPolicy() = null override fun getPolicy() = null
override suspend fun querySummary(startTime: Long, endTime: Long) = null override suspend fun querySummary(startTime: Long, endTime: Long) = null
} }

View File

@@ -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<NetworkTemplate>()
private val networkCycleDataRepository = mock<INetworkCycleDataRepository> {
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
}
}