diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java index e47ecbdc997..6a392341122 100644 --- a/src/com/android/settings/datausage/DataSaverBackend.java +++ b/src/com/android/settings/datausage/DataSaverBackend.java @@ -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) {} } } diff --git a/src/com/android/settings/datausage/DataSaverSummary.kt b/src/com/android/settings/datausage/DataSaverSummary.kt index 1d9cbb73a66..7f38900bbd2 100644 --- a/src/com/android/settings/datausage/DataSaverSummary.kt +++ b/src/com/android/settings/datausage/DataSaverSummary.kt @@ -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 @@ -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) { @@ -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?) {} - - override fun onPackageIconChanged() {} - - override fun onPackageSizeChanged(packageName: String?) {} - - override fun onAllSizesComputed() { - updateUnrestrictedAccessSummary() - } - - override fun onLauncherInfoChanged() { - updateUnrestrictedAccessSummary() - } - - override fun onLoadEntriesCompleted() {} - } - - 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 - } - unrestrictedAccess.summary = - resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count) } companion object { private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access" + @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), + ) + + private suspend fun getAllowCount(context: Context, appListRepository: AppListRepository) = + withContext(Dispatchers.IO) { + coroutineScope { + val appsDeferred = async { + appListRepository.loadAndFilterApps( + userId = UserHandle.myUserId(), + isSystemApp = false, + ) + } + val uidsAllowed = NetworkPolicyManager.from(context) + .getUidsWithPolicy(NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND) + appsDeferred.await().count { app -> app.uid in uidsAllowed } + } + } + private fun Context.isDataSaverVisible(): Boolean = resources.getBoolean(R.bool.config_show_data_saver) diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt new file mode 100644 index 00000000000..3c88d8e875b --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.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 + +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() + + override fun showSystemPredicate( + userIdFlow: Flow, + showSystemFlow: Flow, + ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { false } + + override fun getSystemPackageNamesBlocking(userId: Int): Set = emptySet() + + override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = + listOf(APP1, APP2, APP3) + } + } +} \ No newline at end of file