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:
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user