diff options
14 files changed, 407 insertions, 13 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index d3c345deebad..f5e6caf0d9b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -25,6 +25,7 @@ import android.media.AudioManager.OnCommunicationDeviceChangedListener import android.media.IVolumeController import android.provider.Settings import android.util.Log +import android.view.KeyEvent import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils import com.android.settingslib.volume.data.model.VolumeControllerEvent @@ -104,6 +105,8 @@ interface AudioRepository { @AudioDeviceCategory suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int suspend fun notifyVolumeControllerVisible(isVisible: Boolean) + + fun dispatchMediaKeyEvent(event: KeyEvent) } class AudioRepositoryImpl( @@ -265,6 +268,10 @@ class AudioRepositoryImpl( } } + override fun dispatchMediaKeyEvent(event: KeyEvent) { + audioManager.dispatchMediaKeyEvent(event) + } + private fun getMinVolume(stream: AudioStream): Int = try { audioManager.getStreamMinVolume(stream.value) @@ -320,15 +327,9 @@ private class ProducingVolumeController : IVolumeController.Stub() { mutableEvents.tryEmit(VolumeControllerEvent.SetA11yMode(mode)) } - override fun displayCsdWarning( - csdWarning: Int, - displayDurationMs: Int, - ) { + override fun displayCsdWarning(csdWarning: Int, displayDurationMs: Int) { mutableEvents.tryEmit( - VolumeControllerEvent.DisplayCsdWarning( - csdWarning, - displayDurationMs, - ) + VolumeControllerEvent.DisplayCsdWarning(csdWarning, displayDurationMs) ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt index 945e44afa455..fbdab7d40c9b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.shade.ShadeController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -53,6 +54,7 @@ import org.mockito.kotlin.isNull @RunWith(AndroidJUnit4::class) class KeyguardKeyEventInteractorTest : SysuiTestCase() { @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos() private val actionDownVolumeDownKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN) @@ -85,6 +87,7 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() { mediaSessionLegacyHelperWrapper, backActionInteractor, powerInteractor, + kosmos.keyguardMediaKeyInteractor, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorTest.kt new file mode 100644 index 000000000000..b82e1542903b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 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.platform.test.annotations.EnableFlags +import android.view.KeyEvent +import android.view.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.ACTION_UP +import android.view.KeyEvent.KEYCODE_MEDIA_PAUSE +import android.view.KeyEvent.KEYCODE_MEDIA_PLAY +import android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMPOSE_BOUNCER +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.testKosmos +import com.android.systemui.volume.data.repository.fakeAudioRepository +import com.google.common.truth.Correspondence.transforming +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_COMPOSE_BOUNCER) +class KeyguardMediaKeyInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest = kosmos.keyguardMediaKeyInteractor + + @Before + fun setup() { + underTest.activateIn(testScope) + } + + @Test + fun test_onKeyEvent_playPauseKeyEvents_areSkipped_whenACallIsActive() = + testScope.runTest { + kosmos.fakeTelephonyRepository.setIsInCall(true) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KEYCODE_MEDIA_PLAY)) + assertEventConsumed(KeyEvent(ACTION_DOWN, KEYCODE_MEDIA_PAUSE)) + assertEventConsumed(KeyEvent(ACTION_DOWN, KEYCODE_MEDIA_PLAY_PAUSE)) + + assertThat(kosmos.fakeAudioRepository.dispatchedKeyEvents).isEmpty() + } + + @Test + fun test_onKeyEvent_playPauseKeyEvents_areNotSkipped_whenACallIsNotActive() = + testScope.runTest { + kosmos.fakeTelephonyRepository.setIsInCall(false) + + assertEventNotConsumed(KeyEvent(ACTION_DOWN, KEYCODE_MEDIA_PAUSE)) + assertEventConsumed(KeyEvent(ACTION_UP, KEYCODE_MEDIA_PAUSE)) + assertEventNotConsumed(KeyEvent(ACTION_DOWN, KEYCODE_MEDIA_PLAY)) + assertEventConsumed(KeyEvent(ACTION_UP, KEYCODE_MEDIA_PLAY)) + assertEventNotConsumed(KeyEvent(ACTION_DOWN, KEYCODE_MEDIA_PLAY_PAUSE)) + assertEventConsumed(KeyEvent(ACTION_UP, KEYCODE_MEDIA_PLAY_PAUSE)) + + assertThat(kosmos.fakeAudioRepository.dispatchedKeyEvents) + .comparingElementsUsing<KeyEvent, Pair<Int, Int>>( + transforming({ Pair(it!!.action, it.keyCode) }, "action and keycode") + ) + .containsExactly( + Pair(ACTION_UP, KEYCODE_MEDIA_PAUSE), + Pair(ACTION_UP, KEYCODE_MEDIA_PLAY), + Pair(ACTION_UP, KEYCODE_MEDIA_PLAY_PAUSE), + ) + .inOrder() + } + + @Test + fun test_onKeyEvent_nonPlayPauseKeyEvents_areNotSkipped_whenACallIsActive() = + testScope.runTest { + kosmos.fakeTelephonyRepository.setIsInCall(true) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MUTE)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MUTE)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_REWIND)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_RECORD)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_RECORD)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)) + + assertEventConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK)) + assertEventConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK)) + + assertThat(kosmos.fakeAudioRepository.dispatchedKeyEvents) + .comparingElementsUsing<KeyEvent, Pair<Int, Int>>( + transforming({ Pair(it!!.action, it.keyCode) }, "action and keycode") + ) + .containsExactly( + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MUTE), + Pair(ACTION_UP, KeyEvent.KEYCODE_MUTE), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK), + Pair(ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_REWIND), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_RECORD), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_RECORD), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD), + Pair(ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK), + Pair(ACTION_UP, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK), + ) + .inOrder() + } + + @Test + fun volumeKeyEvents_keyEvents_areSkipped() = + testScope.runTest { + kosmos.fakeTelephonyRepository.setIsInCall(false) + + assertEventNotConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP)) + assertEventNotConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP)) + assertEventNotConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN)) + assertEventNotConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_VOLUME_DOWN)) + assertEventNotConsumed(KeyEvent(ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE)) + assertEventNotConsumed(KeyEvent(ACTION_UP, KeyEvent.KEYCODE_VOLUME_MUTE)) + } + + private fun assertEventConsumed(keyEvent: KeyEvent) { + assertThat(underTest.processMediaKeyEvent(keyEvent)).isTrue() + } + + private fun assertEventNotConsumed(keyEvent: KeyEvent) { + assertThat(underTest.processMediaKeyEvent(keyEvent)).isFalse() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/startable/BouncerStartable.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/startable/BouncerStartable.kt new file mode 100644 index 000000000000..e3cf88c94afd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/startable/BouncerStartable.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 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.bouncer.domain.startable + +import com.android.app.tracing.coroutines.launchTraced +import com.android.systemui.CoreStartable +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardMediaKeyInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Starts interactors needed for the compose bouncer to work as expected. */ +@SysUISingleton +class BouncerStartable +@Inject +constructor( + private val keyguardMediaKeyInteractor: dagger.Lazy<KeyguardMediaKeyInteractor>, + @Application private val scope: CoroutineScope, +) : CoreStartable { + override fun start() { + if (!ComposeBouncerFlags.isEnabled) return + + scope.launchTraced("KeyguardMediaKeyInteractor#start") { + keyguardMediaKeyInteractor.get().activate() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt index 7647cf6081bf..47570a53d663 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt @@ -43,17 +43,25 @@ object ComposeBouncerFlags { fun isUnexpectedlyInLegacyMode() = RefactorFlagUtils.isUnexpectedlyInLegacyMode( isEnabled, - "SceneContainerFlag || ComposeBouncerFlag" + "SceneContainerFlag || ComposeBouncerFlag", ) /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + fun assertInLegacyMode() = + RefactorFlagUtils.assertInLegacyMode(isEnabled, "SceneContainerFlag || ComposeBouncerFlag") + + /** * Returns `true` if only compose bouncer is enabled and scene container framework is not * enabled. */ @Deprecated( "Avoid using this, this is meant to be used only by the glue code " + "that includes compose bouncer in legacy keyguard.", - replaceWith = ReplaceWith("isComposeBouncerOrSceneContainerEnabled()") + replaceWith = ReplaceWith("isComposeBouncerOrSceneContainerEnabled()"), ) fun isOnlyComposeBouncerEnabled(): Boolean { return !SceneContainerFlag.isEnabled && Flags.composeBouncer() diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt index 8427b27b78e1..67d312e17069 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt @@ -34,6 +34,7 @@ import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardMediaKeyInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.assisted.AssistedFactory @@ -63,6 +64,7 @@ constructor( private val patternViewModelFactory: PatternBouncerViewModel.Factory, private val passwordViewModelFactory: PasswordBouncerViewModel.Factory, private val bouncerHapticPlayer: BouncerHapticPlayer, + private val keyguardMediaKeyInteractor: KeyguardMediaKeyInteractor, ) : ExclusiveActivatable() { private val _selectedUserImage = MutableStateFlow<Bitmap?>(null) val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow() @@ -337,6 +339,7 @@ constructor( * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise. */ fun onKeyEvent(keyEvent: KeyEvent): Boolean { + if (keyguardMediaKeyInteractor.processMediaKeyEvent(keyEvent.nativeKeyEvent)) return true return authMethodViewModel.value?.onKeyEvent(keyEvent.type, keyEvent.nativeKeyEvent.keyCode) ?: false } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 0de919deb943..6fb6236a4ed3 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -23,6 +23,7 @@ import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService +import com.android.systemui.bouncer.domain.startable.BouncerStartable import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser @@ -304,6 +305,11 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap + @ClassKey(BouncerStartable::class) + abstract fun bindBouncerStartable(impl: BouncerStartable): CoreStartable + + @Binds + @IntoMap @ClassKey(KeyguardDismissBinder::class) abstract fun bindKeyguardDismissBinder(impl: KeyguardDismissBinder): CoreStartable 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 index fcf486b5696b..d4d7e75a8b41 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt @@ -20,6 +20,7 @@ import android.content.Context import android.media.AudioManager import android.view.KeyEvent import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler.Companion.handleAction import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper @@ -45,6 +46,7 @@ constructor( private val mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper, private val backActionInteractor: BackActionInteractor, private val powerInteractor: PowerInteractor, + private val keyguardMediaKeyInteractor: KeyguardMediaKeyInteractor, ) { fun dispatchKeyEvent(event: KeyEvent): Boolean { @@ -96,8 +98,15 @@ constructor( } fun interceptMediaKey(event: KeyEvent): Boolean { - return statusBarStateController.state == StatusBarState.KEYGUARD && - statusBarKeyguardViewManager.interceptMediaKey(event) + return when (statusBarStateController.state) { + StatusBarState.KEYGUARD -> + if (ComposeBouncerFlags.isEnabled) { + keyguardMediaKeyInteractor.processMediaKeyEvent(event) + } else { + statusBarKeyguardViewManager.interceptMediaKey(event) + } + else -> false + } } private fun dispatchMenuKeyEvent(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractor.kt new file mode 100644 index 000000000000..1404ef6a8fab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractor.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 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.view.KeyEvent +import com.android.settingslib.volume.data.repository.AudioRepository +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import javax.inject.Inject + +/** Handle media key events while on keyguard or bouncer. */ +@SysUISingleton +class KeyguardMediaKeyInteractor +@Inject +constructor( + private val telephonyInteractor: TelephonyInteractor, + private val audioRepository: AudioRepository, +) : ExclusiveActivatable() { + + /** + * Allows the media keys to work when the keyguard is showing. Forwards the relevant media keys + * to [AudioManager]. + * + * @param event The key event + * @return whether the event was consumed as a media key. + */ + fun processMediaKeyEvent(event: KeyEvent): Boolean { + if (ComposeBouncerFlags.isUnexpectedlyInLegacyMode()) { + return false + } + val keyCode = event.keyCode + if (event.action == KeyEvent.ACTION_DOWN) { + when (keyCode) { + KeyEvent.KEYCODE_MEDIA_PLAY, + KeyEvent.KEYCODE_MEDIA_PAUSE, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { + /* Suppress PLAY/PAUSE toggle when phone is ringing or + * in-call to avoid music playback */ + // suppress key event + return telephonyInteractor.isInCall.value + } + + KeyEvent.KEYCODE_MUTE, + KeyEvent.KEYCODE_HEADSETHOOK, + KeyEvent.KEYCODE_MEDIA_STOP, + KeyEvent.KEYCODE_MEDIA_NEXT, + KeyEvent.KEYCODE_MEDIA_PREVIOUS, + KeyEvent.KEYCODE_MEDIA_REWIND, + KeyEvent.KEYCODE_MEDIA_RECORD, + KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, + KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK -> { + audioRepository.dispatchMediaKeyEvent(event) + return true + } + + KeyEvent.KEYCODE_VOLUME_UP, + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEvent.KEYCODE_VOLUME_MUTE -> return false + } + } else if (event.action == KeyEvent.ACTION_UP) { + when (keyCode) { + KeyEvent.KEYCODE_MUTE, + KeyEvent.KEYCODE_HEADSETHOOK, + KeyEvent.KEYCODE_MEDIA_PLAY, + KeyEvent.KEYCODE_MEDIA_PAUSE, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, + KeyEvent.KEYCODE_MEDIA_STOP, + KeyEvent.KEYCODE_MEDIA_NEXT, + KeyEvent.KEYCODE_MEDIA_PREVIOUS, + KeyEvent.KEYCODE_MEDIA_REWIND, + KeyEvent.KEYCODE_MEDIA_RECORD, + KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, + KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK -> { + audioRepository.dispatchMediaKeyEvent(event) + return true + } + } + } + return false + } + + override suspend fun onActivated(): Nothing { + // Collect to keep this flow hot for this interactor. + telephonyInteractor.isInCall.collect {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 74c6e72d3400..f7fea7b0d334 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -1535,6 +1535,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean interceptMediaKey(KeyEvent event) { + ComposeBouncerFlags.assertInLegacyMode(); return mPrimaryBouncerView.getDelegate() != null && mPrimaryBouncerView.getDelegate().interceptMediaKey(event); } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 1b1d8c5d0f63..c77d0aab653d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -28,6 +28,7 @@ import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.haptics.msdl.bouncerHapticPlayer import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardMediaKeyInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.user.domain.interactor.selectedUserInteractor @@ -60,6 +61,7 @@ val Kosmos.bouncerSceneContentViewModel by Fixture { patternViewModelFactory = patternBouncerViewModelFactory, passwordViewModelFactory = passwordBouncerViewModelFactory, bouncerHapticPlayer = bouncerHapticPlayer, + keyguardMediaKeyInteractor = keyguardMediaKeyInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorKosmos.kt new file mode 100644 index 000000000000..6f4787b0290b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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 com.android.systemui.kosmos.Kosmos +import com.android.systemui.telephony.domain.interactor.telephonyInteractor +import com.android.systemui.volume.data.repository.audioRepository + +val Kosmos.keyguardMediaKeyInteractor: KeyguardMediaKeyInteractor by + Kosmos.Fixture { + KeyguardMediaKeyInteractor( + telephonyInteractor = telephonyInteractor, + audioRepository = audioRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt index 5cf214a4e04a..712ec41bbf2d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt @@ -18,4 +18,5 @@ package com.android.systemui.volume.data.repository import com.android.systemui.kosmos.Kosmos -val Kosmos.audioRepository by Kosmos.Fixture { FakeAudioRepository() } +val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() } +val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt index ba6ffd742611..16d2a18cd7b2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.data.repository import android.media.AudioDeviceInfo import android.media.AudioManager +import android.view.KeyEvent import com.android.settingslib.volume.data.model.VolumeControllerEvent import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.shared.model.AudioStream @@ -61,6 +62,15 @@ class FakeAudioRepository : AudioRepository { val isInitialized: Boolean get() = mutableIsInitialized + private val _dispatchedKeyEvents = mutableListOf<KeyEvent>() + + val dispatchedKeyEvents: List<KeyEvent> + get() { + val currentValue = _dispatchedKeyEvents.toList() + _dispatchedKeyEvents.clear() + return currentValue + } + override fun init() { mutableIsInitialized = true } @@ -145,4 +155,8 @@ class FakeAudioRepository : AudioRepository { mutableIsVolumeControllerVisible.value = isVisible } } + + override fun dispatchMediaKeyEvent(event: KeyEvent) { + _dispatchedKeyEvents.add(event) + } } |