Loading packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +39 −10 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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) } Loading @@ -374,7 +406,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { subtypes = List(auxiliarySubtypes + nonAuxiliarySubtypes) { InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes) } }, ) } Loading @@ -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)) } } packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +6 −0 Original line number Diff line number Diff line Loading @@ -86,6 +86,9 @@ sealed class AuthMethodBouncerViewModel( _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED clearInput() if (authenticationResult == AuthenticationResult.SUCCEEDED) { onSuccessfulAuthentication() } } awaitCancellation() } Loading Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +51 −38 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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) } Loading @@ -126,6 +131,10 @@ constructor( } awaitCancellation() } } finally { // reset whenever the view model is "deactivated" wasSuccessfullyAuthenticated = false } } override fun onHidden() { Loading @@ -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()) { Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +39 −10 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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) } Loading @@ -374,7 +406,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { subtypes = List(auxiliarySubtypes + nonAuxiliarySubtypes) { InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes) } }, ) } Loading @@ -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)) } }
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +6 −0 Original line number Diff line number Diff line Loading @@ -86,6 +86,9 @@ sealed class AuthMethodBouncerViewModel( _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED clearInput() if (authenticationResult == AuthenticationResult.SUCCEEDED) { onSuccessfulAuthentication() } } awaitCancellation() } Loading Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +51 −38 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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) } Loading @@ -126,6 +131,10 @@ constructor( } awaitCancellation() } } finally { // reset whenever the view model is "deactivated" wasSuccessfullyAuthenticated = false } } override fun onHidden() { Loading @@ -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()) { Loading