diff --git a/packages/SystemUI/res/drawable/ic_warning.xml b/packages/SystemUI/res/drawable/ic_warning.xml
index fbed779ec70f1..9f90db2104eaf 100644
--- a/packages/SystemUI/res/drawable/ic_warning.xml
+++ b/packages/SystemUI/res/drawable/ic_warning.xml
@@ -16,4 +16,4 @@
-->
-
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 487426cb7f384..e00c7bd398be5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2105,6 +2105,19 @@
Device locked
+
+ Show and control devices from lock screen?
+
+ 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.
+
+ Control devices from lock screen?
+
+ You can control some devices without unlocking your phone or tablet.\n\nYour device app determines which devices can be controlled in this way.
+
+ No thanks
+
+ Yes
+
PIN contains letters or symbols
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 e53f2673841c9..73faa3459c7b5 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(
}
}
- @VisibleForTesting
- fun createAction(controlId: String, f: () -> Unit, blockable: Boolean) =
- Action(controlId, f, blockable)
+ 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
+ }
- inner class Action(val controlId: String, val f: () -> Unit, val blockable: Boolean) {
+ 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,
+ authIsRequired: Boolean
+ ) = Action(controlId, f, blockable, authIsRequired)
+
+ 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 3b272da837d98..bc2ae64dd946d 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 49eaf8239ca0c..bbae5dca10c1e 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 any(): T = Mockito.any()
@@ -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`(cvh.cws.control?.isAuthRequired).thenReturn(false)
- assertThat(coordinator.isAuthRequired(cvh, false)).isTrue()
+ `when`(keyguardStateController.isShowing()).thenReturn(true)
+ `when`(keyguardStateController.isUnlocked()).thenReturn(false)
- assertThat(coordinator.isAuthRequired(cvh, true)).isFalse()
+ coordinator.toggle(cvh, "", true)
+
+ verify(coordinator).bouncerOrRun(action)
+ verify(action, never()).invoke()
+ }
+
+ @Test
+ fun testNullControl() {
+ `when`(cvh.cws.control).thenReturn(null)
+
+ `when`(keyguardStateController.isShowing()).thenReturn(true)
+
+ coordinator.toggle(cvh, "", true)
+
+ verify(coordinator).bouncerOrRun(action)
+ verify(action, never()).invoke()
}
}