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
This commit is contained in:
Matt Pietal
2021-02-09 13:02:20 -05:00
parent 351250ae4c
commit d832feaa41
2 changed files with 138 additions and 5 deletions

View File

@@ -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)) {

View File

@@ -0,0 +1,126 @@
/*
* 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()
}
}