Move unrestricted data summary to DataSaverController

DataSaverController currently used in 2 pages,
- Network & internet > Data Saver
- Apps > Special app access

This helps unify the logic, and instead of showing on "Special app
access", this summary shows on "Unrestricted data" is make more sense.

Bug: 280280596
Test: Manually on above pages
Test: Unit test
Change-Id: Ia151ed8179a250f8f20cc5041f9383fffebdab10
This commit is contained in:
Chaohui Wang
2023-05-26 11:38:38 +08:00
parent d8fbd089d8
commit 3e35cff7ba
11 changed files with 139 additions and 449 deletions

View File

@@ -9702,12 +9702,6 @@
<!-- [CHAR_LIMIT=60] Label for special access screen -->
<string name="special_access">Special app access</string>
<!-- Summary for special access settings [CHAR_LIMIT=NONE] -->
<plurals name="special_access_summary">
<item quantity="one">1 app can use unrestricted data</item>
<item quantity="other"><xliff:g id="count" example="10">%d</xliff:g> apps can use unrestricted data</item>
</plurals>
<!-- Title for the See more preference item in Special app access settings [CHAR LIMIT=30] -->
<string name="special_access_more">See more</string>

View File

@@ -105,7 +105,6 @@
android:key="special_access"
android:fragment="com.android.settings.applications.specialaccess.SpecialAccessSettings"
android:title="@string/special_access"
android:order="20"
settings:controller="com.android.settings.applications.SpecialAppAccessPreferenceController"/>
android:order="20"/>
</PreferenceScreen>

View File

@@ -66,7 +66,6 @@ public class AppDashboardFragment extends DashboardFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle());
mAppsPreferenceController = use(AppsPreferenceController.class);
mAppsPreferenceController.setFragment(this /* fragment */);
getSettingsLifecycle().addObserver(mAppsPreferenceController);

View File

@@ -1,154 +0,0 @@
/*
* Copyright (C) 2017 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.applications;
import android.app.Application;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.datausage.AppStateDataUsageBridge;
import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState;
import com.android.settings.datausage.DataSaverBackend;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.ArrayList;
public class SpecialAppAccessPreferenceController extends BasePreferenceController implements
AppStateBaseBridge.Callback, ApplicationsState.Callbacks, LifecycleObserver, OnStart,
OnStop, OnDestroy {
@VisibleForTesting
ApplicationsState.Session mSession;
private final ApplicationsState mApplicationsState;
private final AppStateDataUsageBridge mDataUsageBridge;
private final DataSaverBackend mDataSaverBackend;
private Preference mPreference;
private boolean mExtraLoaded;
public SpecialAppAccessPreferenceController(Context context, String key) {
super(context, key);
mApplicationsState = ApplicationsState.getInstance(
(Application) context.getApplicationContext());
mDataSaverBackend = new DataSaverBackend(context);
mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend);
}
public void setSession(Lifecycle lifecycle) {
mSession = mApplicationsState.newSession(this, lifecycle);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public void onStart() {
mDataUsageBridge.resume(true /* forceLoadAllApps */);
}
@Override
public void onStop() {
mDataUsageBridge.pause();
}
@Override
public void onDestroy() {
mDataUsageBridge.release();
}
@Override
public void updateState(Preference preference) {
updateSummary();
}
@Override
public void onExtraInfoUpdated() {
mExtraLoaded = true;
updateSummary();
}
private void updateSummary() {
if (!mExtraLoaded || mPreference == null) {
return;
}
final ArrayList<ApplicationsState.AppEntry> allApps = mSession.getAllApps();
int count = 0;
for (ApplicationsState.AppEntry entry : allApps) {
if (!ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(entry)) {
continue;
}
if (entry.extraInfo instanceof DataUsageState
&& ((DataUsageState) entry.extraInfo).isDataSaverAllowlisted) {
count++;
}
}
mPreference.setSummary(mContext.getResources().getQuantityString(
R.plurals.special_access_summary, count, count));
}
@Override
public void onRunningStateChanged(boolean running) {
}
@Override
public void onPackageListChanged() {
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
}
@Override
public void onPackageIconChanged() {
}
@Override
public void onPackageSizeChanged(String packageName) {
}
@Override
public void onAllSizesComputed() {
}
@Override
public void onLauncherInfoChanged() {
// when the value of the AppEntry.hasLauncherEntry was changed.
updateSummary();
}
@Override
public void onLoadEntriesCompleted() {
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2017 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.applications.specialaccess;
import android.content.Context;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
public class DataSaverController extends BasePreferenceController {
public DataSaverController(Context context, String key) {
super(context, key);
}
@AvailabilityStatus
public int getAvailabilityStatus() {
return mContext.getResources().getBoolean(R.bool.config_show_data_saver)
? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2017 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.applications.specialaccess
import android.content.Context
import android.net.NetworkPolicyManager
import android.os.UserHandle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settings.core.BasePreferenceController
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 com.google.common.annotations.VisibleForTesting
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DataSaverController(context: Context, key: String) : BasePreferenceController(context, key) {
private lateinit var preference: Preference
@AvailabilityStatus
override fun getAvailabilityStatus(): Int = when {
mContext.resources.getBoolean(R.bool.config_show_data_saver) -> AVAILABLE
else -> UNSUPPORTED_ON_DEVICE
}
override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen)
preference = screen.findPreference(preferenceKey)!!
}
fun init(viewLifecycleOwner: LifecycleOwner) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
preference.summary = getUnrestrictedSummary(mContext)
}
}
}
companion object {
@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 }
}
}
}
}

