Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 78183707 authored by Chandru S's avatar Chandru S Committed by Android (Google) Code Review
Browse files

Merge changes I50164f57,Ifaaebed0 into main

* changes:
  Handle media key events while on keyguard and bouncer
  Handle keyboard/adb keyevent based authentication confirmation for password bouncer
parents 663057e8 572db0c6
Loading
Loading
Loading
Loading
+9 −8
Original line number Diff line number Diff line
@@ -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)
        )
    }
}
+37 −1
Original line number Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.systemui.bouncer.ui.viewmodel

import android.content.pm.UserInfo
import android.platform.test.annotations.EnableFlags
import android.view.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.SceneKey
@@ -27,6 +30,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.inputmethod.data.model.InputMethodModel
import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
@@ -67,11 +71,12 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
    private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
    private val isInputEnabled = MutableStateFlow(true)

    private val underTest =
    private val underTest by lazy {
        kosmos.passwordBouncerViewModelFactory.create(
            isInputEnabled = isInputEnabled,
            onIntentionalUserInput = {},
        )
    }

    @Before
    fun setUp() {
@@ -345,6 +350,37 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            assertThat(textInputFocusRequested).isFalse()
        }

    @EnableFlags(com.android.systemui.Flags.FLAG_COMPOSE_BOUNCER)
    @Test
    fun consumeConfirmKeyEvents_toPreventItFromPropagating() =
        testScope.runTest { verifyConfirmKeyEventsBehavior(keyUpEventConsumed = true) }

    @EnableFlags(com.android.systemui.Flags.FLAG_COMPOSE_BOUNCER)
    @EnableSceneContainer
    @Test
    fun noops_whenSceneContainerIsAlsoEnabled() =
        testScope.runTest { verifyConfirmKeyEventsBehavior(keyUpEventConsumed = false) }

    private fun verifyConfirmKeyEventsBehavior(keyUpEventConsumed: Boolean) {
        assertThat(underTest.onKeyEvent(KeyEventType.KeyDown, KeyEvent.KEYCODE_DPAD_CENTER))
            .isFalse()
        assertThat(underTest.onKeyEvent(KeyEventType.KeyUp, KeyEvent.KEYCODE_DPAD_CENTER))
            .isEqualTo(keyUpEventConsumed)

        assertThat(underTest.onKeyEvent(KeyEventType.KeyDown, KeyEvent.KEYCODE_ENTER)).isFalse()
        assertThat(underTest.onKeyEvent(KeyEventType.KeyUp, KeyEvent.KEYCODE_ENTER))
            .isEqualTo(keyUpEventConsumed)

        assertThat(underTest.onKeyEvent(KeyEventType.KeyDown, KeyEvent.KEYCODE_NUMPAD_ENTER))
            .isFalse()
        assertThat(underTest.onKeyEvent(KeyEventType.KeyUp, KeyEvent.KEYCODE_NUMPAD_ENTER))
            .isEqualTo(keyUpEventConsumed)

        // space is ignored.
        assertThat(underTest.onKeyEvent(KeyEventType.KeyUp, KeyEvent.KEYCODE_SPACE)).isFalse()
        assertThat(underTest.onKeyEvent(KeyEventType.KeyDown, KeyEvent.KEYCODE_SPACE)).isFalse()
    }

    private fun TestScope.switchToScene(toScene: SceneKey) {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
+3 −0
Original line number Diff line number Diff line
@@ -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,
            )
    }

+171 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}
+43 −0
Original line number Diff line number Diff line
/*
 * 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()
        }
    }
}
Loading