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

Commit d832feaa authored by Matt Pietal's avatar Matt Pietal
Browse files

Controls - Prevent action while locked

After tapping on a control to change the state (lock/unlock, turn
on/off) while the device is locked, the bouncer will be shown. The
action is captured and is supposed to run on the next refresh of the
control, after the phone is unlocked. However, if a well timed refresh
is requested from the controls provider while the user is waiting on the
security bouncer, the action will be fulfilled without the device
being unlocked.

Bug: 179795856
Test: atest ControlActionCoordinatorImplTest
Change-Id: Ie100954a34f19ea6936fe29cbcb205de14f1513c
parent 351250ae
Loading
Loading
Loading
Loading
+12 −5
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.service.controls.actions.CommandAction
import android.service.controls.actions.FloatAction
import android.util.Log
import android.view.HapticFeedbackConstants
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
@@ -71,7 +72,7 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
        bouncerOrRun(Action(cvh.cws.ci.controlId, {
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
            cvh.action(BooleanAction(templateId, !isChecked))
        }, true /* blockable */))
@@ -79,7 +80,7 @@ class ControlActionCoordinatorImpl @Inject constructor(

    override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) {
        val blockable = cvh.usePanel()
        bouncerOrRun(Action(cvh.cws.ci.controlId, {
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
            if (cvh.usePanel()) {
                showDialog(cvh, control.getAppIntent().getIntent())
@@ -98,13 +99,13 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) {
        bouncerOrRun(Action(cvh.cws.ci.controlId, {
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            cvh.action(FloatAction(templateId, newValue))
        }, false /* blockable */))
    }

    override fun longPress(cvh: ControlViewHolder) {
        bouncerOrRun(Action(cvh.cws.ci.controlId, {
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            // Long press snould only be called when there is valid control state, otherwise ignore
            cvh.cws.control?.let {
                cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
@@ -114,6 +115,7 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun runPendingAction(controlId: String) {
        if (!keyguardStateController.isUnlocked()) return
        if (pendingAction?.controlId == controlId) {
            pendingAction?.invoke()
            pendingAction = null
@@ -135,7 +137,8 @@ class ControlActionCoordinatorImpl @Inject constructor(
            false
        }

    private fun bouncerOrRun(action: Action) {
    @VisibleForTesting
    fun bouncerOrRun(action: Action) {
        if (keyguardStateController.isShowing()) {
            var closeDialog = !keyguardStateController.isUnlocked()
            if (closeDialog) {
@@ -190,6 +193,10 @@ class ControlActionCoordinatorImpl @Inject constructor(
        }
    }

    @VisibleForTesting
    fun createAction(controlId: String, f: () -> Unit, blockable: Boolean) =
        Action(controlId, f, blockable)

    inner class Action(val controlId: String, val f: () -> Unit, val blockable: Boolean) {
        fun invoke() {
            if (!blockable || shouldRunAction(controlId)) {
+126 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.controls.ui

import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.globalactions.GlobalActionsComponent
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.wm.shell.TaskViewFactory
import dagger.Lazy
import java.util.Optional
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
class ControlActionCoordinatorImplTest : SysuiTestCase() {

    @Mock
    private lateinit var uiController: ControlsUiController
    @Mock
    private lateinit var lazyUiController: Lazy<ControlsUiController>
    @Mock
    private lateinit var keyguardStateController: KeyguardStateController
    @Mock
    private lateinit var bgExecutor: DelayableExecutor
    @Mock
    private lateinit var uiExecutor: DelayableExecutor
    @Mock
    private lateinit var activityStarter: ActivityStarter
    @Mock
    private lateinit var globalActionsComponent: GlobalActionsComponent
    @Mock
    private lateinit var taskViewFactory: Optional<TaskViewFactory>
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private lateinit var cvh: ControlViewHolder

    companion object {
        fun <T> any(): T = Mockito.any<T>()

        private val ID = "id"
    }

    private lateinit var coordinator: ControlActionCoordinatorImpl
    private lateinit var action: ControlActionCoordinatorImpl.Action

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        coordinator = spy(ControlActionCoordinatorImpl(
            mContext,
            bgExecutor,
            uiExecutor,
            activityStarter,
            keyguardStateController,
            globalActionsComponent,
            taskViewFactory,
            getFakeBroadcastDispatcher(),
            lazyUiController
        ))

        `when`(cvh.cws.ci.controlId).thenReturn(ID)
        action = spy(coordinator.Action(ID, {}, false))
        doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean())
    }

    @Test
    fun testToggleRunsWhenUnlocked() {
        `when`(keyguardStateController.isShowing()).thenReturn(false)

        coordinator.toggle(cvh, "", true)
        verify(coordinator).bouncerOrRun(action)
        verify(action).invoke()
    }

    @Test
    fun testToggleDoesNotRunWhenLockedThenRunsWhenUnlocked() {
        `when`(keyguardStateController.isShowing()).thenReturn(true)
        `when`(keyguardStateController.isUnlocked()).thenReturn(false)

        coordinator.toggle(cvh, "", true)
        verify(coordinator).bouncerOrRun(action)
        verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean())
        verify(action, never()).invoke()

        // Simulate a refresh call from a Publisher, which will trigger a call to runPendingAction
        reset(action)
        coordinator.runPendingAction(ID)
        verify(action, never()).invoke()

        `when`(keyguardStateController.isUnlocked()).thenReturn(true)
        reset(action)
        coordinator.runPendingAction(ID)
        verify(action).invoke()
    }
}