From d832feaa41550238c0bd6e3cc5bdd70fafc33719 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Tue, 9 Feb 2021 13:02:20 -0500 Subject: [PATCH] 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 --- .../ui/ControlActionCoordinatorImpl.kt | 17 ++- .../ControlActionCoordinatorImplTest.kt | 126 ++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt 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 247f25e1ccea3..6b300f4e07e43 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -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)) { 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 new file mode 100644 index 0000000000000..9278570714fe2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -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 + @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 + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private lateinit var cvh: ControlViewHolder + + companion object { + fun any(): T = Mockito.any() + + 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() + } +}