Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7cd86f41 authored by Aaron Liu's avatar Aaron Liu Committed by Presubmit Automerger Backend
Browse files

[automerge] Home Controls: Add settings dialog 2p: 1bced7f9

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17332611

Bug: 226179827
Change-Id: Ic13ba9649f4b09b66aadbf48353d6584ff7f10db
parents 2352aebd 1bced7f9
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -2093,6 +2093,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] -->
+125 −28
Original line number Diff line number Diff line
@@ -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) {
                when (uri) {
                    lockScreenShowControlsUri -> {
                        mAllowTrivialControls = secureSettings.getInt(
                            Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0) != 0
                                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()
+1 −0
Original line number Diff line number Diff line
@@ -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
    }

+51 −15
Original line number Diff line number Diff line
@@ -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()
    }
}
+1 −1

File changed.

Contains only whitespace changes.