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() + } +}