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

Commit 7a998ac1 authored by Chandru S's avatar Chandru S
Browse files

Handle keyboard/adb keyevent based authentication confirmation for password bouncer

Bug: 310005730
Test: verified manually and through e2e tests
 1. Enable password bouncer
 2. Send a test messaging notification with inline reply option
 3. Lock the device
 4. Go to lockscreen, expand the notification shade, tap reply on the
    notificaiton
 5. password bouncer shows up
 6. enter the password and use `adb shell input keyevent ENTER` for sending the
    confirmation key
 7. Password bouncer dismisses as expected and focus returns to the
    inline reply text box in the notification, instead of showing a
black empty screen
Flag: com.android.systemui.compose_bouncer
Change-Id: I25538d204cc7a56c9566b7fbded1c7013bd28d10

Change-Id: Ifaaebed061205f40139f86816081b3c61f7d3c7e
parent 0a884b6d
Loading
Loading
Loading
Loading
+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
+8 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.bouncer.ui.viewmodel

import android.annotation.StringRes
import androidx.compose.ui.input.key.KeyEventType
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
@@ -122,6 +123,13 @@ sealed class AuthMethodBouncerViewModel(
    /** Invoked after a successful authentication. */
    protected open fun onSuccessfulAuthentication() = Unit

    /**
     * Invoked for any key events on the bouncer.
     *
     * @return whether the event was consumed by this method and should not be propagated further.
     */
    open fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean = false

    /** Perform authentication result haptics */
    private fun performAuthenticationHapticFeedback(result: AuthenticationResult) {
        if (result == AuthenticationResult.SKIPPED) return
+2 −4
Original line number Diff line number Diff line
@@ -337,10 +337,8 @@ constructor(
     * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
     */
    fun onKeyEvent(keyEvent: KeyEvent): Boolean {
        return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
            keyEvent.type,
            keyEvent.nativeKeyEvent.keyCode,
        ) ?: false
        return authMethodViewModel.value?.onKeyEvent(keyEvent.type, keyEvent.nativeKeyEvent.keyCode)
            ?: false
    }

    data class DialogViewModel(
+14 −0
Original line number Diff line number Diff line
@@ -16,9 +16,12 @@

package com.android.systemui.bouncer.ui.viewmodel

import android.view.KeyEvent
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.input.key.KeyEventType
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
import com.android.systemui.res.R
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -150,6 +153,17 @@ constructor(
        return _password.value.toCharArray().toList()
    }

    override fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean {
        // Ignore SPACE as a confirm key to allow the space character within passwords.
        val isKeyboardEnterKey =
            KeyEvent.isConfirmKey(keyCode) &&
                keyCode != KeyEvent.KEYCODE_SPACE &&
                type == KeyEventType.KeyUp
        // consume confirm key events while on the bouncer. This prevents it from propagating
        // and avoids other parent elements from receiving it.
        return isKeyboardEnterKey && ComposeBouncerFlags.isOnlyComposeBouncerEnabled()
    }

    override fun onSuccessfulAuthentication() {
        wasSuccessfullyAuthenticated = true
    }
+1 −1
Original line number Diff line number Diff line
@@ -251,7 +251,7 @@ constructor(
     *
     * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
     */
    fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean {
    override fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean {
        return when (type) {
            KeyEventType.KeyUp -> {
                if (isConfirmKey(keyCode)) {