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

Commit cd7db757 authored by Chandru S's avatar Chandru S
Browse files

Stop requesting focus to the input text field if authentication was successful

Right now, after successful authentication,
 1. onTextFieldFocusChanged is invoked with false
 2. This triggers the coroutine which sees that `inputEnabled && !hasFocus` is true
 3. isTextFieldFocusRequested is then set to true
 4. The LaunchedEffect(isTextFieldFocusRequested) runs and requests focus on the text field
 5. This repeats until the DisposableEffect and the view is disposed.

This is preventing the RemoteInputView in the notification from getting focus and showing inline reply input area

Test: atest PlatformScenarioTests:android.platform.test.scenario.sysui.notification.NotificationRemoteInput#remoteInputReply_lockscreenPasswordLockedLifetimeExtends -- --abi arm64-v8a
Bug: 368108228
Flag: com.android.systemui.compose_bouncer
Change-Id: I06e2e0d6609ff12be830e712e46ce7bce2e39b5b
parent 30c37da0
Loading
Loading
Loading
Loading
+39 −10
Original line number Diff line number Diff line
@@ -310,6 +310,41 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
                .isEqualTo(displayId)
        }

    @Test
    fun afterSuccessfulAuthentication_focusIsNotRequested() =
        testScope.runTest {
            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
            val textInputFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            lockDeviceAndOpenPasswordBouncer()

            // remove focus from text field
            underTest.onTextFieldFocusChanged(false)
            runCurrent()

            // focus should be requested
            assertThat(textInputFocusRequested).isTrue()

            // simulate text field getting focus
            underTest.onTextFieldFocusChanged(true)
            runCurrent()

            // focus should not be requested anymore
            assertThat(textInputFocusRequested).isFalse()

            // authenticate successfully.
            underTest.onPasswordInputChanged("password")
            underTest.onAuthenticateKeyPressed()
            runCurrent()

            assertThat(authResult).isTrue()

            // remove focus from text field
            underTest.onTextFieldFocusChanged(false)
            runCurrent()
            // focus should not be requested again
            assertThat(textInputFocusRequested).isFalse()
        }

    private fun TestScope.switchToScene(toScene: SceneKey) {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
@@ -327,10 +362,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
        switchToScene(Scenes.Bouncer)
    }

    private suspend fun TestScope.setLockout(
        isLockedOut: Boolean,
        failedAttemptCount: Int = 5,
    ) {
    private suspend fun TestScope.setLockout(isLockedOut: Boolean, failedAttemptCount: Int = 5) {
        if (isLockedOut) {
            repeat(failedAttemptCount) {
                kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(false)
@@ -350,7 +382,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
        kosmos.fakeUserRepository.selectedUser.value =
            SelectedUserModel(
                userInfo = userInfo,
                selectionStatus = SelectionStatus.SELECTION_COMPLETE
                selectionStatus = SelectionStatus.SELECTION_COMPLETE,
            )
        advanceTimeBy(PasswordBouncerViewModel.DELAY_TO_FETCH_IMES)
    }
@@ -374,7 +406,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            subtypes =
                List(auxiliarySubtypes + nonAuxiliarySubtypes) {
                    InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
                }
                },
        )
    }

@@ -383,9 +415,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
        private const val WRONG_PASSWORD = "Wrong password"

        private val USER_INFOS =
            listOf(
                UserInfo(100, "First user", 0),
                UserInfo(101, "Second user", 0),
            )
            listOf(UserInfo(100, "First user", 0), UserInfo(101, "Second user", 0))
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -86,6 +86,9 @@ sealed class AuthMethodBouncerViewModel(

            _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
            clearInput()
            if (authenticationResult == AuthenticationResult.SUCCEEDED) {
                onSuccessfulAuthentication()
            }
        }
        awaitCancellation()
    }
@@ -116,6 +119,9 @@ sealed class AuthMethodBouncerViewModel(
    /** Returns the input entered so far. */
    protected abstract fun getInput(): List<Any>

    /** Invoked after a successful authentication. */
    protected open fun onSuccessfulAuthentication() = Unit

    /** Perform authentication result haptics */
    private fun performAuthenticationHapticFeedback(result: AuthenticationResult) {
        if (result == AuthenticationResult.SKIPPED) return
+51 −38
Original line number Diff line number Diff line
@@ -81,8 +81,10 @@ constructor(
    val selectedUserId: StateFlow<Int> = _selectedUserId.asStateFlow()

    private val requests = Channel<Request>(Channel.BUFFERED)
    private var wasSuccessfullyAuthenticated = false

    override suspend fun onActivated(): Nothing {
        try {
            coroutineScope {
                launch { super.onActivated() }
                launch {
@@ -102,23 +104,26 @@ constructor(
                }
                launch {
                    combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus ->
                        hasInput && !hasFocus
                            hasInput && !hasFocus && !wasSuccessfullyAuthenticated
                        }
                        .collect { _isTextFieldFocusRequested.value = it }
                }
            launch { selectedUserInteractor.selectedUser.collect { _selectedUserId.value = it } }
                launch {
                    selectedUserInteractor.selectedUser.collect { _selectedUserId.value = it }
                }
                launch {
                    // Re-fetch the currently-enabled IMEs whenever the selected user changes, and
                    // whenever
                    // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
                    combine(
                        // InputMethodManagerService sometimes takes some time to update its
                        // internal
                        // state when the selected user changes. As a workaround, delay fetching the
                        // IME
                        // info.
                        selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
                        _isImeSwitcherButtonVisible.onSubscriberAdded()
                            // InputMethodManagerService sometimes takes
                            // some time to update its internal state when the
                            // selected user changes.
                            // As a workaround, delay fetching the IME info.
                            selectedUserInteractor.selectedUser.onEach {
                                delay(DELAY_TO_FETCH_IMES)
                            },
                            _isImeSwitcherButtonVisible.onSubscriberAdded(),
                        ) { selectedUserId, _ ->
                            inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
                        }
@@ -126,6 +131,10 @@ constructor(
                }
                awaitCancellation()
            }
        } finally {
            // reset whenever the view model is "deactivated"
            wasSuccessfullyAuthenticated = false
        }
    }

    override fun onHidden() {
@@ -141,6 +150,10 @@ constructor(
        return _password.value.toCharArray().toList()
    }

    override fun onSuccessfulAuthentication() {
        wasSuccessfullyAuthenticated = true
    }

    /** Notifies that the user has changed the password input. */
    fun onPasswordInputChanged(newPassword: String) {
        if (newPassword.isNotEmpty()) {