Loading src/com/android/settings/datausage/DataSaverBackend.java +4 −2 Original line number Diff line number Diff line Loading @@ -196,8 +196,10 @@ public class DataSaverBackend { public interface Listener { void onDataSaverChanged(boolean isDataSaving); void onAllowlistStatusChanged(int uid, boolean isAllowlisted); /** This is called when allow list status is changed. */ default void onAllowlistStatusChanged(int uid, boolean isAllowlisted) {} void onDenylistStatusChanged(int uid, boolean isDenylisted); /** This is called when deny list status is changed. */ default void onDenylistStatusChanged(int uid, boolean isDenylisted) {} } } src/com/android/settings/datausage/DataSaverSummary.kt +37 −62 Original line number Diff line number Diff line Loading @@ -15,33 +15,36 @@ */ package com.android.settings.datausage import android.app.Application import android.app.settings.SettingsEnums import android.content.Context import android.net.NetworkPolicyManager import android.os.Bundle import android.os.UserHandle import android.telephony.SubscriptionManager import android.widget.Switch import androidx.annotation.VisibleForTesting import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import com.android.settings.R import com.android.settings.SettingsActivity import com.android.settings.SettingsPreferenceFragment import com.android.settings.applications.AppStateBaseBridge import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.widget.SettingsMainSwitchBar import com.android.settingslib.applications.ApplicationsState import com.android.settingslib.search.SearchIndexable import com.android.settingslib.spa.framework.util.formatString import com.android.settingslib.spaprivileged.model.app.AppListRepository import com.android.settingslib.spaprivileged.model.app.AppListRepositoryImpl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @SearchIndexable class DataSaverSummary : SettingsPreferenceFragment() { private lateinit var switchBar: SettingsMainSwitchBar private lateinit var dataSaverBackend: DataSaverBackend private lateinit var unrestrictedAccess: Preference private var dataUsageBridge: AppStateDataUsageBridge? = null private var session: ApplicationsState.Session? = null // Flag used to avoid infinite loop due if user switch it on/off too quick. private var switching = false Loading Loading @@ -72,27 +75,15 @@ class DataSaverSummary : SettingsPreferenceFragment() { override fun onResume() { super.onResume() dataSaverBackend.refreshAllowlist() dataSaverBackend.refreshDenylist() dataSaverBackend.addListener(dataSaverBackendListener) dataUsageBridge?.resume(/* forceLoadAllApps= */ true) ?: viewLifecycleOwner.lifecycleScope.launch { val applicationsState = ApplicationsState.getInstance( requireContext().applicationContext as Application ) dataUsageBridge = AppStateDataUsageBridge( applicationsState, dataUsageBridgeCallbacks, dataSaverBackend ) session = applicationsState.newSession(applicationsStateCallbacks, settingsLifecycle) dataUsageBridge?.resume(/* forceLoadAllApps= */ true) viewLifecycleOwner.lifecycleScope.launch { unrestrictedAccess.summary = getUnrestrictedSummary(requireContext()) } } override fun onPause() { super.onPause() dataSaverBackend.remListener(dataSaverBackendListener) dataUsageBridge?.pause() } private fun onSwitchChanged(isChecked: Boolean) { Loading @@ -115,52 +106,36 @@ class DataSaverSummary : SettingsPreferenceFragment() { switching = false } } override fun onAllowlistStatusChanged(uid: Int, isAllowlisted: Boolean) {} override fun onDenylistStatusChanged(uid: Int, isDenylisted: Boolean) {} } private val dataUsageBridgeCallbacks = AppStateBaseBridge.Callback { updateUnrestrictedAccessSummary() } private val applicationsStateCallbacks = object : ApplicationsState.Callbacks { override fun onRunningStateChanged(running: Boolean) {} override fun onPackageListChanged() {} override fun onRebuildComplete(apps: ArrayList<ApplicationsState.AppEntry>?) {} override fun onPackageIconChanged() {} override fun onPackageSizeChanged(packageName: String?) {} override fun onAllSizesComputed() { updateUnrestrictedAccessSummary() } companion object { private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access" override fun onLauncherInfoChanged() { updateUnrestrictedAccessSummary() } @VisibleForTesting suspend fun getUnrestrictedSummary( context: Context, appListRepository: AppListRepository = AppListRepositoryImpl(context.applicationContext), ) = context.formatString( R.string.data_saver_unrestricted_summary, "count" to getAllowCount(context.applicationContext, appListRepository), ) override fun onLoadEntriesCompleted() {} private suspend fun getAllowCount(context: Context, appListRepository: AppListRepository) = withContext(Dispatchers.IO) { coroutineScope { val appsDeferred = async { appListRepository.loadAndFilterApps( userId = UserHandle.myUserId(), isSystemApp = false, ) } private fun updateUnrestrictedAccessSummary() { if (!isAdded || isFinishingOrDestroyed) return val allApps = session?.allApps ?: return val count = allApps.count { ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(it) && (it.extraInfo as? DataUsageState)?.isDataSaverAllowlisted == true val uidsAllowed = NetworkPolicyManager.from(context) .getUidsWithPolicy(NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND) appsDeferred.await().count { app -> app.uid in uidsAllowed } } unrestrictedAccess.summary = resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count) } companion object { private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access" private fun Context.isDataSaverVisible(): Boolean = resources.getBoolean(R.bool.config_show_data_saver) Loading tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt 0 → 100644 +109 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.datausage import android.content.Context import android.content.pm.ApplicationInfo import android.net.NetworkPolicyManager import android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.datausage.DataSaverSummary.Companion.getUnrestrictedSummary import com.android.settingslib.spaprivileged.model.app.AppListRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class DataSaverSummaryTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var networkPolicyManager: NetworkPolicyManager @Before fun setUp() { whenever(context.applicationContext).thenReturn(context) whenever(NetworkPolicyManager.from(context)).thenReturn(networkPolicyManager) } @Test fun getUnrestrictedSummary_whenTwoAppsAllowed() = runTest { whenever( networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND) ).thenReturn(intArrayOf(APP1.uid, APP2.uid)) val summary = getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository) assertThat(summary) .isEqualTo("2 apps allowed to use unrestricted data when Data Saver is on") } @Test fun getUnrestrictedSummary_whenNoAppsAllowed() = runTest { whenever( networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND) ).thenReturn(intArrayOf()) val summary = getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository) assertThat(summary) .isEqualTo("0 apps allowed to use unrestricted data when Data Saver is on") } private companion object { val APP1 = ApplicationInfo().apply { uid = 10001 } val APP2 = ApplicationInfo().apply { uid = 10002 } val APP3 = ApplicationInfo().apply { uid = 10003 } object FakeAppListRepository : AppListRepository { override suspend fun loadApps( userId: Int, loadInstantApps: Boolean, matchAnyUserForAdmin: Boolean, ) = emptyList<ApplicationInfo>() override fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { false } override fun getSystemPackageNamesBlocking(userId: Int): Set<String> = emptySet() override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = listOf(APP1, APP2, APP3) } } } No newline at end of file Loading
src/com/android/settings/datausage/DataSaverBackend.java +4 −2 Original line number Diff line number Diff line Loading @@ -196,8 +196,10 @@ public class DataSaverBackend { public interface Listener { void onDataSaverChanged(boolean isDataSaving); void onAllowlistStatusChanged(int uid, boolean isAllowlisted); /** This is called when allow list status is changed. */ default void onAllowlistStatusChanged(int uid, boolean isAllowlisted) {} void onDenylistStatusChanged(int uid, boolean isDenylisted); /** This is called when deny list status is changed. */ default void onDenylistStatusChanged(int uid, boolean isDenylisted) {} } }
src/com/android/settings/datausage/DataSaverSummary.kt +37 −62 Original line number Diff line number Diff line Loading @@ -15,33 +15,36 @@ */ package com.android.settings.datausage import android.app.Application import android.app.settings.SettingsEnums import android.content.Context import android.net.NetworkPolicyManager import android.os.Bundle import android.os.UserHandle import android.telephony.SubscriptionManager import android.widget.Switch import androidx.annotation.VisibleForTesting import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import com.android.settings.R import com.android.settings.SettingsActivity import com.android.settings.SettingsPreferenceFragment import com.android.settings.applications.AppStateBaseBridge import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.widget.SettingsMainSwitchBar import com.android.settingslib.applications.ApplicationsState import com.android.settingslib.search.SearchIndexable import com.android.settingslib.spa.framework.util.formatString import com.android.settingslib.spaprivileged.model.app.AppListRepository import com.android.settingslib.spaprivileged.model.app.AppListRepositoryImpl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @SearchIndexable class DataSaverSummary : SettingsPreferenceFragment() { private lateinit var switchBar: SettingsMainSwitchBar private lateinit var dataSaverBackend: DataSaverBackend private lateinit var unrestrictedAccess: Preference private var dataUsageBridge: AppStateDataUsageBridge? = null private var session: ApplicationsState.Session? = null // Flag used to avoid infinite loop due if user switch it on/off too quick. private var switching = false Loading Loading @@ -72,27 +75,15 @@ class DataSaverSummary : SettingsPreferenceFragment() { override fun onResume() { super.onResume() dataSaverBackend.refreshAllowlist() dataSaverBackend.refreshDenylist() dataSaverBackend.addListener(dataSaverBackendListener) dataUsageBridge?.resume(/* forceLoadAllApps= */ true) ?: viewLifecycleOwner.lifecycleScope.launch { val applicationsState = ApplicationsState.getInstance( requireContext().applicationContext as Application ) dataUsageBridge = AppStateDataUsageBridge( applicationsState, dataUsageBridgeCallbacks, dataSaverBackend ) session = applicationsState.newSession(applicationsStateCallbacks, settingsLifecycle) dataUsageBridge?.resume(/* forceLoadAllApps= */ true) viewLifecycleOwner.lifecycleScope.launch { unrestrictedAccess.summary = getUnrestrictedSummary(requireContext()) } } override fun onPause() { super.onPause() dataSaverBackend.remListener(dataSaverBackendListener) dataUsageBridge?.pause() } private fun onSwitchChanged(isChecked: Boolean) { Loading @@ -115,52 +106,36 @@ class DataSaverSummary : SettingsPreferenceFragment() { switching = false } } override fun onAllowlistStatusChanged(uid: Int, isAllowlisted: Boolean) {} override fun onDenylistStatusChanged(uid: Int, isDenylisted: Boolean) {} } private val dataUsageBridgeCallbacks = AppStateBaseBridge.Callback { updateUnrestrictedAccessSummary() } private val applicationsStateCallbacks = object : ApplicationsState.Callbacks { override fun onRunningStateChanged(running: Boolean) {} override fun onPackageListChanged() {} override fun onRebuildComplete(apps: ArrayList<ApplicationsState.AppEntry>?) {} override fun onPackageIconChanged() {} override fun onPackageSizeChanged(packageName: String?) {} override fun onAllSizesComputed() { updateUnrestrictedAccessSummary() } companion object { private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access" override fun onLauncherInfoChanged() { updateUnrestrictedAccessSummary() } @VisibleForTesting suspend fun getUnrestrictedSummary( context: Context, appListRepository: AppListRepository = AppListRepositoryImpl(context.applicationContext), ) = context.formatString( R.string.data_saver_unrestricted_summary, "count" to getAllowCount(context.applicationContext, appListRepository), ) override fun onLoadEntriesCompleted() {} private suspend fun getAllowCount(context: Context, appListRepository: AppListRepository) = withContext(Dispatchers.IO) { coroutineScope { val appsDeferred = async { appListRepository.loadAndFilterApps( userId = UserHandle.myUserId(), isSystemApp = false, ) } private fun updateUnrestrictedAccessSummary() { if (!isAdded || isFinishingOrDestroyed) return val allApps = session?.allApps ?: return val count = allApps.count { ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(it) && (it.extraInfo as? DataUsageState)?.isDataSaverAllowlisted == true val uidsAllowed = NetworkPolicyManager.from(context) .getUidsWithPolicy(NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND) appsDeferred.await().count { app -> app.uid in uidsAllowed } } unrestrictedAccess.summary = resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count) } companion object { private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access" private fun Context.isDataSaverVisible(): Boolean = resources.getBoolean(R.bool.config_show_data_saver) Loading
tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt 0 → 100644 +109 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.datausage import android.content.Context import android.content.pm.ApplicationInfo import android.net.NetworkPolicyManager import android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.datausage.DataSaverSummary.Companion.getUnrestrictedSummary import com.android.settingslib.spaprivileged.model.app.AppListRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class DataSaverSummaryTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var networkPolicyManager: NetworkPolicyManager @Before fun setUp() { whenever(context.applicationContext).thenReturn(context) whenever(NetworkPolicyManager.from(context)).thenReturn(networkPolicyManager) } @Test fun getUnrestrictedSummary_whenTwoAppsAllowed() = runTest { whenever( networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND) ).thenReturn(intArrayOf(APP1.uid, APP2.uid)) val summary = getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository) assertThat(summary) .isEqualTo("2 apps allowed to use unrestricted data when Data Saver is on") } @Test fun getUnrestrictedSummary_whenNoAppsAllowed() = runTest { whenever( networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND) ).thenReturn(intArrayOf()) val summary = getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository) assertThat(summary) .isEqualTo("0 apps allowed to use unrestricted data when Data Saver is on") } private companion object { val APP1 = ApplicationInfo().apply { uid = 10001 } val APP2 = ApplicationInfo().apply { uid = 10002 } val APP3 = ApplicationInfo().apply { uid = 10003 } object FakeAppListRepository : AppListRepository { override suspend fun loadApps( userId: Int, loadInstantApps: Boolean, matchAnyUserForAdmin: Boolean, ) = emptyList<ApplicationInfo>() override fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { false } override fun getSystemPackageNamesBlocking(userId: Int): Set<String> = emptySet() override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = listOf(APP1, APP2, APP3) } } } No newline at end of file