From b8b2e51e8de8e41f9962260918c47459911a0a4a Mon Sep 17 00:00:00 2001 From: Sudheer Shanka Date: Tue, 23 Jan 2018 01:46:53 -0800 Subject: [PATCH] Add policy transparency for metered data related settings. With API Dpm.setMeteredDataDisabled, admins can restrict some packages from accessing metered data and when admin does restrict packages, user settings to enable usage of mobile data in background or allow unrestricted access during data saver mode are not relevant. Add policy transparency to these settings so that user knows that admin disabled them. Bug: 63700027 Test: make RunSettingsRoboTests Test: manual Change-Id: I450f7a91356ed8fb33f464620c73fa9407a1ff83 --- res/xml/app_data_usage.xml | 14 ++- .../settings/datausage/AppDataUsage.java | 18 +++- .../datausage/UnrestrictedDataAccess.java | 55 ++++++++++- .../settings/datausage/AppDataUsageTest.java | 39 +++++++- .../datausage/UnrestrictedDataAccessTest.java | 97 ++++++++++++++++++- .../shadow/ShadowRestrictedLockUtils.java | 65 +++++++++++++ 6 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowRestrictedLockUtils.java diff --git a/res/xml/app_data_usage.xml b/res/xml/app_data_usage.xml index 3e94135fb4b..a4b215980aa 100644 --- a/res/xml/app_data_usage.xml +++ b/res/xml/app_data_usage.xml @@ -16,6 +16,8 @@ - + android:summary="@string/data_usage_app_restrict_background_summary" + settings:useAdditionalSummary="true" + settings:restrictedSwitchSummary="@string/disabled_by_admin" /> - + android:summary="@string/unrestricted_app_summary" + settings:useAdditionalSummary="true" + settings:restrictedSwitchSummary="@string/disabled_by_admin" /> diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java index 5470e6372db..a0d0ec03551 100644 --- a/src/com/android/settings/datausage/AppDataUsage.java +++ b/src/com/android/settings/datausage/AppDataUsage.java @@ -33,7 +33,6 @@ import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.support.annotation.VisibleForTesting; -import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceCategory; import android.text.format.Formatter; @@ -48,6 +47,9 @@ import com.android.settings.R; import com.android.settings.applications.AppInfoBase; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.AppItem; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.net.ChartData; import com.android.settingslib.net.ChartDataLoader; import com.android.settingslib.net.UidDetail; @@ -80,7 +82,7 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen private Preference mForegroundUsage; private Preference mBackgroundUsage; private Preference mAppSettings; - private SwitchPreference mRestrictBackground; + private RestrictedSwitchPreference mRestrictBackground; private PreferenceCategory mAppList; private Drawable mIcon; @@ -97,7 +99,7 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen private AppItem mAppItem; private Intent mAppSettingsIntent; private SpinnerPreference mCycle; - private SwitchPreference mUnrestrictedData; + private RestrictedSwitchPreference mUnrestrictedData; private DataSaverBackend mDataSaverBackend; @Override @@ -160,9 +162,11 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen removePreference(KEY_UNRESTRICTED_DATA); removePreference(KEY_RESTRICT_BACKGROUND); } else { - mRestrictBackground = (SwitchPreference) findPreference(KEY_RESTRICT_BACKGROUND); + mRestrictBackground = (RestrictedSwitchPreference) findPreference( + KEY_RESTRICT_BACKGROUND); mRestrictBackground.setOnPreferenceChangeListener(this); - mUnrestrictedData = (SwitchPreference) findPreference(KEY_UNRESTRICTED_DATA); + mUnrestrictedData = (RestrictedSwitchPreference) findPreference( + KEY_UNRESTRICTED_DATA); mUnrestrictedData.setOnPreferenceChangeListener(this); } mDataSaverBackend = new DataSaverBackend(getContext()); @@ -261,8 +265,11 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } private void updatePrefs(boolean restrictBackground, boolean unrestrictData) { + final EnforcedAdmin admin = RestrictedLockUtils.checkIfMeteredDataRestricted( + getContext(), mPackageName, UserHandle.getUserId(mAppItem.key)); if (mRestrictBackground != null) { mRestrictBackground.setChecked(!restrictBackground); + mRestrictBackground.setDisabledByAdmin(admin); } if (mUnrestrictedData != null) { if (restrictBackground) { @@ -270,6 +277,7 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } else { mUnrestrictedData.setVisible(true); mUnrestrictedData.setChecked(unrestrictData); + mUnrestrictedData.setDisabledByAdmin(admin); } } } diff --git a/src/com/android/settings/datausage/UnrestrictedDataAccess.java b/src/com/android/settings/datausage/UnrestrictedDataAccess.java index e8a7bbfa2f9..cff4a502147 100644 --- a/src/com/android/settings/datausage/UnrestrictedDataAccess.java +++ b/src/com/android/settings/datausage/UnrestrictedDataAccess.java @@ -14,6 +14,8 @@ package com.android.settings.datausage; +import static com.android.settingslib.RestrictedLockUtils.checkIfMeteredDataRestricted; + import android.app.Application; import android.content.Context; import android.os.Bundle; @@ -37,6 +39,8 @@ import com.android.settings.core.FeatureFlags; import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState; import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.AppSwitchPreference; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedPreferenceHelper; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppFilter; @@ -172,6 +176,8 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment preference.setOnPreferenceChangeListener(this); getPreferenceScreen().addPreference(preference); } else { + preference.setDisabledByAdmin(checkIfMeteredDataRestricted(getContext(), + entry.info.packageName, UserHandle.getUserId(entry.info.uid))); preference.reuse(); } preference.setOrder(i); @@ -242,16 +248,22 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment return app != null && UserHandle.isApp(app.info.uid); } - private class AccessPreference extends AppSwitchPreference + @VisibleForTesting + class AccessPreference extends AppSwitchPreference implements DataSaverBackend.Listener { private final AppEntry mEntry; private final DataUsageState mState; + private final RestrictedPreferenceHelper mHelper; public AccessPreference(final Context context, AppEntry entry) { super(context); + setWidgetLayoutResource(R.layout.restricted_switch_widget); + mHelper = new RestrictedPreferenceHelper(context, this, null); mEntry = entry; mState = (DataUsageState) mEntry.extraInfo; mEntry.ensureLabel(getContext()); + setDisabledByAdmin(checkIfMeteredDataRestricted(context, entry.info.packageName, + UserHandle.getUserId(entry.info.uid))); setState(); if (mEntry.icon != null) { setIcon(mEntry.icon); @@ -291,12 +303,21 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment } } + @Override + public void performClick() { + if (!mHelper.performClick()) { + super.performClick(); + } + } + // Sets UI state based on whitelist/blacklist status. private void setState() { setTitle(mEntry.label); if (mState != null) { setChecked(mState.isDataSaverWhitelisted); - if (mState.isDataSaverBlacklisted) { + if (isDisabledByAdmin()) { + setSummary(R.string.disabled_by_admin); + } else if (mState.isDataSaverBlacklisted) { setSummary(R.string.restrict_background_blacklisted); } else { setSummary(""); @@ -323,10 +344,21 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment } }); } - holder.findViewById(android.R.id.widget_frame) - .setVisibility(mState != null && mState.isDataSaverBlacklisted - ? View.INVISIBLE : View.VISIBLE); + final boolean disabledByAdmin = isDisabledByAdmin(); + final View widgetFrame = holder.findViewById(android.R.id.widget_frame); + if (disabledByAdmin) { + widgetFrame.setVisibility(View.VISIBLE); + } else { + widgetFrame.setVisibility(mState != null && mState.isDataSaverBlacklisted + ? View.INVISIBLE : View.VISIBLE); + } super.onBindViewHolder(holder); + + mHelper.onBindViewHolder(holder); + holder.findViewById(R.id.restricted_icon).setVisibility( + disabledByAdmin ? View.VISIBLE : View.GONE); + holder.findViewById(android.R.id.switch_widget).setVisibility( + disabledByAdmin ? View.GONE : View.VISIBLE); } @Override @@ -348,6 +380,19 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment reuse(); } } + + public void setDisabledByAdmin(EnforcedAdmin admin) { + mHelper.setDisabledByAdmin(admin); + } + + public boolean isDisabledByAdmin() { + return mHelper.isDisabledByAdmin(); + } + + @VisibleForTesting + public AppEntry getEntryForTest() { + return mEntry; + } } } diff --git a/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java b/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java index 7cd09dec861..58643b6560f 100644 --- a/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java +++ b/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java @@ -29,8 +29,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.pm.PackageManager; +import android.net.NetworkPolicyManager; import android.os.Bundle; -import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.PreferenceManager; import android.support.v7.preference.PreferenceScreen; import android.util.ArraySet; @@ -40,8 +40,11 @@ import com.android.settings.TestConfig; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.shadow.ShadowEntityHeaderController; +import com.android.settings.testutils.shadow.ShadowRestrictedLockUtils; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.AppItem; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.wrapper.PackageManagerWrapper; import org.junit.After; @@ -57,7 +60,10 @@ import org.robolectric.util.ReflectionHelpers; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, - shadows = ShadowEntityHeaderController.class) + shadows = { + ShadowEntityHeaderController.class, + ShadowRestrictedLockUtils.class + }) public class AppDataUsageTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) @@ -134,7 +140,7 @@ public class AppDataUsageTest { public void changePreference_backgroundData_shouldUpdateUI() { mFragment = spy(new AppDataUsage()); final AppItem appItem = new AppItem(123456789); - final SwitchPreference pref = mock(SwitchPreference.class); + final RestrictedSwitchPreference pref = mock(RestrictedSwitchPreference.class); final DataSaverBackend dataSaverBackend = mock(DataSaverBackend.class); ReflectionHelpers.setField(mFragment, "mAppItem", appItem); ReflectionHelpers.setField(mFragment, "mRestrictBackground", pref); @@ -146,4 +152,31 @@ public class AppDataUsageTest { verify(mFragment).updatePrefs(); } + + @Test + public void updatePrefs_restrictedByAdmin_shouldDisablePreference() { + mFragment = spy(new AppDataUsage()); + final int testUid = 123123; + final AppItem appItem = new AppItem(testUid); + final RestrictedSwitchPreference restrictBackgroundPref + = mock(RestrictedSwitchPreference.class); + final RestrictedSwitchPreference unrestrictedDataPref + = mock(RestrictedSwitchPreference.class); + final DataSaverBackend dataSaverBackend = mock(DataSaverBackend.class); + final NetworkPolicyManager networkPolicyManager = mock(NetworkPolicyManager.class); + ReflectionHelpers.setField(mFragment, "mAppItem", appItem); + ReflectionHelpers.setField(mFragment, "mRestrictBackground", restrictBackgroundPref); + ReflectionHelpers.setField(mFragment, "mUnrestrictedData", unrestrictedDataPref); + ReflectionHelpers.setField(mFragment, "mDataSaverBackend", dataSaverBackend); + ReflectionHelpers.setField(mFragment.services, "mPolicyManager", networkPolicyManager); + + ShadowRestrictedLockUtils.setRestricted(true); + doReturn(NetworkPolicyManager.POLICY_NONE).when(networkPolicyManager) + .getUidPolicy(testUid); + + mFragment.updatePrefs(); + + verify(restrictBackgroundPref).setDisabledByAdmin(any(EnforcedAdmin.class)); + verify(unrestrictedDataPref).setDisabledByAdmin(any(EnforcedAdmin.class)); + } } diff --git a/tests/robotests/src/com/android/settings/datausage/UnrestrictedDataAccessTest.java b/tests/robotests/src/com/android/settings/datausage/UnrestrictedDataAccessTest.java index 53cb7edd25e..fff879f9f11 100644 --- a/tests/robotests/src/com/android/settings/datausage/UnrestrictedDataAccessTest.java +++ b/tests/robotests/src/com/android/settings/datausage/UnrestrictedDataAccessTest.java @@ -16,41 +16,68 @@ package com.android.settings.datausage; import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Process; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.preference.PreferenceScreen; import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; import com.android.settings.TestConfig; +import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState; +import com.android.settings.datausage.UnrestrictedDataAccess.AccessPreference; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; -import com.android.settingslib.applications.ApplicationsState; +import com.android.settings.testutils.shadow.ShadowRestrictedLockUtils; +import com.android.settingslib.applications.ApplicationsState.AppEntry; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import java.util.ArrayList; @RunWith(SettingsRobolectricTestRunner.class) -@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, + shadows = { + ShadowRestrictedLockUtils.class + }) public class UnrestrictedDataAccessTest { @Mock - private ApplicationsState.AppEntry mAppEntry; + private AppEntry mAppEntry; private UnrestrictedDataAccess mFragment; private FakeFeatureFactory mFeatureFactory; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private PreferenceManager mPreferenceManager; + @Mock + private DataSaverBackend mDataSaverBackend; @Before public void setUp() { MockitoAnnotations.initMocks(this); mFeatureFactory = FakeFeatureFactory.setupForTest(); - mFragment = new UnrestrictedDataAccess(); + mFragment = spy(new UnrestrictedDataAccess()); } @Test @@ -80,4 +107,66 @@ public class UnrestrictedDataAccessTest { eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_UNL_DATA_DENY), eq("app")); } + @Test + public void testOnRebuildComplete_restricted_shouldBeDisabled() { + final Context context = RuntimeEnvironment.application; + doReturn(context).when(mFragment).getContext(); + doReturn(context).when(mPreferenceManager).getContext(); + doReturn(true).when(mFragment).shouldAddPreference(any(AppEntry.class)); + doNothing().when(mFragment).setLoading(anyBoolean(), anyBoolean()); + doReturn(mPreferenceScreen).when(mFragment).getPreferenceScreen(); + doReturn(mPreferenceManager).when(mFragment).getPreferenceManager(); + ReflectionHelpers.setField(mFragment, "mDataSaverBackend", mDataSaverBackend); + + final String testPkg1 = "com.example.one"; + final String testPkg2 = "com.example.two"; + ShadowRestrictedLockUtils.setRestrictedPkgs(testPkg2); + + doAnswer((invocation) -> { + final AccessPreference preference = invocation.getArgument(0); + final AppEntry entry = preference.getEntryForTest(); + // Verify preference is disabled by admin and the summary is changed accordingly. + if (testPkg1.equals(entry.info.packageName)) { + assertThat(preference.isDisabledByAdmin()).isFalse(); + assertThat(preference.getSummary()).isEqualTo(""); + } else if (testPkg2.equals(entry.info.packageName)) { + assertThat(preference.isDisabledByAdmin()).isTrue(); + assertThat(preference.getSummary()).isEqualTo( + context.getString(R.string.disabled_by_admin)); + } + assertThat(preference.isChecked()).isFalse(); + preference.performClick(); + // Verify that when the preference is clicked, support details intent is launched + // if the preference is disabled by admin, otherwise the switch is toggled. + if (testPkg1.equals(entry.info.packageName)) { + assertThat(preference.isChecked()).isTrue(); + assertThat(ShadowRestrictedLockUtils.hasAdminSupportDetailsIntentLaunched()) + .isFalse(); + } else if (testPkg2.equals(entry.info.packageName)) { + assertThat(preference.isChecked()).isFalse(); + assertThat(ShadowRestrictedLockUtils.hasAdminSupportDetailsIntentLaunched()) + .isTrue(); + } + ShadowRestrictedLockUtils.clearAdminSupportDetailsIntentLaunch(); + return null; + }).when(mPreferenceScreen).addPreference(any(AccessPreference.class)); + mFragment.onRebuildComplete(createAppEntries(testPkg1, testPkg2)); + } + + private ArrayList createAppEntries(String... packageNames) { + final ArrayList appEntries = new ArrayList<>(); + for (int i = 0; i < packageNames.length; ++i) { + final ApplicationInfo info = new ApplicationInfo(); + info.packageName = packageNames[i]; + info.uid = Process.FIRST_APPLICATION_UID + i; + info.sourceDir = info.packageName; + final AppEntry appEntry = spy(new AppEntry(RuntimeEnvironment.application, + info, i)); + appEntry.extraInfo = new DataUsageState(false, false); + doNothing().when(appEntry).ensureLabel(any(Context.class)); + ReflectionHelpers.setField(appEntry, "info", info); + appEntries.add(appEntry); + } + return appEntries; + } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowRestrictedLockUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowRestrictedLockUtils.java new file mode 100644 index 00000000000..afede1a200e --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowRestrictedLockUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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.testutils.shadow; + +import android.content.Context; + +import com.android.internal.util.ArrayUtils; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(RestrictedLockUtils.class) +public class ShadowRestrictedLockUtils { + private static boolean isRestricted; + private static String[] restrictedPkgs; + private static boolean adminSupportDetailsIntentLaunched; + + @Implementation + public static RestrictedLockUtils.EnforcedAdmin checkIfMeteredDataRestricted(Context context, + String packageName, int userId) { + if (isRestricted) { + return new EnforcedAdmin(); + } + if (ArrayUtils.contains(restrictedPkgs, packageName)) { + return new EnforcedAdmin(); + } + return null; + } + + @Implementation + public static void sendShowAdminSupportDetailsIntent(Context context, EnforcedAdmin admin) { + adminSupportDetailsIntentLaunched = true; + } + + public static boolean hasAdminSupportDetailsIntentLaunched() { + return adminSupportDetailsIntentLaunched; + } + + public static void clearAdminSupportDetailsIntentLaunch() { + adminSupportDetailsIntentLaunched = false; + } + + public static void setRestricted(boolean restricted) { + isRestricted = restricted; + } + + public static void setRestrictedPkgs(String... pkgs) { + restrictedPkgs = pkgs; + } +}