diff options
7 files changed, 97 insertions, 87 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index 53e437a2944f..0b1338305076 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -63,11 +64,13 @@ internal fun PasswordBouncer( val isImeVisible by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density) > 0) LaunchedEffect(isImeVisible) { viewModel.onImeVisibilityChanged(isImeVisible) } - LaunchedEffect(Unit) { + DisposableEffect(Unit) { + viewModel.onShown() + // When the UI comes up, request focus on the TextField to bring up the software keyboard. focusRequester.requestFocus() - // Also, report that the UI is shown to let the view-model run some logic. - viewModel.onShown() + + onDispose { viewModel.onHidden() } } LaunchedEffect(animateFailure) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt index 03efbe0fe1ff..2bbe9b8fc20a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -65,8 +66,10 @@ internal fun PatternBouncer( viewModel: PatternBouncerViewModel, modifier: Modifier = Modifier, ) { - // Report that the UI is shown to let the view-model run some logic. - LaunchedEffect(Unit) { viewModel.onShown() } + DisposableEffect(Unit) { + viewModel.onShown() + onDispose { viewModel.onHidden() } + } val colCount = viewModel.columnCount val rowCount = viewModel.rowCount diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index 243751fafe5d..59617c9022ab 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -69,8 +70,10 @@ fun PinPad( viewModel: PinBouncerViewModel, modifier: Modifier = Modifier, ) { - // Report that the UI is shown to let the view-model run some logic. - LaunchedEffect(Unit) { viewModel.onShown() } + DisposableEffect(Unit) { + viewModel.onShown() + onDispose { viewModel.onHidden() } + } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index f46574ca5bbe..80248744c25a 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -62,6 +62,13 @@ sealed class AuthMethodBouncerViewModel( /** Notifies that the UI has been shown to the user. */ fun onShown() { + interactor.resetMessage() + } + + /** + * Notifies that the UI has been hidden from the user (after any transitions have completed). + */ + fun onHidden() { clearInput() interactor.resetMessage() } @@ -113,8 +120,6 @@ sealed class AuthMethodBouncerViewModel( } _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED - // TODO(b/291528545): On success, this should only be cleared after the view is animated - // away). clearInput() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index c498edf0e971..9b1e9585979a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -78,12 +78,28 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { lockDeviceAndOpenPasswordBouncer() assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) - assertThat(password).isEqualTo("") + assertThat(password).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password) } @Test + fun onHidden_resetsPasswordInputAndMessage() = + testScope.runTest { + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + lockDeviceAndOpenPasswordBouncer() + + underTest.onPasswordInputChanged("password") + assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD) + assertThat(password).isNotEmpty() + + underTest.onHidden() + assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) + assertThat(password).isEmpty() + } + + @Test fun onPasswordInputChanged() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) @@ -121,7 +137,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onPasswordInputChanged("wrong") underTest.onAuthenticateKeyPressed() - assertThat(password).isEqualTo("") + assertThat(password).isEmpty() assertThat(message?.text).isEqualTo(WRONG_PASSWORD) } @@ -134,14 +150,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { AuthenticationMethodModel.Password ) utils.deviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - underTest.onShown() - // Enter nothing. + switchToScene(SceneKey.Bouncer) + + // No input entered. underTest.onAuthenticateKeyPressed() - assertThat(password).isEqualTo("") + assertThat(password).isEmpty() assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) } @@ -182,32 +197,33 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { assertThat(password).isEqualTo("password") // The user doesn't confirm the password, but navigates back to the lockscreen instead. - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + switchToScene(SceneKey.Lockscreen) // The user navigates to the bouncer again. - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - - underTest.onShown() + switchToScene(SceneKey.Bouncer) // Ensure the previously-entered password is not shown. assertThat(password).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } + private fun TestScope.switchToScene(toScene: SceneKey) { + val currentScene by collectLastValue(sceneInteractor.desiredScene) + val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer + val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer + sceneInteractor.changeScene(SceneModel(toScene), "reason") + sceneInteractor.onSceneChanged(SceneModel(toScene), "reason") + if (bouncerShown) underTest.onShown() + if (bouncerHidden) underTest.onHidden() + runCurrent() + + assertThat(currentScene).isEqualTo(SceneModel(toScene)) + } + private fun TestScope.lockDeviceAndOpenPasswordBouncer() { utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password) utils.deviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - - assertThat(collectLastValue(sceneInteractor.desiredScene).invoke()) - .isEqualTo(SceneModel(SceneKey.Bouncer)) - underTest.onShown() - runCurrent() + switchToScene(SceneKey.Bouncer) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 3f5ddba23165..125fe680db21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -373,15 +373,23 @@ class PatternBouncerViewModelTest : SysuiTestCase() { ) } + private fun TestScope.switchToScene(toScene: SceneKey) { + val currentScene by collectLastValue(sceneInteractor.desiredScene) + val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer + val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer + sceneInteractor.changeScene(SceneModel(toScene), "reason") + sceneInteractor.onSceneChanged(SceneModel(toScene), "reason") + if (bouncerShown) underTest.onShown() + if (bouncerHidden) underTest.onHidden() + runCurrent() + + assertThat(currentScene).isEqualTo(SceneModel(toScene)) + } + private fun TestScope.lockDeviceAndOpenPatternBouncer() { utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern) utils.deviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - assertThat(collectLastValue(sceneInteractor.desiredScene).invoke()) - .isEqualTo(SceneModel(SceneKey.Bouncer)) - underTest.onShown() - runCurrent() + switchToScene(SceneKey.Bouncer) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 52844cf7f79a..c30e405ab911 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -76,20 +76,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) - utils.deviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - - underTest.onShown() + lockDeviceAndOpenPinBouncer() assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN) assertThat(pin).isEmpty() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin) } @@ -142,29 +134,19 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinButtonClicked() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - underTest.onShown() - runCurrent() + lockDeviceAndOpenPinBouncer() underTest.onPinButtonClicked(1) assertThat(message?.text).isEmpty() assertThat(pin).containsExactly(1) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @Test fun onBackspaceButtonClicked() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -176,7 +158,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @Test @@ -224,9 +205,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { collectLastValue(authenticationInteractor.authenticationChallengeResult) lockDeviceAndOpenPinBouncer() - FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> - underTest.onPinButtonClicked(digit) - } + FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) underTest.onAuthenticateButtonClicked() @@ -274,9 +253,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(authResult).isFalse() // Enter the correct PIN: - FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> - underTest.onPinButtonClicked(digit) - } + FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) assertThat(message?.text).isEmpty() underTest.onAuthenticateButtonClicked() @@ -292,9 +269,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { collectLastValue(authenticationInteractor.authenticationChallengeResult) lockDeviceAndOpenPinBouncer() - FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> - underTest.onPinButtonClicked(digit) - } + FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) assertThat(authResult).isTrue() } @@ -323,31 +298,21 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown_againAfterSceneChange_resetsPin() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() // The user types a PIN. - FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> - underTest.onPinButtonClicked(digit) - } + FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) assertThat(pin).isNotEmpty() // The user doesn't confirm the PIN, but navigates back to the lockscreen instead. - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + switchToScene(SceneKey.Lockscreen) // The user navigates to the bouncer again. - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - - underTest.onShown() + switchToScene(SceneKey.Bouncer) // Ensure the previously-entered PIN is not shown. assertThat(pin).isEmpty() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @Test @@ -414,16 +379,23 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(isAnimationEnabled).isTrue() } + private fun TestScope.switchToScene(toScene: SceneKey) { + val currentScene by collectLastValue(sceneInteractor.desiredScene) + val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer + val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer + sceneInteractor.changeScene(SceneModel(toScene), "reason") + sceneInteractor.onSceneChanged(SceneModel(toScene), "reason") + if (bouncerShown) underTest.onShown() + if (bouncerHidden) underTest.onHidden() + runCurrent() + + assertThat(currentScene).isEqualTo(SceneModel(toScene)) + } + private fun TestScope.lockDeviceAndOpenPinBouncer() { utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.deviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason") - - assertThat(collectLastValue(sceneInteractor.desiredScene).invoke()) - .isEqualTo(SceneModel(SceneKey.Bouncer)) - underTest.onShown() - runCurrent() + switchToScene(SceneKey.Bouncer) } companion object { |