summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Chandru S <chandruis@google.com> 2024-10-16 22:05:53 +0000
committer Chandru S <chandruis@google.com> 2024-10-20 16:20:24 +0000
commit572db0c63de80c419a04e6b7784015c7ebff0239 (patch)
treeb0851035491a2ec0a2603ef0f66f849a54b41cab
parent7a998ac16c57e26ae8d56069df7698b608b4309f (diff)
Handle media key events while on keyguard and bouncer
This was previously handled by KeyguardSecurityContainerController which will not be active when compose bouncer is enabeld Bug: 310005730 Fixes: 374147259 Flag: com.android.systemui.compose_bouncer Flag: com.android.systemui.scene_container Test: unit tests Change-Id: I50164f57f741545d3eee2f16eb61e82886aa2756
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorTest.kt171
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/startable/BouncerStartable.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractor.kt103
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardMediaKeyInteractorKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt14
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)
+ }
}