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; + } +}