View File

@@ -21,6 +21,10 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.MANAGE_DE
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
@@ -46,6 +50,12 @@ public class SpecialAccessSettings extends DashboardFragment {
MANAGE_DEVICE_ADMIN_APPS, R.string.manage_device_admin);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
use(DataSaverController.class).init(getViewLifecycleOwner());
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.special_access;

View File

@@ -17,34 +17,22 @@ package com.android.settings.datausage
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.view.View
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.specialaccess.DataSaverController
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.widget.SettingsMainSwitchBar
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() {
class DataSaverSummary : DashboardFragment() {
private lateinit var switchBar: SettingsMainSwitchBar
private lateinit var dataSaverBackend: DataSaverBackend
private lateinit var unrestrictedAccess: Preference
// Flag used to avoid infinite loop due if user switch it on/off too quick.
private var switching = false
@@ -57,8 +45,6 @@ class DataSaverSummary : SettingsPreferenceFragment() {
return
}
addPreferencesFromResource(R.xml.data_saver)
unrestrictedAccess = findPreference(KEY_UNRESTRICTED_ACCESS)!!
dataSaverBackend = DataSaverBackend(requireContext())
}
@@ -73,12 +59,14 @@ class DataSaverSummary : SettingsPreferenceFragment() {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
use(DataSaverController::class.java).init(viewLifecycleOwner)
}
override fun onResume() {
super.onResume()
dataSaverBackend.addListener(dataSaverBackendListener)
viewLifecycleOwner.lifecycleScope.launch {
unrestrictedAccess.summary = getUnrestrictedSummary(requireContext())
}
}
override fun onPause() {
@@ -95,9 +83,10 @@ class DataSaverSummary : SettingsPreferenceFragment() {
}
}
override fun getPreferenceScreenResId() = R.xml.data_saver
override fun getMetricsCategory() = SettingsEnums.DATA_SAVER_SUMMARY
override fun getHelpResource() = R.string.help_url_data_saver
override fun getLogTag() = TAG
private val dataSaverBackendListener = object : DataSaverBackend.Listener {
override fun onDataSaverChanged(isDataSaving: Boolean) {
@@ -109,32 +98,7 @@ class DataSaverSummary : SettingsPreferenceFragment() {
}
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 const val TAG = "DataSaverSummary"
private fun Context.isDataSaverVisible(): Boolean =
resources.getBoolean(R.bool.config_show_data_saver)

View File

@@ -1,125 +0,0 @@
/*
* Copyright (C) 2017 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.applications;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ModuleInfo;
import android.content.pm.PackageManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.datausage.AppStateDataUsageBridge;
import com.android.settings.testutils.shadow.ShadowApplicationsState;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settingslib.applications.ApplicationsState;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowUserManager.class, ShadowApplicationsState.class})
public class SpecialAppAccessPreferenceControllerTest {
private Context mContext;
@Mock
private ApplicationsState.Session mSession;
@Mock
private PreferenceScreen mScreen;
@Mock
private PackageManager mPackageManager;
private SpecialAppAccessPreferenceController mController;
private Preference mPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
when(mContext.getApplicationContext()).thenReturn(mContext);
ShadowUserManager.getShadow().setProfileIdsWithDisabled(new int[]{0});
doReturn(mPackageManager).when(mContext).getPackageManager();
doReturn(new ArrayList<ModuleInfo>()).when(mPackageManager).getInstalledModules(anyInt());
mController = new SpecialAppAccessPreferenceController(mContext, "test_key");
mPreference = new Preference(mContext);
when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
mController.mSession = mSession;
}
@Test
public void getAvailabilityState_unsearchable() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void updateState_shouldSetSummary() {
final ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>();
final ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class);
entry.hasLauncherEntry = true;
entry.info = new ApplicationInfo();
entry.extraInfo = new AppStateDataUsageBridge.DataUsageState(
true /* allowlisted */, false /* denylisted */);
apps.add(entry);
when(mSession.getAllApps()).thenReturn(apps);
mController.displayPreference(mScreen);
mController.onExtraInfoUpdated();
assertThat(mPreference.getSummary())
.isEqualTo(mContext.getResources().getQuantityString(
R.plurals.special_access_summary, 1, 1));
}
@Test
public void updateState_wrongExtraInfo_shouldNotIncludeInSummary() {
final ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>();
final ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class);
entry.hasLauncherEntry = true;
entry.info = new ApplicationInfo();
entry.extraInfo = new AppStateNotificationBridge.NotificationsSentState();
apps.add(entry);
when(mSession.getAllApps()).thenReturn(apps);
mController.displayPreference(mScreen);
mController.onExtraInfoUpdated();
assertThat(mPreference.getSummary())
.isEqualTo(mContext.getResources().getQuantityString(
R.plurals.special_access_summary, 0, 0));
}
}

