diff options
| author | 2022-03-28 16:51:15 +0000 | |
|---|---|---|
| committer | 2022-03-28 16:51:15 +0000 | |
| commit | aa69987467d15b8db6ecfb86ee2cf30d5ce58c8d (patch) | |
| tree | dc876c385492262425eb9fc6165fd0c7c9651825 | |
| parent | bc16160052c93a78e380cee85871f98bc199e0f9 (diff) | |
| parent | 1bced7f98a92b0d8ea885de427c1d639061e3580 (diff) | |
Merge "Home Controls: Add settings dialog" into tm-dev
5 files changed, 191 insertions, 44 deletions
diff --git a/packages/SystemUI/res/drawable/ic_warning.xml b/packages/SystemUI/res/drawable/ic_warning.xml index fbed779ec70f..9f90db2104ea 100644 --- a/packages/SystemUI/res/drawable/ic_warning.xml +++ b/packages/SystemUI/res/drawable/ic_warning.xml @@ -16,4 +16,4 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> <path android:fillColor="@android:color/white" android:pathData="M12,12.5zM1,21L12,2l11,19zM11,15h2v-5h-2zM12,18q0.425,0 0.713,-0.288Q13,17.425 13,17t-0.287,-0.712Q12.425,16 12,16t-0.713,0.288Q11,16.575 11,17t0.287,0.712Q11.575,18 12,18zM4.45,19h15.1L12,6z"/> -</vector> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 487426cb7f38..e00c7bd398be 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2105,6 +2105,19 @@ <!-- Controls tile secondary label when device is locked and user does not want access to controls from lockscreen [CHAR LIMIT=20] --> <string name="controls_tile_locked">Device locked</string> + <!-- Title of the dialog to show and control devices from lock screen [CHAR LIMIT=NONE] --> + <string name="controls_settings_show_controls_dialog_title">Show and control devices from lock screen?</string> + <!-- Message of the dialog to show and control devices from lock screen [CHAR LIMIT=NONE] --> + <string name="controls_settings_show_controls_dialog_message">You can add controls for your external devices to the lock screen.\n\nYour device app may allow you to control some devices without unlocking your phone or tablet.\n\nYou can make changes any time in Settings.</string> + <!-- Title of the dialog to control certain devices from lock screen without auth [CHAR LIMIT=NONE] --> + <string name="controls_settings_trivial_controls_dialog_title">Control devices from lock screen?</string> + <!-- Message of the dialog to control certain devices from lock screen without auth [CHAR LIMIT=NONE] --> + <string name="controls_settings_trivial_controls_dialog_message">You can control some devices without unlocking your phone or tablet.\n\nYour device app determines which devices can be controlled in this way.</string> + <!-- Neutral button title of the controls dialog [CHAR LIMIT=NONE] --> + <string name="controls_settings_dialog_neutral_button">No thanks</string> + <!-- Positive button title of the controls dialog [CHAR LIMIT=NONE] --> + <string name="controls_settings_dialog_positive_button">Yes</string> + <!-- Controls PIN entry dialog, switch to alphanumeric keyboard [CHAR LIMIT=100] --> <string name="controls_pin_use_alphanumeric">PIN contains letters or symbols</string> <!-- Controls PIN entry dialog, title [CHAR LIMIT=30] --> diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index e53f2673841c..73faa3459c7b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -18,6 +18,7 @@ package com.android.systemui.controls.ui import android.annotation.AnyThread import android.annotation.MainThread +import android.app.AlertDialog import android.app.Dialog import android.app.PendingIntent import android.content.Context @@ -27,7 +28,7 @@ import android.database.ContentObserver import android.net.Uri import android.os.Handler import android.os.VibrationEffect -import android.provider.Settings +import android.provider.Settings.Secure import android.service.controls.Control import android.service.controls.actions.BooleanAction import android.service.controls.actions.CommandAction @@ -35,12 +36,17 @@ import android.service.controls.actions.FloatAction import android.util.Log import android.view.HapticFeedbackConstants import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_FILE +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.settings.SecureSettings @@ -60,6 +66,7 @@ class ControlActionCoordinatorImpl @Inject constructor( private val controlsMetricsLogger: ControlsMetricsLogger, private val vibrator: VibratorHelper, private val secureSettings: SecureSettings, + private val userContextProvider: UserContextProvider, @Main mainHandler: Handler ) : ControlActionCoordinator { private var dialog: Dialog? = null @@ -68,22 +75,33 @@ class ControlActionCoordinatorImpl @Inject constructor( private val isLocked: Boolean get() = !keyguardStateController.isUnlocked() private var mAllowTrivialControls: Boolean = secureSettings.getInt( - Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0) != 0 + Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0) != 0 + private var mShowDeviceControlsInLockscreen: Boolean = secureSettings.getInt( + Secure.LOCKSCREEN_SHOW_CONTROLS, 0) != 0 override lateinit var activityContext: Context companion object { private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L + private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 } init { val lockScreenShowControlsUri = - secureSettings.getUriFor(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) + secureSettings.getUriFor(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) + val showControlsUri = + secureSettings.getUriFor(Secure.LOCKSCREEN_SHOW_CONTROLS) val controlsContentObserver = object : ContentObserver(mainHandler) { override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) - if (uri == lockScreenShowControlsUri) { - mAllowTrivialControls = secureSettings.getInt( - Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0) != 0 + when (uri) { + lockScreenShowControlsUri -> { + mAllowTrivialControls = secureSettings.getInt( + Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0) != 0 + } + showControlsUri -> { + mShowDeviceControlsInLockscreen = secureSettings + .getInt(Secure.LOCKSCREEN_SHOW_CONTROLS, 0) != 0 + } } } } @@ -91,6 +109,10 @@ class ControlActionCoordinatorImpl @Inject constructor( lockScreenShowControlsUri, false /* notifyForDescendants */, controlsContentObserver ) + secureSettings.registerContentObserver( + showControlsUri, + false /* notifyForDescendants */, controlsContentObserver + ) } override fun closeDialogs() { @@ -107,9 +129,9 @@ class ControlActionCoordinatorImpl @Inject constructor( cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) cvh.action(BooleanAction(templateId, !isChecked)) }, - true /* blockable */ - ), - isAuthRequired(cvh, mAllowTrivialControls) + true /* blockable */, + cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */ + ) ) } @@ -127,9 +149,9 @@ class ControlActionCoordinatorImpl @Inject constructor( cvh.action(CommandAction(templateId)) } }, - blockable - ), - isAuthRequired(cvh, mAllowTrivialControls) + blockable /* blockable */, + cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */ + ) ) } @@ -147,9 +169,9 @@ class ControlActionCoordinatorImpl @Inject constructor( createAction( cvh.cws.ci.controlId, { cvh.action(FloatAction(templateId, newValue)) }, - false /* blockable */ - ), - isAuthRequired(cvh, mAllowTrivialControls) + false /* blockable */, + cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */ + ) ) } @@ -166,15 +188,16 @@ class ControlActionCoordinatorImpl @Inject constructor( showDetail(cvh, it.getAppIntent()) } }, - false /* blockable */ - ), - isAuthRequired(cvh, mAllowTrivialControls) + false /* blockable */, + cvh.cws.control?.isAuthRequired ?: true /* authIsRequired */ + ) ) } override fun runPendingAction(controlId: String) { if (isLocked) return if (pendingAction?.controlId == controlId) { + showSettingsDialogIfNeeded(pendingAction!!) pendingAction?.invoke() pendingAction = null } @@ -185,12 +208,6 @@ class ControlActionCoordinatorImpl @Inject constructor( actionsInProgress.remove(controlId) } - @VisibleForTesting() - fun isAuthRequired(cvh: ControlViewHolder, allowTrivialControls: Boolean): Boolean { - val isAuthRequired = cvh.cws.control?.isAuthRequired ?: true - return isAuthRequired || !allowTrivialControls - } - private fun shouldRunAction(controlId: String) = if (actionsInProgress.add(controlId)) { uiExecutor.executeDelayed({ @@ -203,7 +220,9 @@ class ControlActionCoordinatorImpl @Inject constructor( @AnyThread @VisibleForTesting - fun bouncerOrRun(action: Action, authRequired: Boolean) { + fun bouncerOrRun(action: Action) { + val authRequired = action.authIsRequired || !mAllowTrivialControls + if (keyguardStateController.isShowing() && authRequired) { if (isLocked) { broadcastSender.closeSystemDialogs() @@ -217,6 +236,7 @@ class ControlActionCoordinatorImpl @Inject constructor( true }, { pendingAction = null }, true /* afterKeyguardGone */) } else { + showSettingsDialogIfNeeded(action) action.invoke() } } @@ -251,11 +271,88 @@ class ControlActionCoordinatorImpl @Inject constructor( } } + private fun showSettingsDialogIfNeeded(action: Action) { + if (action.authIsRequired) { + return + } + val prefs = userContextProvider.userContext.getSharedPreferences( + PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) + val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) + if (attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || + (mShowDeviceControlsInLockscreen && mAllowTrivialControls)) { + return + } + val builder = AlertDialog + .Builder(activityContext, R.style.Theme_SystemUI_Dialog) + .setIcon(R.drawable.ic_warning) + .setOnCancelListener { + if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1) + .commit() + } + true + } + .setNeutralButton(R.string.controls_settings_dialog_neutral_button) { _, _ -> + if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, + MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + .commit() + } + true + } + + if (mShowDeviceControlsInLockscreen) { + dialog = builder + .setTitle(R.string.controls_settings_trivial_controls_dialog_title) + .setMessage(R.string.controls_settings_trivial_controls_dialog_message) + .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ -> + if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, + MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + .commit() + } + secureSettings.putInt(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 1) + true + } + .create() + } else { + dialog = builder + .setTitle(R.string.controls_settings_show_controls_dialog_title) + .setMessage(R.string.controls_settings_show_controls_dialog_message) + .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ -> + if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, + MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + .commit() + } + secureSettings.putInt(Secure.LOCKSCREEN_SHOW_CONTROLS, 1) + secureSettings.putInt(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 1) + true + } + .create() + } + + SystemUIDialog.registerDismissListener(dialog) + SystemUIDialog.setDialogSize(dialog) + + dialog?.create() + dialog?.show() + } + @VisibleForTesting - fun createAction(controlId: String, f: () -> Unit, blockable: Boolean) = - Action(controlId, f, blockable) + fun createAction( + controlId: String, + f: () -> Unit, + blockable: Boolean, + authIsRequired: Boolean + ) = Action(controlId, f, blockable, authIsRequired) - inner class Action(val controlId: String, val f: () -> Unit, val blockable: Boolean) { + inner class Action( + val controlId: String, + val f: () -> Unit, + val blockable: Boolean, + val authIsRequired: Boolean + ) { fun invoke() { if (!blockable || shouldRunAction(controlId)) { f.invoke() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index 3b272da837d9..bc2ae64dd946 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -68,6 +68,7 @@ public class DeviceControlsControllerImpl @Inject constructor( internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted" internal const val PREFS_CONTROLS_FILE = "controls_prefs" + internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" private const val SEEDING_MAX = 2 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 49eaf8239ca0..bbae5dca10c1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.controls.ui +import android.content.Context +import android.content.SharedPreferences import android.database.ContentObserver import android.net.Uri import android.os.Handler @@ -26,13 +28,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.TaskViewFactory -import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -42,6 +45,7 @@ import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.spy @@ -74,6 +78,8 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { private lateinit var secureSettings: SecureSettings @Mock private lateinit var mainHandler: Handler + @Mock + private lateinit var userContextProvider: UserContextProvider companion object { fun <T> any(): T = Mockito.any<T>() @@ -91,6 +97,8 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { `when`(secureSettings.getUriFor(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS)) .thenReturn(Settings.Secure .getUriFor(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS)) + `when`(secureSettings.getInt(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0)) + .thenReturn(1) coordinator = spy(ControlActionCoordinatorImpl( mContext, @@ -103,15 +111,27 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { metricsLogger, vibratorHelper, secureSettings, - mainHandler)) + userContextProvider, + mainHandler + )) + + val userContext = mock(Context::class.java) + val pref = mock(SharedPreferences::class.java) + `when`(userContextProvider.userContext).thenReturn(userContext) + `when`(userContext.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE)) + .thenReturn(pref) + // Just return 2 so we don't test any Dialog logic which requires a launched activity. + `when`(pref.getInt(DeviceControlsControllerImpl.PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .thenReturn(2) verify(secureSettings).registerContentObserver(any(Uri::class.java), anyBoolean(), any(ContentObserver::class.java)) `when`(cvh.cws.ci.controlId).thenReturn(ID) `when`(cvh.cws.control?.isAuthRequired()).thenReturn(true) - action = spy(coordinator.Action(ID, {}, false)) - doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean()) + action = spy(coordinator.Action(ID, {}, false, true)) + doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) } @Test @@ -119,7 +139,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { `when`(keyguardStateController.isShowing()).thenReturn(false) coordinator.toggle(cvh, "", true) - verify(coordinator).bouncerOrRun(action, true /*authRequired */) + verify(coordinator).bouncerOrRun(action) verify(action).invoke() } @@ -129,7 +149,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { `when`(keyguardStateController.isUnlocked()).thenReturn(false) coordinator.toggle(cvh, "", true) - verify(coordinator).bouncerOrRun(action, true /*authRequired */) + verify(coordinator).bouncerOrRun(action) verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean()) verify(action, never()).invoke() @@ -146,25 +166,41 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { @Test fun testToggleRunsWhenLockedAndAuthNotRequired() { + action = spy(coordinator.Action(ID, {}, false, false)) + doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) + `when`(keyguardStateController.isShowing()).thenReturn(true) `when`(keyguardStateController.isUnlocked()).thenReturn(false) - doReturn(false).`when`(coordinator).isAuthRequired( - any(), anyBoolean()) coordinator.toggle(cvh, "", true) - verify(coordinator).bouncerOrRun(action, false /* authRequired */) + verify(coordinator).bouncerOrRun(action) verify(action).invoke() } @Test - fun testIsAuthRequired() { - `when`(cvh.cws.control?.isAuthRequired).thenReturn(true) - assertThat(coordinator.isAuthRequired(cvh, false)).isTrue() + fun testToggleDoesNotRunsWhenLockedAndAuthRequired() { + action = spy(coordinator.Action(ID, {}, false, true)) + doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) + + `when`(keyguardStateController.isShowing()).thenReturn(true) + `when`(keyguardStateController.isUnlocked()).thenReturn(false) + + coordinator.toggle(cvh, "", true) + + verify(coordinator).bouncerOrRun(action) + verify(action, never()).invoke() + } + + @Test + fun testNullControl() { + `when`(cvh.cws.control).thenReturn(null) - `when`(cvh.cws.control?.isAuthRequired).thenReturn(false) - assertThat(coordinator.isAuthRequired(cvh, false)).isTrue() + `when`(keyguardStateController.isShowing()).thenReturn(true) + + coordinator.toggle(cvh, "", true) - assertThat(coordinator.isAuthRequired(cvh, true)).isFalse() + verify(coordinator).bouncerOrRun(action) + verify(action, never()).invoke() } } |