From ce5fbaed64a797adb96f9bca8f40e57f0573a621 Mon Sep 17 00:00:00 2001 From: Beverly Date: Mon, 14 Aug 2023 20:57:54 +0000 Subject: [PATCH] Add interactors for KeyEvents This is in preparation to swap over to using the KeyEventInteractor instead of calling methods on CentralSurfaces. However, this is NOT and ideal way for the interactors themselves to be structured. In the future the interactors should be updated to reflect true modern architecture patterns. The logic in these interactors match logic from NotificationShadeWindowViewController and CentralSurfacesImpl. Test: atest KeyEventInteractorTest Test: atest KeyguardKeyEventInteractorTest Bug: 295927655 Change-Id: Id5c5659a4a19a0b0feb45d6ee391930c2063a9b1 --- .../domain/interactor/KeyEventInteractor.kt | 67 +++++ .../interactor/KeyguardKeyEventInteractor.kt | 117 ++++++++ .../util/MediaSessionLegacyHelperWrapper.kt | 30 +++ .../interactor/KeyEventInteractorTest.kt | 147 ++++++++++ .../KeyguardKeyEventInteractorTest.kt | 255 ++++++++++++++++++ 5 files changed, 616 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt new file mode 100644 index 0000000000000..3f2f67dbba37c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 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.keyevent.domain.interactor + +import android.view.KeyEvent +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor +import javax.inject.Inject + +/** + * Sends key events to the appropriate interactors and then acts upon key events that haven't + * already been handled but should be handled by SystemUI. + */ +@SysUISingleton +class KeyEventInteractor +@Inject +constructor( + private val backActionInteractor: BackActionInteractor, + private val keyguardKeyEventInteractor: KeyguardKeyEventInteractor, +) { + fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (keyguardKeyEventInteractor.dispatchKeyEvent(event)) { + return true + } + + when (event.keyCode) { + KeyEvent.KEYCODE_BACK -> { + if (event.handleAction()) { + backActionInteractor.onBackRequested() + } + return true + } + } + return false + } + + fun interceptMediaKey(event: KeyEvent): Boolean { + return keyguardKeyEventInteractor.interceptMediaKey(event) + } + + fun dispatchKeyEventPreIme(event: KeyEvent): Boolean { + return keyguardKeyEventInteractor.dispatchKeyEventPreIme(event) + } + + companion object { + // Most actions shouldn't be handled on the down event and instead handled on subsequent + // key events like ACTION_UP. + fun KeyEvent.handleAction(): Boolean { + return action != KeyEvent.ACTION_DOWN + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt new file mode 100644 index 0000000000000..635961b0ea01e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 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.keyguard.domain.interactor + +import android.content.Context +import android.media.AudioManager +import android.view.KeyEvent +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor.Companion.handleAction +import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import javax.inject.Inject + +/** Handles key events arriving when the keyguard is showing or device is dozing. */ +@SysUISingleton +class KeyguardKeyEventInteractor +@Inject +constructor( + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val keyguardInteractor: KeyguardInteractor, + private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + private val shadeController: ShadeController, + private val mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper, + private val backActionInteractor: BackActionInteractor, +) { + + fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (statusBarStateController.isDozing) { + when (event.keyCode) { + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEvent.KEYCODE_VOLUME_UP -> return dispatchVolumeKeyEvent(event) + } + } + + if (event.handleAction()) { + when (event.keyCode) { + KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent() + KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent() + } + } + return false + } + + /** + * While IME is active and a BACK event is detected, check with {@link + * StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event should be + * handled before routing to IME, in order to prevent the user from having to hit back twice to + * exit bouncer. + */ + fun dispatchKeyEventPreIme(event: KeyEvent): Boolean { + when (event.keyCode) { + KeyEvent.KEYCODE_BACK -> + if ( + statusBarStateController.state == StatusBarState.KEYGUARD && + statusBarKeyguardViewManager.dispatchBackKeyEventPreIme() + ) { + return backActionInteractor.onBackRequested() + } + } + return false + } + + fun interceptMediaKey(event: KeyEvent): Boolean { + return statusBarStateController.state == StatusBarState.KEYGUARD && + statusBarKeyguardViewManager.interceptMediaKey(event) + } + + private fun dispatchMenuKeyEvent(): Boolean { + val shouldUnlockOnMenuPressed = + isDeviceInteractive() && + (statusBarStateController.state != StatusBarState.SHADE) && + statusBarKeyguardViewManager.shouldDismissOnMenuPressed() + if (shouldUnlockOnMenuPressed) { + shadeController.animateCollapseShadeForced() + return true + } + return false + } + + private fun dispatchSpaceEvent(): Boolean { + if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) { + shadeController.animateCollapseShadeForced() + return true + } + return false + } + + private fun dispatchVolumeKeyEvent(event: KeyEvent): Boolean { + mediaSessionLegacyHelperWrapper + .getHelper(context) + .sendVolumeKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE, true) + return true + } + + private fun isDeviceInteractive(): Boolean { + return keyguardInteractor.wakefulnessModel.value.isDeviceInteractive() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt new file mode 100644 index 0000000000000..99243696a4e0e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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.media.controls.util + +import android.content.Context +import android.media.session.MediaSessionLegacyHelper +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** Injectable wrapper around `MediaSessionLegacyHelper` functions */ +@SysUISingleton +class MediaSessionLegacyHelperWrapper @Inject constructor() { + fun getHelper(context: Context): MediaSessionLegacyHelper { + return MediaSessionLegacyHelper.getHelper(context) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt new file mode 100644 index 0000000000000..632d149c95202 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 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.keyevent.domain.interactor + +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyEventInteractorTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private lateinit var keyguardInteractorWithDependencies: + KeyguardInteractorFactory.WithDependencies + @Mock private lateinit var keyguardKeyEventInteractor: KeyguardKeyEventInteractor + @Mock private lateinit var backActionInteractor: BackActionInteractor + + private lateinit var underTest: KeyEventInteractor + + @Before + fun setup() { + keyguardInteractorWithDependencies = KeyguardInteractorFactory.create() + underTest = + KeyEventInteractor( + backActionInteractor, + keyguardKeyEventInteractor, + ) + } + + @Test + fun dispatchBackKey_notHandledByKeyguardKeyEventInteractor_handledByBackActionInteractor() { + val backKeyEventActionDown = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK) + val backKeyEventActionUp = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK) + + // GIVEN back key ACTION_DOWN and ACTION_UP aren't handled by the keyguardKeyEventInteractor + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionDown)) + .thenReturn(false) + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionUp)) + .thenReturn(false) + + // WHEN back key event ACTION_DOWN, the event is handled even though back isn't requested + assertThat(underTest.dispatchKeyEvent(backKeyEventActionDown)).isTrue() + // THEN back event isn't handled on ACTION_DOWN + verify(backActionInteractor, never()).onBackRequested() + + // WHEN back key event ACTION_UP + assertThat(underTest.dispatchKeyEvent(backKeyEventActionUp)).isTrue() + // THEN back event is handled on ACTION_UP + verify(backActionInteractor).onBackRequested() + } + + @Test + fun dispatchKeyEvent_isNotHandledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(false) + assertThat(underTest.dispatchKeyEvent(keyEvent)).isFalse() + } + + @Test + fun dispatchKeyEvent_handledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(true) + assertThat(underTest.dispatchKeyEvent(keyEvent)).isTrue() + } + + @Test + fun interceptMediaKey_isNotHandledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(false) + assertThat(underTest.interceptMediaKey(keyEvent)).isFalse() + } + + @Test + fun interceptMediaKey_handledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(true) + assertThat(underTest.interceptMediaKey(keyEvent)).isTrue() + } + + @Test + fun dispatchKeyEventPreIme_isNotHandledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(false) + assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isFalse() + } + + @Test + fun dispatchKeyEventPreIme_handledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(true) + assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt new file mode 100644 index 0000000000000..a3f7fc5fc8cf4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 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.keyguard.domain.interactor + +import android.media.AudioManager +import android.media.session.MediaSessionLegacyHelper +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardKeyEventInteractorTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private val actionDownVolumeDownKeyEvent = + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN) + private val actionDownVolumeUpKeyEvent = + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP) + private val backKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK) + private val awakeWakefulnessMode = + WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + private val asleepWakefulnessMode = + WakefulnessModel(WakefulnessState.ASLEEP, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + + private lateinit var keyguardInteractorWithDependencies: + KeyguardInteractorFactory.WithDependencies + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock private lateinit var shadeController: ShadeController + @Mock private lateinit var mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper + @Mock private lateinit var mediaSessionLegacyHelper: MediaSessionLegacyHelper + @Mock private lateinit var backActionInteractor: BackActionInteractor + + private lateinit var underTest: KeyguardKeyEventInteractor + + @Before + fun setup() { + whenever(mediaSessionLegacyHelperWrapper.getHelper(any())) + .thenReturn(mediaSessionLegacyHelper) + keyguardInteractorWithDependencies = KeyguardInteractorFactory.create() + underTest = + KeyguardKeyEventInteractor( + context, + statusBarStateController, + keyguardInteractorWithDependencies.keyguardInteractor, + statusBarKeyguardViewManager, + shadeController, + mediaSessionLegacyHelperWrapper, + backActionInteractor, + ) + } + + @Test + fun dispatchKeyEvent_volumeKey_dozing_handlesEvents() { + whenever(statusBarStateController.isDozing).thenReturn(true) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeDownKeyEvent)).isTrue() + verify(mediaSessionLegacyHelper) + .sendVolumeKeyEvent( + eq(actionDownVolumeDownKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isTrue() + verify(mediaSessionLegacyHelper) + .sendVolumeKeyEvent( + eq(actionDownVolumeUpKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + } + + @Test + fun dispatchKeyEvent_volumeKey_notDozing_doesNotHandleEvents() { + whenever(statusBarStateController.isDozing).thenReturn(false) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeDownKeyEvent)).isFalse() + verify(mediaSessionLegacyHelper, never()) + .sendVolumeKeyEvent( + eq(actionDownVolumeDownKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isFalse() + verify(mediaSessionLegacyHelper, never()) + .sendVolumeKeyEvent( + eq(actionDownVolumeUpKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + } + + @Test + fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) + + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEvent_menuActionUp_interactiveShadeLocked_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) + + // action down: does NOT collapse the shade + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + + // action up: collapses the shade + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) + + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + + // action down: does NOT collapse the shade + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + + // action up: collapses the shade + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEventPreIme_back_keyguard_onBackRequested() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(true) + + whenever(backActionInteractor.onBackRequested()).thenReturn(false) + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse() + verify(backActionInteractor).onBackRequested() + clearInvocations(backActionInteractor) + + whenever(backActionInteractor.onBackRequested()).thenReturn(true) + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isTrue() + verify(backActionInteractor).onBackRequested() + } + + @Test + fun dispatchKeyEventPreIme_back_keyguard_SBKVMdoesNotHandle_neverOnBackRequested() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(false) + whenever(backActionInteractor.onBackRequested()).thenReturn(true) + + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse() + verify(backActionInteractor, never()).onBackRequested() + } + + @Test + fun dispatchKeyEventPreIme_back_shade_neverOnBackRequested() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(true) + whenever(backActionInteractor.onBackRequested()).thenReturn(true) + + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse() + verify(backActionInteractor, never()).onBackRequested() + } + + @Test + fun interceptMediaKey_keyguard_SBKVMdoesNotHandle_doesNotHandleMediaKey() { + val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.interceptMediaKey(eq(keyEvent))).thenReturn(false) + + assertThat(underTest.interceptMediaKey(keyEvent)).isFalse() + verify(statusBarKeyguardViewManager).interceptMediaKey(eq(keyEvent)) + } + + @Test + fun interceptMediaKey_keyguard_handleMediaKey() { + val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.interceptMediaKey(eq(keyEvent))).thenReturn(true) + + assertThat(underTest.interceptMediaKey(keyEvent)).isTrue() + verify(statusBarKeyguardViewManager).interceptMediaKey(eq(keyEvent)) + } + + @Test + fun interceptMediaKey_shade_doesNotHandleMediaKey() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + + assertThat( + underTest.interceptMediaKey( + KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP) + ) + ) + .isFalse() + verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any()) + } +}