summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt55
10 files changed, 174 insertions, 5 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 8a8557aa1f43..df22a7023ebf 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
@@ -17,8 +17,11 @@
package com.android.systemui.bouncer.ui.composable
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
@@ -29,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -44,6 +48,7 @@ import androidx.compose.ui.unit.dp
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
/** UI for the input part of a password-requiring version of the bouncer. */
+@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun PasswordBouncer(
viewModel: PasswordBouncerViewModel,
@@ -54,6 +59,10 @@ internal fun PasswordBouncer(
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+ val density = LocalDensity.current
+ val isImeVisible by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density) > 0)
+ LaunchedEffect(isImeVisible) { viewModel.onImeVisibilityChanged(isImeVisible) }
+
LaunchedEffect(Unit) {
// When the UI comes up, request focus on the TextField to bring up the software keyboard.
focusRequester.requestFocus()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 199036f09e9a..9b2f2baba94b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -225,6 +225,20 @@ constructor(
repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod()))
}
+ /** If the bouncer is showing, hides the bouncer and return to the lockscreen scene. */
+ fun hide(
+ loggingReason: String,
+ ) {
+ if (sceneInteractor.desiredScene.value.key != SceneKey.Bouncer) {
+ return
+ }
+
+ sceneInteractor.changeScene(
+ scene = SceneModel(SceneKey.Lockscreen),
+ loggingReason = loggingReason,
+ )
+ }
+
private fun promptMessage(authMethod: AuthenticationMethodModel): String {
return when (authMethod) {
is AuthenticationMethodModel.Pin ->
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 d95b70c85fe0..4546bea3b89b 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
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.ui.viewmodel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -28,6 +29,7 @@ sealed class AuthMethodBouncerViewModel(
* being able to attempt to unlock the device.
*/
val isInputEnabled: StateFlow<Boolean>,
+ private val interactor: BouncerInteractor,
) {
private val _animateFailure = MutableStateFlow(false)
@@ -37,6 +39,9 @@ sealed class AuthMethodBouncerViewModel(
*/
val animateFailure: StateFlow<Boolean> = _animateFailure.asStateFlow()
+ /** Whether the input method editor (for example, the software keyboard) is visible. */
+ private var isImeVisible: Boolean = false
+
/**
* Notifies that the failure animation has been shown. This should be called to consume a `true`
* value in [animateFailure].
@@ -45,6 +50,21 @@ sealed class AuthMethodBouncerViewModel(
_animateFailure.value = false
}
+ /**
+ * Notifies that the input method editor (for example, the software keyboard) has been shown or
+ * hidden.
+ */
+ fun onImeVisibilityChanged(isVisible: Boolean) {
+ if (isImeVisible && !isVisible) {
+ // The IME has gone from visible to invisible, dismiss the bouncer.
+ interactor.hide(
+ loggingReason = "IME hidden",
+ )
+ }
+
+ isImeVisible = isVisible
+ }
+
/** Ask the UI to show the failure animation. */
protected fun showFailureAnimation() {
_animateFailure.value = true
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index d21479746744..9e10f29a00f9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -31,6 +31,7 @@ class PasswordBouncerViewModel(
) :
AuthMethodBouncerViewModel(
isInputEnabled = isInputEnabled,
+ interactor = interactor,
) {
private val _password = MutableStateFlow("")
@@ -60,6 +61,10 @@ class PasswordBouncerViewModel(
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
fun onAuthenticateKeyPressed() {
val password = _password.value.toCharArray().toList()
+ if (password.isEmpty()) {
+ return
+ }
+
_password.value = ""
applicationScope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 1985c37e1d5d..497276b47996 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -42,6 +42,7 @@ class PatternBouncerViewModel(
) :
AuthMethodBouncerViewModel(
isInputEnabled = isInputEnabled,
+ interactor = interactor,
) {
/** The number of columns in the dot grid. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index dc5c5288df9f..8e6421ed3f0a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -37,6 +37,7 @@ class PinBouncerViewModel(
) :
AuthMethodBouncerViewModel(
isInputEnabled = isInputEnabled,
+ interactor = interactor,
) {
val pinShapes = PinShapeAdapter(applicationContext)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 6205c277bbd9..77d8102fff2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -353,6 +353,34 @@ class BouncerInteractorTest : SysuiTestCase() {
assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
}
+ @Test
+ fun hide_whenOnBouncerScene_hidesBouncerAndGoesToLockscreenScene() =
+ testScope.runTest {
+ sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "")
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "")
+ val currentScene by collectLastValue(sceneInteractor.desiredScene)
+ val bouncerSceneKey = currentScene?.key
+ assertThat(bouncerSceneKey).isEqualTo(SceneKey.Bouncer)
+
+ underTest.hide("")
+
+ assertThat(currentScene?.key).isEqualTo(SceneKey.Lockscreen)
+ }
+
+ @Test
+ fun hide_whenNotOnBouncerScene_doesNothing() =
+ testScope.runTest {
+ sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "")
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "")
+ val currentScene by collectLastValue(sceneInteractor.desiredScene)
+ val notBouncerSceneKey = currentScene?.key
+ assertThat(notBouncerSceneKey).isNotEqualTo(SceneKey.Bouncer)
+
+ underTest.hide("")
+
+ assertThat(currentScene?.key).isEqualTo(notBouncerSceneKey)
+ }
+
private fun assertTryAgainMessage(
message: String?,
time: Int,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 7af8a0425402..9011c2f296c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -22,6 +22,8 @@ import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
@@ -39,6 +41,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() {
utils.authenticationInteractor(
utils.authenticationRepository(),
)
+ private val sceneInteractor = utils.sceneInteractor()
private val underTest =
PinBouncerViewModel(
applicationContext = context,
@@ -46,7 +49,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() {
interactor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
- sceneInteractor = utils.sceneInteractor(),
+ sceneInteractor = sceneInteractor,
),
isInputEnabled = MutableStateFlow(true),
)
@@ -75,4 +78,22 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() {
underTest.onAuthenticateButtonClicked()
assertThat(animateFailure).isFalse()
}
+
+ @Test
+ fun onImeVisibilityChanged() =
+ testScope.runTest {
+ val desiredScene by collectLastValue(sceneInteractor.desiredScene)
+ sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "")
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "")
+ assertThat(desiredScene?.key).isEqualTo(SceneKey.Bouncer)
+
+ underTest.onImeVisibilityChanged(false)
+ assertThat(desiredScene?.key).isEqualTo(SceneKey.Bouncer)
+
+ underTest.onImeVisibilityChanged(true)
+ assertThat(desiredScene?.key).isEqualTo(SceneKey.Bouncer)
+
+ underTest.onImeVisibilityChanged(false)
+ assertThat(desiredScene?.key).isEqualTo(SceneKey.Lockscreen)
+ }
}
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 8df29e4d4e58..3375184c1cf6 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
@@ -157,6 +157,29 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
}
@Test
+ fun onAuthenticateKeyPressed_whenEmpty() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.desiredScene)
+ val message by collectLastValue(bouncerViewModel.message)
+ val password by collectLastValue(underTest.password)
+ utils.authenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ utils.authenticationRepository.setUnlocked(false)
+ sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ underTest.onShown()
+ // Enter nothing.
+
+ underTest.onAuthenticateKeyPressed()
+
+ assertThat(password).isEqualTo("")
+ assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
+ assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ }
+
+ @Test
fun onAuthenticateKeyPressed_correctAfterWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 9c8d14de1c74..6b918c6ba15b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -22,7 +22,6 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
-import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.shared.model.WakefulnessState
@@ -48,7 +47,9 @@ import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -159,6 +160,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
repository = keyguardRepository,
)
+ private var bouncerSceneJob: Job? = null
+
@Before
fun setUp() {
shadeHeaderViewModel =
@@ -288,7 +291,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
@Test
fun withAuthMethodNone_deviceWakeUp_skipsLockscreen() =
testScope.runTest {
- setAuthMethod(AuthenticationMethodModel.None)
+ setAuthMethod(DomainLayerAuthenticationMethodModel.None)
putDeviceToSleep(instantlyLockDevice = false)
assertCurrentScene(SceneKey.Lockscreen)
@@ -299,7 +302,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
@Test
fun withAuthMethodSwipe_deviceWakeUp_doesNotSkipLockscreen() =
testScope.runTest {
- setAuthMethod(AuthenticationMethodModel.Swipe)
+ setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
putDeviceToSleep(instantlyLockDevice = false)
assertCurrentScene(SceneKey.Lockscreen)
@@ -364,6 +367,23 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
assertCurrentScene(SceneKey.Lockscreen)
}
+ @Test
+ fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() =
+ testScope.runTest {
+ setAuthMethod(DomainLayerAuthenticationMethodModel.Password)
+ val upDestinationSceneKey by
+ collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+ assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer)
+ emulateUserDrivenTransition(
+ to = upDestinationSceneKey,
+ )
+
+ dismissIme()
+
+ assertCurrentScene(SceneKey.Lockscreen)
+ emulateUiSceneTransition()
+ }
+
/**
* Asserts that the current scene in the view-model matches what's expected.
*
@@ -396,7 +416,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
// Set the lockscreen enabled bit _before_ set the auth method as the code picks up on the
// lockscreen enabled bit _after_ the auth method is changed and the lockscreen enabled bit
// is not an observable that can trigger a new evaluation.
- authenticationRepository.setLockscreenEnabled(authMethod !is AuthenticationMethodModel.None)
+ authenticationRepository.setLockscreenEnabled(
+ authMethod !is DomainLayerAuthenticationMethodModel.None
+ )
authenticationRepository.setAuthenticationMethod(authMethod.toDataLayer())
if (!authMethod.isSecure) {
// When the auth method is not secure, the device is never considered locked.
@@ -455,6 +477,19 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
assertWithMessage("Visibility mismatch after scene transition from $from to ${to.key}!")
.that(sceneContainerViewModel.isVisible.value)
.isEqualTo(expectedVisible)
+
+ bouncerSceneJob =
+ if (to.key == SceneKey.Bouncer) {
+ testScope.backgroundScope.launch {
+ bouncerViewModel.authMethod.collect {
+ // Do nothing. Need this to turn this otherwise cold flow, hot.
+ }
+ }
+ } else {
+ bouncerSceneJob?.cancel()
+ null
+ }
+ runCurrent()
}
/**
@@ -573,4 +608,16 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
lockDevice()
}
}
+
+ /** Emulates the dismissal of the IME (soft keyboard). */
+ private fun TestScope.dismissIme(
+ showImeBeforeDismissing: Boolean = true,
+ ) {
+ if (showImeBeforeDismissing) {
+ bouncerViewModel.authMethod.value?.onImeVisibilityChanged(true)
+ }
+
+ bouncerViewModel.authMethod.value?.onImeVisibilityChanged(false)
+ runCurrent()
+ }
}