View File

@@ -1,74 +0,0 @@
/*
* Copyright (C) 2017 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.applications.specialaccess;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import com.android.settings.R;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
public class DataSaverControllerTest {
private Context mContext;
private Resources mResources;
private DataSaverController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application.getApplicationContext());
mResources = spy(mContext.getResources());
when(mContext.getResources()).thenReturn(mResources);
mController = new DataSaverController(mContext, "key");
}
@Test
public void testDataSaver_byDefault_shouldBeShown() {
when(mResources.getBoolean(R.bool.config_show_data_saver)).thenReturn(true);
assertThat(mController.isAvailable()).isTrue();
}
@Ignore
@Test
@Config(qualifiers = "mcc999")
public void testDataSaver_ifDisabledByCarrier_shouldNotBeShown() {
assertThat(mController.isAvailable()).isFalse();
}
@Test
public void testDataSaver_ifDisabled_shouldNotBeShown() {
when(mResources.getBoolean(R.bool.config_show_data_saver)).thenReturn(false);
assertThat(mController.isAvailable()).isFalse();
}
}

View File

@@ -14,15 +14,19 @@
* limitations under the License.
*/
package com.android.settings.datausage
package com.android.settings.applications.specialaccess
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.res.Resources
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.settings.R
import com.android.settings.applications.specialaccess.DataSaverController.Companion.getUnrestrictedSummary
import com.android.settings.core.BasePreferenceController.AVAILABLE
import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
import com.android.settingslib.spaprivileged.model.app.AppListRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -41,20 +45,41 @@ import org.mockito.Mockito.`when` as whenever
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class DataSaverSummaryTest {
class DataSaverControllerTest {
@get:Rule
val mockito: MockitoRule = MockitoJUnit.rule()
@Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@Spy
private val resources: Resources = context.resources
@Mock
private lateinit var networkPolicyManager: NetworkPolicyManager
@Mock
private lateinit var dataSaverController: DataSaverController
@Before
fun setUp() {
whenever(context.applicationContext).thenReturn(context)
whenever(context.resources).thenReturn(resources)
whenever(NetworkPolicyManager.from(context)).thenReturn(networkPolicyManager)
dataSaverController = DataSaverController(context, "key")
}
@Test
fun getAvailabilityStatus_whenConfigOn_available() {
whenever(resources.getBoolean(R.bool.config_show_data_saver)).thenReturn(true)
assertThat(dataSaverController.availabilityStatus).isEqualTo(AVAILABLE)
}
@Test
fun getAvailabilityStatus_whenConfigOff_unsupportedOnDevice() {
whenever(resources.getBoolean(R.bool.config_show_data_saver)).thenReturn(false)
assertThat(dataSaverController.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
}
@Test