Merge "Add policy transparency for metered data related settings."
diff --git a/res/xml/app_data_usage.xml b/res/xml/app_data_usage.xml
index 3e94135..a4b2159 100644
--- a/res/xml/app_data_usage.xml
+++ b/res/xml/app_data_usage.xml
@@ -16,6 +16,8 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:key="app_data_usage_screen"
android:title="@string/data_usage_app_summary_title">
<com.android.settings.datausage.SpinnerPreference
@@ -50,15 +52,19 @@
android:key="app_settings"
android:title="@string/data_usage_app_settings" />
- <SwitchPreference
+ <com.android.settingslib.RestrictedSwitchPreference
android:key="restrict_background"
android:title="@string/data_usage_app_restrict_background"
- android:summary="@string/data_usage_app_restrict_background_summary" />
+ android:summary="@string/data_usage_app_restrict_background_summary"
+ settings:useAdditionalSummary="true"
+ settings:restrictedSwitchSummary="@string/disabled_by_admin" />
- <SwitchPreference
+ <com.android.settingslib.RestrictedSwitchPreference
android:key="unrestricted_data_saver"
android:title="@string/unrestricted_app_title"
- android:summary="@string/unrestricted_app_summary" />
+ android:summary="@string/unrestricted_app_summary"
+ settings:useAdditionalSummary="true"
+ settings:restrictedSwitchSummary="@string/disabled_by_admin" />
</PreferenceCategory>
diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java
index 5470e63..a0d0ec0 100644
--- a/src/com/android/settings/datausage/AppDataUsage.java
+++ b/src/com/android/settings/datausage/AppDataUsage.java
@@ -33,7 +33,6 @@
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.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 @@
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 @@
private AppItem mAppItem;
private Intent mAppSettingsIntent;
private SpinnerPreference mCycle;
- private SwitchPreference mUnrestrictedData;
+ private RestrictedSwitchPreference mUnrestrictedData;
private DataSaverBackend mDataSaverBackend;
@Override
@@ -160,9 +162,11 @@
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 @@
}
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 @@
} 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 e8a7bbf..cff4a50 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.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 @@
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 @@
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 @@
}
}
+ @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 @@
}
});
}
- 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 @@
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 7cd09de..58643b6 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.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.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 @@
@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 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 @@
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 53cb7ed..fff879f 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 @@
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<AppEntry> createAppEntries(String... packageNames) {
+ final ArrayList<AppEntry> 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 0000000..afede1a
--- /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;
+ }
+}