From f61ccf3fa36ddd1ed462c89d1d9066a4b5dab635 Mon Sep 17 00:00:00 2001 From: Eran Messeri Date: Mon, 2 Apr 2018 23:10:00 +0300 Subject: [PATCH 1/5] Require work profile to be unlocked for changing notification settings in a different way Rather than check for the state of the work profile in LockScreenNotificationPreferenceController#handlePreferenceTreeClick, do so in the RestrictedListPreference#performClick. The drawback of checking the state in handlePreferenceTreeClick is that the preferences are displayed first and then the requirement to unlock/enable the work profile is displayed on top of it. This is rather poor UX, so switch to doing the check in performClick and returning early if the work profile needs to be unlocked/enabled. This is similar to Patchset 1 from ag/3805482. The main difference is that the user is returned to the settings screen both after enabling the work profile and unlocking it. Test: Manually with TestDPC Test: atest SettingsRoboTests:RestrictedListPreferenceTest Bug: 77408805 Merged-In: Id168911b082fffac193cd7c7a658ab92d6ce2c15 Change-Id: I0a3a4ec4dda78e28ee88a11d383eda49e9cf50a6 --- .../settings/RestrictedListPreference.java | 37 +++++- ...creenNotificationPreferenceController.java | 39 +----- .../RestrictedListPreferenceTest.java | 124 ++++++++++++++++++ .../testutils/shadow/ShadowUserManager.java | 11 ++ 4 files changed, 173 insertions(+), 38 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/RestrictedListPreferenceTest.java diff --git a/src/com/android/settings/RestrictedListPreference.java b/src/com/android/settings/RestrictedListPreference.java index 25d4fc98d8f..d581af6ad26 100644 --- a/src/com/android/settings/RestrictedListPreference.java +++ b/src/com/android/settings/RestrictedListPreference.java @@ -16,10 +16,16 @@ package com.android.settings; +import android.app.ActivityManager; import android.app.AlertDialog; +import android.app.KeyguardManager; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserManager; +import android.support.annotation.VisibleForTesting; import android.support.v14.preference.ListPreferenceDialogFragment; import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; @@ -32,6 +38,7 @@ import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; +import com.android.settings.Utils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedPreferenceHelper; @@ -43,6 +50,8 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; public class RestrictedListPreference extends CustomListPreference { private final RestrictedPreferenceHelper mHelper; private final List mRestrictedItems = new ArrayList<>(); + private boolean mRequiresActiveUnlockedProfile = false; + private int mProfileUserId; public RestrictedListPreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -68,6 +77,24 @@ public class RestrictedListPreference extends CustomListPreference { @Override public void performClick() { + if (mRequiresActiveUnlockedProfile) { + // Check if the profile is started, first. + if (Utils.startQuietModeDialogIfNecessary(getContext(), UserManager.get(getContext()), + mProfileUserId)) { + return; + } + + // Next, check if the profile is unlocked. + KeyguardManager manager = + (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE); + if (manager.isDeviceLocked(mProfileUserId)) { + Intent intent = manager.createConfirmDeviceCredentialIntent( + null, null, mProfileUserId); + getContext().startActivity(intent); + return; + } + } + if (!mHelper.performClick()) { super.performClick(); } @@ -92,6 +119,14 @@ public class RestrictedListPreference extends CustomListPreference { return mHelper.isDisabledByAdmin(); } + public void setRequiresActiveUnlockedProfile(boolean reqState) { + mRequiresActiveUnlockedProfile = reqState; + } + + public void setProfileUserId(int profileUserId) { + mProfileUserId = profileUserId; + } + public boolean isRestrictedForEntry(CharSequence entry) { if (entry == null) { return false; @@ -263,4 +298,4 @@ public class RestrictedListPreference extends CustomListPreference { this.enforcedAdmin = enforcedAdmin; } } -} \ No newline at end of file +} diff --git a/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java b/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java index 2dfe8f35ab6..3b4f00eebb5 100644 --- a/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java +++ b/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java @@ -19,16 +19,12 @@ package com.android.settings.notification; import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS; import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS; -import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; -import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; -import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -101,6 +97,8 @@ public class LockScreenNotificationPreferenceController extends AbstractPreferen } if (mProfileUserId != UserHandle.USER_NULL) { mLockscreenProfile = (RestrictedListPreference) screen.findPreference(mWorkSettingKey); + mLockscreenProfile.setRequiresActiveUnlockedProfile(true); + mLockscreenProfile.setProfileUserId(mProfileUserId); } else { setVisible(screen, mWorkSettingKey, false /* visible */); setVisible(screen, mWorkSettingCategoryKey, false /* visible */); @@ -244,39 +242,6 @@ public class LockScreenNotificationPreferenceController extends AbstractPreferen return false; } - @Override - public boolean handlePreferenceTreeClick(Preference preference) { - final String key = preference.getKey(); - if (!TextUtils.equals(mWorkSettingKey, key)) { - return false; - } - - // Check if the profile is started, first. - if (Utils.startQuietModeDialogIfNecessary(mContext, UserManager.get(mContext), - mProfileUserId)) { - return true; - } - - // Next, check if the profile is unlocked. - KeyguardManager manager = - (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); - if (manager.isDeviceLocked(mProfileUserId)) { - //TODO: Figure out how to return the user to the current activity so they - //don't have to navigate to the settings again. - Intent intent = manager.createConfirmDeviceCredentialIntent( - null, null, mProfileUserId); - try { - ActivityManager.getService().startConfirmDeviceCredentialIntent(intent, - null /*options*/); - } catch (RemoteException ignored) { - } - - return true; - } - - return false; - } - private void setRestrictedIfNotificationFeaturesDisabled(CharSequence entry, CharSequence entryValue, int keyguardNotificationFeatures) { RestrictedLockUtils.EnforcedAdmin admin = diff --git a/tests/robotests/src/com/android/settings/RestrictedListPreferenceTest.java b/tests/robotests/src/com/android/settings/RestrictedListPreferenceTest.java new file mode 100644 index 00000000000..1cca8428761 --- /dev/null +++ b/tests/robotests/src/com/android/settings/RestrictedListPreferenceTest.java @@ -0,0 +1,124 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.RuntimeEnvironment.application; +import static org.robolectric.Shadows.shadowOf; + +import android.app.KeyguardManager; +import android.content.Intent; +import android.os.Bundle; +import android.util.AttributeSet; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowUserManager; +import com.android.settingslib.RestrictedPreferenceHelper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Shadows; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowKeyguardManager; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config( + shadows = { + ShadowUserManager.class, + ShadowKeyguardManager.class, + }) +public class RestrictedListPreferenceTest { + private static final int PROFILE_USER_ID = 11; + // From UnlaunchableAppActivity + private static final int UNLAUNCHABLE_REASON_QUIET_MODE = 1; + private static final String EXTRA_UNLAUNCHABLE_REASON = "unlaunchable_reason"; + + private ShadowUserManager mShadowUserManager; + private ShadowKeyguardManager mShadowKeyguardManager; + private RestrictedListPreference mPreference; + private RestrictedPreferenceHelper mMockHelper; + + @Before + public void setUp() { + mShadowKeyguardManager = + Shadows.shadowOf(application.getSystemService(KeyguardManager.class)); + mMockHelper = mock(RestrictedPreferenceHelper.class); + mShadowUserManager = ShadowUserManager.getShadow(); + mPreference = new RestrictedListPreference(application, mock(AttributeSet.class)); + mPreference.setProfileUserId(PROFILE_USER_ID); + ReflectionHelpers.setField(mPreference, "mHelper", mMockHelper); + } + + @Test + public void performClick_profileLocked() { + mPreference.setRequiresActiveUnlockedProfile(true); + mShadowUserManager.setQuietModeEnabled(false); + mShadowKeyguardManager.setIsDeviceLocked(PROFILE_USER_ID, true); + // Device has to be marked as secure so the real KeyguardManager will create a non-null + // intent. + mShadowKeyguardManager.setIsDeviceSecure(PROFILE_USER_ID, true); + mPreference.performClick(); + // Make sure that the performClick method on the helper is never reached. + verify(mMockHelper, never()).performClick(); + // Assert that a CONFIRM_DEVICE_CREDENTIAL intent has been started. + Intent started = shadowOf(application).getNextStartedActivity(); + assertThat(started.getExtras().getInt(Intent.EXTRA_USER_ID)).isEqualTo(PROFILE_USER_ID); + assertThat(started.getAction()) + .isEqualTo(KeyguardManager.ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER); + } + + @Test + public void performClick_profileDisabled() { + mPreference.setRequiresActiveUnlockedProfile(true); + mShadowUserManager.setQuietModeEnabled(true); + mShadowKeyguardManager.setIsDeviceLocked(PROFILE_USER_ID, false); + mPreference.performClick(); + // Make sure that the performClick method on the helper is never reached. + verify(mMockHelper, never()).performClick(); + // Assert that a new intent for enabling the work profile is started. + Intent started = shadowOf(application).getNextStartedActivity(); + Bundle extras = started.getExtras(); + int reason = extras.getInt(EXTRA_UNLAUNCHABLE_REASON); + assertThat(reason).isEqualTo(UNLAUNCHABLE_REASON_QUIET_MODE); + } + + @Test + public void performClick_profileAvailable() { + // Verify that the helper's perfomClick method is called if the profile is + // available and unlocked. + mPreference.setRequiresActiveUnlockedProfile(true); + mShadowUserManager.setQuietModeEnabled(false); + mShadowKeyguardManager.setIsDeviceLocked(PROFILE_USER_ID, false); + when(mMockHelper.performClick()).thenReturn(true); + mPreference.performClick(); + verify(mMockHelper).performClick(); + } + + @Test + public void performClick_profileLockedAndUnlockedProfileNotRequired() { + // Verify that even if the profile is disabled, if the Preference class does not + // require it than the regular flow takes place. + mPreference.setRequiresActiveUnlockedProfile(false); + mShadowUserManager.setQuietModeEnabled(true); + when(mMockHelper.performClick()).thenReturn(true); + mPreference.performClick(); + verify(mMockHelper).performClick(); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java index f7fd12f58fe..d83c8148fff 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java @@ -44,6 +44,7 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager private final Map> mRestrictionSources = new HashMap<>(); private final List mUserProfileInfos = new ArrayList<>(); private final Set mManagedProfiles = new HashSet<>(); + private boolean mIsQuietModeEnabled = false; @Resetter public void reset() { @@ -52,6 +53,7 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager mUserProfileInfos.clear(); mRestrictionSources.clear(); mManagedProfiles.clear(); + mIsQuietModeEnabled = false; } public void setUserInfo(int userHandle, UserInfo userInfo) { @@ -110,4 +112,13 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager public void addManagedProfile(int userId) { mManagedProfiles.add(userId); } + + @Implementation + public boolean isQuietModeEnabled(UserHandle userHandle) { + return mIsQuietModeEnabled; + } + + public void setQuietModeEnabled(boolean enabled) { + mIsQuietModeEnabled = enabled; + } } From 8dcd550d36a701c2d19b3f78ae231e5afcf1da0c Mon Sep 17 00:00:00 2001 From: hughchen Date: Tue, 24 Apr 2018 15:46:30 +0800 Subject: [PATCH 2/5] Remove the gear icon and change tapping event * Remove the gear icon in "currently connected" section. * Change the tapping event in "currently connected" section. When tapping device in this section, take user to device detail page. Bug: 78490845 Test: make -j40 RunSettingsRoboTests Change-Id: I25f8455def3c38e24dea9af9e9e29ba37c250f67 Merged-In: I25f8455def3c38e24dea9af9e9e29ba37c250f67 --- .../bluetooth/BluetoothDevicePreference.java | 8 +++- .../bluetooth/BluetoothDeviceUpdater.java | 43 +++++++++++-------- .../ConnectedBluetoothDeviceUpdater.java | 19 ++++++++ .../ConnectedBluetoothDeviceUpdaterTest.java | 12 ++++++ 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 3fd7ced3e03..2cc623331e2 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -56,6 +56,7 @@ public final class BluetoothDevicePreference extends GearPreference implements private AlertDialog mDisconnectDialog; private String contentDescription = null; + private boolean mHideSecondTarget = false; /* Talk-back descriptions for various BT icons */ Resources mResources; @@ -86,7 +87,8 @@ public final class BluetoothDevicePreference extends GearPreference implements protected boolean shouldHideSecondTarget() { return mCachedDevice == null || mCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED - || mUserManager.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH); + || mUserManager.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH) + || mHideSecondTarget; } @Override @@ -112,6 +114,10 @@ public final class BluetoothDevicePreference extends GearPreference implements return mCachedDevice; } + public void hideSecondTarget(boolean hideSecondTarget) { + mHideSecondTarget = hideSecondTarget; + } + public void onDeviceAttributesChanged() { /* * The preference framework takes care of making sure the value has diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java index 595d9518944..dfcbfca1e06 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java @@ -56,29 +56,14 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, protected final DevicePreferenceCallback mDevicePreferenceCallback; protected final Map mPreferenceMap; protected Context mPrefContext; + protected DashboardFragment mFragment; private final boolean mShowDeviceWithoutNames; - private DashboardFragment mFragment; private Preference.OnPreferenceClickListener mDevicePreferenceClickListener = null; @VisibleForTesting final GearPreference.OnGearClickListener mDeviceProfilesListener = pref -> { - final CachedBluetoothDevice device = - ((BluetoothDevicePreference) pref).getBluetoothDevice(); - if (device == null) { - return; - } - final Bundle args = new Bundle(); - args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, - device.getDevice().getAddress()); - - new SubSettingLauncher(mFragment.getContext()) - .setDestination(BluetoothDeviceDetailsFragment.class.getName()) - .setArguments(args) - .setTitle(R.string.device_details_title) - .setSourceMetricsCategory(mFragment.getMetricsCategory()) - .launch(); - + launchDeviceDetails(pref); }; private class PreferenceClickListener implements @@ -201,7 +186,7 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, public abstract boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice); /** - * Update whether to show {@cde cachedBluetoothDevice} in the list. + * Update whether to show {@link CachedBluetoothDevice} in the list. */ protected void update(CachedBluetoothDevice cachedBluetoothDevice) { if (isFilterMatched(cachedBluetoothDevice)) { @@ -239,6 +224,28 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, } } + /** + * Get {@link CachedBluetoothDevice} from {@link Preference} and it is used to init + * {@link SubSettingLauncher} to launch {@link BluetoothDeviceDetailsFragment} + */ + protected void launchDeviceDetails(Preference preference) { + final CachedBluetoothDevice device = + ((BluetoothDevicePreference) preference).getBluetoothDevice(); + if (device == null) { + return; + } + final Bundle args = new Bundle(); + args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, + device.getDevice().getAddress()); + + new SubSettingLauncher(mFragment.getContext()) + .setDestination(BluetoothDeviceDetailsFragment.class.getName()) + .setArguments(args) + .setTitle(R.string.device_details_title) + .setSourceMetricsCategory(mFragment.getMetricsCategory()) + .launch(); + } + /** * @return {@code true} if {@code cachedBluetoothDevice} is connected * and the bond state is bonded. diff --git a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java index 55f4bb1dd6b..11702bcefc2 100644 --- a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java @@ -16,11 +16,14 @@ package com.android.settings.bluetooth; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.media.AudioManager; import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.Preference; import android.util.Log; + import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -116,4 +119,20 @@ public class ConnectedBluetoothDeviceUpdater extends BluetoothDeviceUpdater { } return isFilterMatched; } + + @Override + protected void addPreference(CachedBluetoothDevice cachedDevice) { + super.addPreference(cachedDevice); + final BluetoothDevice device = cachedDevice.getDevice(); + if (mPreferenceMap.containsKey(device)) { + final BluetoothDevicePreference btPreference = + (BluetoothDevicePreference) mPreferenceMap.get(device); + btPreference.setOnGearClickListener(null); + btPreference.hideSecondTarget(true); + btPreference.setOnPreferenceClickListener((Preference p) -> { + launchDeviceDetails(p); + return true; + }); + } + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java index 9d69f59b7d3..56e638a9ed1 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java @@ -15,6 +15,7 @@ */ package com.android.settings.bluetooth; +import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -207,4 +208,15 @@ public class ConnectedBluetoothDeviceUpdaterTest { verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); } + + @Test + public void addPreference_addPreference_shouldHideSecondTarget() { + BluetoothDevicePreference btPreference = + new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true); + mBluetoothDeviceUpdater.mPreferenceMap.put(mBluetoothDevice, btPreference); + + mBluetoothDeviceUpdater.addPreference(mCachedBluetoothDevice); + + assertThat(btPreference.shouldHideSecondTarget()).isTrue(); + } } From 16bafbbc74be6d737c936b0cd531bbb926112c22 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 1 May 2018 16:57:50 -0400 Subject: [PATCH 3/5] DO NOT MERGE Update onboarding Change text and add 'repeat callers' Test: robotests Change-Id: I4b7125a2a9d70cf5ec7e4d184528a444d7d2bcf1 Fixes: 78448988 --- res/values/strings.xml | 4 ++-- .../notification/ZenOnboardingActivity.java | 13 ++++++------- .../notification/ZenOnboardingActivityTest.java | 4 +++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index bd7a47cee67..28973d76281 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7365,9 +7365,9 @@ Don\'t update - Your phone mutes sounds & vibrations\n\nUpdate settings to also:\n\n- Hide notifications\n\n- Only show calls from starred contacts + Your phone can do more to help you focus.\n\nUpdate settings to:\n\n- Hide notifications completely\n\n- Allow calls from starred contacts and repeat callers - Update your Do Not Disturb settings? + Update Do Not Disturb? Settings diff --git a/src/com/android/settings/notification/ZenOnboardingActivity.java b/src/com/android/settings/notification/ZenOnboardingActivity.java index baca8110ff3..9d71f54c712 100644 --- a/src/com/android/settings/notification/ZenOnboardingActivity.java +++ b/src/com/android/settings/notification/ZenOnboardingActivity.java @@ -18,12 +18,10 @@ package com.android.settings.notification; import android.app.Activity; import android.app.NotificationManager; -import android.content.Intent; +import android.app.NotificationManager.Policy; import android.os.Bundle; -import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.view.View; -import android.widget.CheckBox; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -67,12 +65,13 @@ public class ZenOnboardingActivity extends Activity { public void save(View button) { mMetrics.action(MetricsEvent.ACTION_ZEN_ONBOARDING_OK); - NotificationManager.Policy policy = mNm.getNotificationPolicy(); + Policy policy = mNm.getNotificationPolicy(); - NotificationManager.Policy newPolicy = new NotificationManager.Policy( - policy.priorityCategories, NotificationManager.Policy.PRIORITY_SENDERS_STARRED, + Policy newPolicy = new NotificationManager.Policy( + Policy.PRIORITY_CATEGORY_REPEAT_CALLERS | policy.priorityCategories, + Policy.PRIORITY_SENDERS_STARRED, policy.priorityMessageSenders, - NotificationManager.Policy.getAllSuppressedVisualEffects()); + Policy.getAllSuppressedVisualEffects()); mNm.setNotificationPolicy(newPolicy); finishAndRemoveTask(); diff --git a/tests/robotests/src/com/android/settings/notification/ZenOnboardingActivityTest.java b/tests/robotests/src/com/android/settings/notification/ZenOnboardingActivityTest.java index 57dc855a780..7bc93713977 100644 --- a/tests/robotests/src/com/android/settings/notification/ZenOnboardingActivityTest.java +++ b/tests/robotests/src/com/android/settings/notification/ZenOnboardingActivityTest.java @@ -17,6 +17,7 @@ package com.android.settings.notification; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS; +import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS; import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; @@ -94,7 +95,8 @@ public class ZenOnboardingActivityTest { verify(mNm).setNotificationPolicy(captor.capture()); Policy actual = captor.getValue(); - assertThat(actual.priorityCategories).isEqualTo(PRIORITY_CATEGORY_ALARMS); + assertThat(actual.priorityCategories).isEqualTo(PRIORITY_CATEGORY_ALARMS + | PRIORITY_CATEGORY_REPEAT_CALLERS); assertThat(actual.priorityCallSenders).isEqualTo(Policy.PRIORITY_SENDERS_STARRED); assertThat(actual.priorityMessageSenders).isEqualTo(Policy.PRIORITY_SENDERS_ANY); assertThat(actual.suppressedVisualEffects).isEqualTo( From 01816f4cbbf61f8a1dfa2d04f448008f8836ffe9 Mon Sep 17 00:00:00 2001 From: yuemingw Date: Thu, 19 Apr 2018 12:12:11 +0100 Subject: [PATCH 4/5] Policy transparency dialog should be shown when SwitchBar is restricted. After ag/3818911, touch event of Switchbar is delegated to the switch. When the SwithBar is disabled by admin, switch is GONE and restricted icon is VISIBLE instead. We should let touch event be delegaed to restricted icon and show policy transparency dialog when it's clicked in this case. Bug: 77898233 Bug: 70206452 Test: Manually via setting disallow_share_location in TestDPC. Merged-In: Ifa4fa6ebbe7986277f5cd0951a399ea2377a39f9 Change-Id: If4a5349134e6f0e064561b4860966f950ce423b3 --- .../android/settings/widget/SwitchBar.java | 25 +++++++++++++++++-- .../settings/widget/SwitchBarTest.java | 13 ++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/widget/SwitchBar.java b/src/com/android/settings/widget/SwitchBar.java index c33f6032cac..004e89c0ed6 100644 --- a/src/com/android/settings/widget/SwitchBar.java +++ b/src/com/android/settings/widget/SwitchBar.java @@ -25,6 +25,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.ColorInt; import android.support.annotation.StringRes; +import android.support.annotation.VisibleForTesting; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; @@ -132,6 +133,17 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC (switchView, isChecked) -> setTextViewLabelAndBackground(isChecked)); mRestrictedIcon = findViewById(R.id.restricted_icon); + mRestrictedIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mDisabledByAdmin) { + mMetricsFeatureProvider.count(mContext, + mMetricsTag + "/switch_bar|restricted", 1); + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, + mEnforcedAdmin); + } + } + }); // Default is hide setVisibility(View.GONE); @@ -196,6 +208,11 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC mSwitch.setEnabled(enabled); } + @VisibleForTesting + View getDelegatingView() { + return mDisabledByAdmin ? mRestrictedIcon : mSwitch; + } + /** * If admin is not null, disables the text and switch but keeps the view clickable. * Otherwise, calls setEnabled which will enables the entire view including @@ -216,6 +233,8 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC mRestrictedIcon.setVisibility(View.GONE); setEnabled(true); } + setTouchDelegate(new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), + getDelegatingView())); } public final ToggleSwitch getSwitch() { @@ -228,7 +247,8 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC mSwitch.setOnCheckedChangeListener(this); // Make the entire bar work as a switch post(() -> setTouchDelegate( - new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), mSwitch))); + new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), + getDelegatingView()))); } } @@ -242,7 +262,8 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if ((w > 0) && (h > 0)) { - setTouchDelegate(new TouchDelegate(new Rect(0, 0, w, h), mSwitch)); + setTouchDelegate(new TouchDelegate(new Rect(0, 0, w, h), + getDelegatingView())); } } diff --git a/tests/robotests/src/com/android/settings/widget/SwitchBarTest.java b/tests/robotests/src/com/android/settings/widget/SwitchBarTest.java index bd99bbbbb8f..818daecd26d 100644 --- a/tests/robotests/src/com/android/settings/widget/SwitchBarTest.java +++ b/tests/robotests/src/com/android/settings/widget/SwitchBarTest.java @@ -26,6 +26,7 @@ import android.widget.TextView; import com.android.settings.R; import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,4 +83,16 @@ public class SwitchBarTest { assertThat(((TextView) mBar.findViewById(R.id.switch_text)).getText()) .isEqualTo(mContext.getString(onText)); } + + @Test + public void disabledByAdmin_shouldDelegateToRestrictedIcon() { + mBar.setDisabledByAdmin(new EnforcedAdmin()); + assertThat(mBar.getDelegatingView().getId()).isEqualTo(R.id.restricted_icon); + } + + @Test + public void notDisabledByAdmin_shouldDelegateToSwitch() { + mBar.setDisabledByAdmin(null); + assertThat(mBar.getDelegatingView().getId()).isEqualTo(R.id.switch_widget); + } } From f249555f97cf3a5cd128774bd4338d83e455e772 Mon Sep 17 00:00:00 2001 From: Chalard Jean Date: Thu, 19 Apr 2018 18:23:49 +0900 Subject: [PATCH 5/5] Show the status of private DNS. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works as follows : Off → "Off" Opportunistic, inactive → "Automatic" Opportunistic, active → "On" (stealing a string from notifications for this) Strict, not resolved and/or not validated → "Couldn't connect" Strict, resolved and validated → Set up hostname Bug: 73641539 Test: manual, and updated tests pass Change-Id: Id1132467288d51aa9cb81a04db65dee438ddfad9 --- .../PrivateDnsPreferenceController.java | 49 +++++++++- .../PrivateDnsPreferenceControllerTest.java | 91 +++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/network/PrivateDnsPreferenceController.java b/src/com/android/settings/network/PrivateDnsPreferenceController.java index 50224caba1a..47aa4dcffe4 100644 --- a/src/com/android/settings/network/PrivateDnsPreferenceController.java +++ b/src/com/android/settings/network/PrivateDnsPreferenceController.java @@ -24,6 +24,10 @@ import android.content.Context; import android.content.ContentResolver; import android.content.res.Resources; import android.database.ContentObserver; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.LinkProperties; +import android.net.Network; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -31,6 +35,7 @@ import android.provider.Settings; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; +import com.android.internal.util.ArrayUtils; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; @@ -38,6 +43,8 @@ import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; import com.android.settingslib.core.lifecycle.LifecycleObserver; +import java.net.InetAddress; +import java.util.List; public class PrivateDnsPreferenceController extends BasePreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart, OnStop { @@ -50,12 +57,15 @@ public class PrivateDnsPreferenceController extends BasePreferenceController private final Handler mHandler; private final ContentObserver mSettingsObserver; + private final ConnectivityManager mConnectivityManager; + private LinkProperties mLatestLinkProperties; private Preference mPreference; public PrivateDnsPreferenceController(Context context) { super(context, KEY_PRIVATE_DNS_SETTINGS); mHandler = new Handler(Looper.getMainLooper()); mSettingsObserver = new PrivateDnsSettingsObserver(mHandler); + mConnectivityManager = context.getSystemService(ConnectivityManager.class); } @Override @@ -80,11 +90,17 @@ public class PrivateDnsPreferenceController extends BasePreferenceController for (Uri uri : SETTINGS_URIS) { mContext.getContentResolver().registerContentObserver(uri, false, mSettingsObserver); } + final Network defaultNetwork = mConnectivityManager.getActiveNetwork(); + if (defaultNetwork != null) { + mLatestLinkProperties = mConnectivityManager.getLinkProperties(defaultNetwork); + } + mConnectivityManager.registerDefaultNetworkCallback(mNetworkCallback, mHandler); } @Override public void onStop() { mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } @Override @@ -92,13 +108,23 @@ public class PrivateDnsPreferenceController extends BasePreferenceController final Resources res = mContext.getResources(); final ContentResolver cr = mContext.getContentResolver(); final String mode = PrivateDnsModeDialogPreference.getModeFromSettings(cr); + final LinkProperties lp = mLatestLinkProperties; + final List dnses = (lp == null) ? null : lp.getValidatedPrivateDnsServers(); + final boolean dnsesResolved = !ArrayUtils.isEmpty(dnses); switch (mode) { case PRIVATE_DNS_MODE_OFF: return res.getString(R.string.private_dns_mode_off); case PRIVATE_DNS_MODE_OPPORTUNISTIC: - return res.getString(R.string.private_dns_mode_opportunistic); + // TODO (b/79122154) : create a string specifically for this, instead of + // hijacking a string from notifications. This is necessary at this time + // because string freeze is in the past and this string has the right + // content at this moment. + return dnsesResolved ? res.getString(R.string.switch_on_text) + : res.getString(R.string.private_dns_mode_opportunistic); case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME: - return PrivateDnsModeDialogPreference.getHostnameFromSettings(cr); + return dnsesResolved + ? PrivateDnsModeDialogPreference.getHostnameFromSettings(cr) + : res.getString(R.string.private_dns_mode_provider_failure); } return ""; } @@ -111,8 +137,25 @@ public class PrivateDnsPreferenceController extends BasePreferenceController @Override public void onChange(boolean selfChange) { if (mPreference != null) { - PrivateDnsPreferenceController.this.updateState(mPreference); + updateState(mPreference); } } } + + private final NetworkCallback mNetworkCallback = new NetworkCallback() { + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + mLatestLinkProperties = lp; + if (mPreference != null) { + updateState(mPreference); + } + } + @Override + public void onLost(Network network) { + mLatestLinkProperties = null; + if (mPreference != null) { + updateState(mPreference); + } + } + }; } diff --git a/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java index 83d4bd530b7..ce40ab69364 100644 --- a/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java @@ -24,17 +24,29 @@ import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME import static android.provider.Settings.Global.PRIVATE_DNS_MODE; import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; import static org.mockito.Mockito.when; import android.arch.lifecycle.LifecycleOwner; import android.content.Context; import android.content.ContentResolver; import android.database.ContentObserver; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.LinkProperties; +import android.net.Network; +import android.os.Handler; import android.provider.Settings; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; @@ -46,22 +58,45 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowContentResolver; +import org.robolectric.shadows.ShadowServiceManager; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) public class PrivateDnsPreferenceControllerTest { private final static String HOSTNAME = "dns.example.com"; + private final static List NON_EMPTY_ADDRESS_LIST; + static { + try { + NON_EMPTY_ADDRESS_LIST = Arrays.asList( + InetAddress.getByAddress(new byte[] { 8, 8, 8, 8 })); + } catch (UnknownHostException e) { + throw new RuntimeException("Invalid hardcoded IP addresss: " + e); + } + } @Mock private PreferenceScreen mScreen; @Mock + private ConnectivityManager mConnectivityManager; + @Mock + private Network mNetwork; + @Mock private Preference mPreference; + @Captor + private ArgumentCaptor mCallbackCaptor; private PrivateDnsPreferenceController mController; private Context mContext; private ContentResolver mContentResolver; @@ -75,15 +110,41 @@ public class PrivateDnsPreferenceControllerTest { mContext = spy(RuntimeEnvironment.application); mContentResolver = mContext.getContentResolver(); mShadowContentResolver = Shadow.extract(mContentResolver); + when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) + .thenReturn(mConnectivityManager); + doNothing().when(mConnectivityManager).registerDefaultNetworkCallback( + mCallbackCaptor.capture(), nullable(Handler.class)); when(mScreen.findPreference(anyString())).thenReturn(mPreference); mController = spy(new PrivateDnsPreferenceController(mContext)); + mLifecycleOwner = () -> mLifecycle; mLifecycle = new Lifecycle(mLifecycleOwner); mLifecycle.addObserver(mController); } + private void updateLinkProperties(LinkProperties lp) { + NetworkCallback nc = mCallbackCaptor.getValue(); + // The network callback that has been captured by the captor is the `mNetworkCallback' + // member of mController. mController being a spy, it has copied that member from the + // original object it was spying on, which means the object returned by the captor + // has a reference to the original object instead of the mock as its outer instance + // and will call methods and modify members of the original object instead of the spy, + // so methods subsequently called on the spy will not be aware of the changes. To work + // around this, the following code will create a new instance of the same class with + // the same code, but it sets the spy as the outer instance. + // A more recent version of Mockito would have made possible to create the spy with + // spy(PrivateDnsPreferenceController.class, withSettings().useConstructor(mContext)) + // and that would have solved the problem by removing the original object entirely + // in a more elegant manner, but useConstructor(Object...) is only available starting + // with Mockito 2.7.14. Other solutions involve modifying the code under test for + // the sake of the test. + nc = mock(nc.getClass(), withSettings().useConstructor().outerInstance(mController) + .defaultAnswer(CALLS_REAL_METHODS)); + nc.onLinkPropertiesChanged(mNetwork, lp); + } + @Test public void goThroughLifecycle_shouldRegisterUnregisterSettingsObserver() { mLifecycle.handleLifecycleEvent(ON_START); @@ -113,20 +174,50 @@ public class PrivateDnsPreferenceControllerTest { @Test public void getSummary_PrivateDnsModeOpportunistic() { + mLifecycle.handleLifecycleEvent(ON_START); setPrivateDnsMode(PRIVATE_DNS_MODE_OPPORTUNISTIC); setPrivateDnsProviderHostname(HOSTNAME); mController.updateState(mPreference); verify(mController, atLeastOnce()).getSummary(); verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); + + LinkProperties lp = mock(LinkProperties.class); + when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST); + updateLinkProperties(lp); + mController.updateState(mPreference); + verify(mPreference).setSummary(getResourceString(R.string.switch_on_text)); + + reset(mPreference); + lp = mock(LinkProperties.class); + when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList()); + updateLinkProperties(lp); + mController.updateState(mPreference); + verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); } @Test public void getSummary_PrivateDnsModeProviderHostname() { + mLifecycle.handleLifecycleEvent(ON_START); setPrivateDnsMode(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); setPrivateDnsProviderHostname(HOSTNAME); mController.updateState(mPreference); verify(mController, atLeastOnce()).getSummary(); + verify(mPreference).setSummary( + getResourceString(R.string.private_dns_mode_provider_failure)); + + LinkProperties lp = mock(LinkProperties.class); + when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST); + updateLinkProperties(lp); + mController.updateState(mPreference); verify(mPreference).setSummary(HOSTNAME); + + reset(mPreference); + lp = mock(LinkProperties.class); + when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList()); + updateLinkProperties(lp); + mController.updateState(mPreference); + verify(mPreference).setSummary( + getResourceString(R.string.private_dns_mode_provider_failure)); } private void setPrivateDnsMode(String mode) {