[flexiglass] Bouncer throttling - composables.

Compose rendering logic for throttling input entry on the
bouncer when the user enters wrong input too many times.

Bug: 280877228
Test: unit tests
Test: manually verified in PIN, pattern, and password that entering the
wrong input 5, 10, 15, or any number above 15, times shows the
throttling dialog.
Test: manually verified that the throttling dialog cannot be dismissed
without touching its "Ok" button (tapping outside or hitting back don't
dismiss it)
Test: manually verified that input on the bouncer is disabled as the
message is showing the countdown for 30 seconds.
Test: manually verified that after the 30 second countdown, the input
is enabled again and entering the correct input unlocks Flexiglass.

Change-Id: I7f5b8b7e572c4fe00f3315ccb957f036ed9e8d29
This commit is contained in:
Alejandro Nijamkin
2023-05-17 16:02:01 -07:00
parent 4b5abe26d4
commit 33ddd9ab17
10 changed files with 174 additions and 83 deletions

View File

@@ -16,6 +16,7 @@
package com.android.systemui.scene.ui.composable package com.android.systemui.scene.ui.composable
import android.content.Context
import com.android.systemui.bouncer.ui.composable.BouncerScene import com.android.systemui.bouncer.ui.composable.BouncerScene
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
@@ -28,6 +29,7 @@ import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerNames import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.shade.ui.composable.ShadeScene import com.android.systemui.shade.ui.composable.ShadeScene
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
import com.android.systemui.statusbar.phone.SystemUIDialog
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import javax.inject.Named import javax.inject.Named
@@ -57,6 +59,7 @@ object SceneModule {
@SysUISingleton @SysUISingleton
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT) @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
fun bouncerScene( fun bouncerScene(
@Application context: Context,
viewModelFactory: BouncerViewModel.Factory, viewModelFactory: BouncerViewModel.Factory,
): BouncerScene { ): BouncerScene {
return BouncerScene( return BouncerScene(
@@ -64,6 +67,7 @@ object SceneModule {
viewModelFactory.create( viewModelFactory.create(
containerName = SceneContainerNames.SYSTEM_UI_DEFAULT, containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
), ),
dialogFactory = { SystemUIDialog(context) },
) )
} }

View File

@@ -14,9 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package com.android.systemui.bouncer.ui.composable package com.android.systemui.bouncer.ui.composable
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -26,15 +33,20 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
@@ -51,6 +63,7 @@ import kotlinx.coroutines.flow.asStateFlow
/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */ /** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
class BouncerScene( class BouncerScene(
private val viewModel: BouncerViewModel, private val viewModel: BouncerViewModel,
private val dialogFactory: () -> AlertDialog,
) : ComposableScene { ) : ComposableScene {
override val key = SceneKey.Bouncer override val key = SceneKey.Bouncer
@@ -68,16 +81,19 @@ class BouncerScene(
override fun Content( override fun Content(
containerName: String, containerName: String,
modifier: Modifier, modifier: Modifier,
) = BouncerScene(viewModel, modifier) ) = BouncerScene(viewModel, dialogFactory, modifier)
} }
@Composable @Composable
private fun BouncerScene( private fun BouncerScene(
viewModel: BouncerViewModel, viewModel: BouncerViewModel,
dialogFactory: () -> AlertDialog,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val message: String by viewModel.message.collectAsState() val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState() val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
var dialog: Dialog? by remember { mutableStateOf(null) }
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -88,9 +104,10 @@ private fun BouncerScene(
Crossfade( Crossfade(
targetState = message, targetState = message,
label = "Bouncer message", label = "Bouncer message",
) { animationSpec = if (message.isUpdateAnimated) tween() else snap(),
) { message ->
Text( Text(
text = it, text = message.text,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
@@ -132,5 +149,26 @@ private fun BouncerScene(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
} }
if (dialogMessage != null) {
if (dialog == null) {
dialog =
dialogFactory().apply {
setMessage(dialogMessage)
setButton(
DialogInterface.BUTTON_NEUTRAL,
context.getString(R.string.ok),
) { _, _ ->
viewModel.onThrottlingDialogDismissed()
}
setCancelable(false)
setCanceledOnTouchOutside(false)
show()
}
}
} else {
dialog?.dismiss()
dialog = null
}
} }
} }

View File

@@ -53,6 +53,7 @@ internal fun PasswordBouncer(
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val password: String by viewModel.password.collectAsState() val password: String by viewModel.password.collectAsState()
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// When the UI comes up, request focus on the TextField to bring up the software keyboard. // When the UI comes up, request focus on the TextField to bring up the software keyboard.
@@ -71,6 +72,7 @@ internal fun PasswordBouncer(
TextField( TextField(
value = password, value = password,
onValueChange = viewModel::onPasswordInputChanged, onValueChange = viewModel::onPasswordInputChanged,
enabled = isInputEnabled,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
singleLine = true, singleLine = true,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),

View File

@@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp
import com.android.internal.R import com.android.internal.R
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import com.android.systemui.compose.modifiers.thenIf
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -82,6 +83,8 @@ internal fun PatternBouncer(
val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState() val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState()
// The dots selected so far, if the user is currently dragging. // The dots selected so far, if the user is currently dragging.
val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState() val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val isAnimationEnabled: Boolean by viewModel.isPatternVisible.collectAsState()
// Map of animatables for the scale of each dot, keyed by dot. // Map of animatables for the scale of each dot, keyed by dot.
val dotScalingAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } } val dotScalingAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } }
@@ -96,16 +99,24 @@ internal fun PatternBouncer(
val view = LocalView.current val view = LocalView.current
// When the current dot is changed, we need to update our animations. // When the current dot is changed, we need to update our animations.
LaunchedEffect(currentDot) { LaunchedEffect(currentDot, isAnimationEnabled) {
view.performHapticFeedback( view.performHapticFeedback(
HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.VIRTUAL_KEY,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
) )
// Make sure that the current dot is scaled up while the other dots are scaled back down. if (!isAnimationEnabled) {
return@LaunchedEffect
}
// Make sure that the current dot is scaled up while the other dots are scaled back
// down.
dotScalingAnimatables.entries.forEach { (dot, animatable) -> dotScalingAnimatables.entries.forEach { (dot, animatable) ->
val isSelected = dot == currentDot val isSelected = dot == currentDot
launch { // Launch using the longer-lived scope because we want these animations to proceed to
// completion even if the LaunchedEffect is canceled because its key objects have
// changed.
scope.launch {
animatable.animateTo(if (isSelected) 2f else 1f) animatable.animateTo(if (isSelected) 2f else 1f)
if (isSelected) { if (isSelected) {
animatable.animateTo(1f) animatable.animateTo(1f)
@@ -116,14 +127,18 @@ internal fun PatternBouncer(
selectedDots.forEach { dot -> selectedDots.forEach { dot ->
lineFadeOutAnimatables[dot]?.let { line -> lineFadeOutAnimatables[dot]?.let { line ->
if (!line.isRunning) { if (!line.isRunning) {
// Launch using the longer-lived scope because we want these animations to
// proceed to completion even if the LaunchedEffect is canceled because its key
// objects have changed.
scope.launch { scope.launch {
if (dot == currentDot) { if (dot == currentDot) {
// Reset the fade-out animation for the current dot. When the current // Reset the fade-out animation for the current dot. When the
// dot is switched, this entire code block runs again for the newly // current dot is switched, this entire code block runs again for
// selected dot. // the newly selected dot.
line.snapTo(1f) line.snapTo(1f)
} else { } else {
// For all non-current dots, make sure that the lines are fading out. // For all non-current dots, make sure that the lines are fading
// out.
line.animateTo( line.animateTo(
targetValue = 0f, targetValue = 0f,
animationSpec = animationSpec =
@@ -148,7 +163,8 @@ internal fun PatternBouncer(
// when it leaves the bounds of the dot grid. // when it leaves the bounds of the dot grid.
.clipToBounds() .clipToBounds()
.onSizeChanged { containerSize = it } .onSizeChanged { containerSize = it }
.pointerInput(Unit) { .thenIf(isInputEnabled) {
Modifier.pointerInput(Unit) {
detectDragGestures( detectDragGestures(
onDragStart = { start -> onDragStart = { start ->
inputPosition = start inputPosition = start
@@ -156,9 +172,14 @@ internal fun PatternBouncer(
}, },
onDragEnd = { onDragEnd = {
inputPosition = null inputPosition = null
if (isAnimationEnabled) {
lineFadeOutAnimatables.values.forEach { animatable -> lineFadeOutAnimatables.values.forEach { animatable ->
// Launch using the longer-lived scope because we want these
// animations to proceed to completion even if the surrounding
// scope is canceled.
scope.launch { animatable.animateTo(1f) } scope.launch { animatable.animateTo(1f) }
} }
}
viewModel.onDragEnd() viewModel.onDragEnd()
}, },
) { change, _ -> ) { change, _ ->
@@ -171,6 +192,7 @@ internal fun PatternBouncer(
) )
} }
} }
}
) { ) {
// Draw lines between dots. // Draw lines between dots.
selectedDots.forEachIndexed { index, dot -> selectedDots.forEachIndexed { index, dot ->

View File

@@ -63,6 +63,7 @@ import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.thenIf
import kotlin.math.max import kotlin.math.max
@Composable @Composable
@@ -75,6 +76,7 @@ internal fun PinBouncer(
// The length of the PIN input received so far, so we know how many dots to render. // The length of the PIN input received so far, so we know how many dots to render.
val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState() val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState()
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -116,6 +118,7 @@ internal fun PinBouncer(
val digit = index + 1 val digit = index + 1
PinButton( PinButton(
onClicked = { viewModel.onPinButtonClicked(digit) }, onClicked = { viewModel.onPinButtonClicked(digit) },
isEnabled = isInputEnabled,
) { contentColor -> ) { contentColor ->
PinDigit(digit, contentColor) PinDigit(digit, contentColor)
} }
@@ -124,6 +127,7 @@ internal fun PinBouncer(
PinButton( PinButton(
onClicked = { viewModel.onBackspaceButtonClicked() }, onClicked = { viewModel.onBackspaceButtonClicked() },
onLongPressed = { viewModel.onBackspaceButtonLongPressed() }, onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
isEnabled = isInputEnabled,
isHighlighted = true, isHighlighted = true,
) { contentColor -> ) { contentColor ->
PinIcon( PinIcon(
@@ -138,6 +142,7 @@ internal fun PinBouncer(
PinButton( PinButton(
onClicked = { viewModel.onPinButtonClicked(0) }, onClicked = { viewModel.onPinButtonClicked(0) },
isEnabled = isInputEnabled,
) { contentColor -> ) { contentColor ->
PinDigit(0, contentColor) PinDigit(0, contentColor)
} }
@@ -145,6 +150,7 @@ internal fun PinBouncer(
PinButton( PinButton(
onClicked = { viewModel.onAuthenticateButtonClicked() }, onClicked = { viewModel.onAuthenticateButtonClicked() },
isHighlighted = true, isHighlighted = true,
isEnabled = isInputEnabled,
) { contentColor -> ) { contentColor ->
PinIcon( PinIcon(
Icon.Resource( Icon.Resource(
@@ -187,6 +193,7 @@ private fun PinIcon(
@Composable @Composable
private fun PinButton( private fun PinButton(
onClicked: () -> Unit, onClicked: () -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onLongPressed: (() -> Unit)? = null, onLongPressed: (() -> Unit)? = null,
isHighlighted: Boolean = false, isHighlighted: Boolean = false,
@@ -228,7 +235,8 @@ private fun PinButton(
cornerRadius = CornerRadius(cornerRadius.toPx()), cornerRadius = CornerRadius(cornerRadius.toPx()),
) )
} }
.pointerInput(Unit) { .thenIf(isEnabled) {
Modifier.pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onPress = { onPress = {
isPressed = true isPressed = true
@@ -238,6 +246,7 @@ private fun PinButton(
onTap = { onClicked() }, onTap = { onClicked() },
onLongPress = onLongPressed?.let { { onLongPressed() } }, onLongPress = onLongPressed?.let { { onLongPressed() } },
) )
}
}, },
) { ) {
content(contentColor) content(contentColor)

View File

@@ -20,6 +20,7 @@ import android.content.Context
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -45,21 +47,6 @@ constructor(
) { ) {
private val interactor: BouncerInteractor = interactorFactory.create(containerName) private val interactor: BouncerInteractor = interactorFactory.create(containerName)
/**
* Whether updates to the message should be cross-animated from one message to another.
*
* If `false`, no animation should be applied, the message text should just be replaced
* instantly.
*/
val isMessageUpdateAnimationsEnabled: StateFlow<Boolean> =
interactor.throttling
.map { it == null }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = interactor.throttling.value == null,
)
private val isInputEnabled: StateFlow<Boolean> = private val isInputEnabled: StateFlow<Boolean> =
interactor.throttling interactor.throttling
.map { it == null } .map { it == null }
@@ -104,13 +91,21 @@ constructor(
) )
/** The user-facing message to show in the bouncer. */ /** The user-facing message to show in the bouncer. */
val message: StateFlow<String> = val message: StateFlow<MessageViewModel> =
interactor.message combine(
.map { it ?: "" } interactor.message,
interactor.throttling,
) { message, throttling ->
toMessageViewModel(message, throttling)
}
.stateIn( .stateIn(
scope = applicationScope, scope = applicationScope,
started = SharingStarted.WhileSubscribed(), started = SharingStarted.WhileSubscribed(),
initialValue = interactor.message.value ?: "", initialValue =
toMessageViewModel(
message = interactor.message.value,
throttling = interactor.throttling.value,
),
) )
private val _throttlingDialogMessage = MutableStateFlow<String?>(null) private val _throttlingDialogMessage = MutableStateFlow<String?>(null)
@@ -177,6 +172,28 @@ constructor(
} }
} }
private fun toMessageViewModel(
message: String?,
throttling: AuthenticationThrottledModel?,
): MessageViewModel {
return MessageViewModel(
text = message ?: "",
isUpdateAnimated = throttling == null,
)
}
data class MessageViewModel(
val text: String,
/**
* Whether updates to the message should be cross-animated from one message to another.
*
* If `false`, no animation should be applied, the message text should just be replaced
* instantly.
*/
val isUpdateAnimated: Boolean,
)
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create( fun create(

View File

@@ -93,22 +93,21 @@ class BouncerViewModelTest : SysuiTestCase() {
} }
@Test @Test
fun isMessageUpdateAnimationsEnabled() = fun message() =
testScope.runTest { testScope.runTest {
val isMessageUpdateAnimationsEnabled by val message by collectLastValue(underTest.message)
collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
val throttling by collectLastValue(bouncerInteractor.throttling) val throttling by collectLastValue(bouncerInteractor.throttling)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isMessageUpdateAnimationsEnabled).isTrue() assertThat(message?.isUpdateAnimated).isTrue()
repeat(BouncerInteractor.THROTTLE_EVERY) { repeat(BouncerInteractor.THROTTLE_EVERY) {
// Wrong PIN. // Wrong PIN.
bouncerInteractor.authenticate(listOf(3, 4, 5, 6)) bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
} }
assertThat(isMessageUpdateAnimationsEnabled).isFalse() assertThat(message?.isUpdateAnimated).isFalse()
throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) } throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
assertThat(isMessageUpdateAnimationsEnabled).isTrue() assertThat(message?.isUpdateAnimated).isTrue()
} }
@Test @Test

View File

@@ -85,7 +85,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
underTest.onShown() underTest.onShown()
assertThat(message).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isEqualTo("") assertThat(password).isEqualTo("")
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -109,7 +109,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
underTest.onPasswordInputChanged("password") underTest.onPasswordInputChanged("password")
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
assertThat(password).isEqualTo("password") assertThat(password).isEqualTo("password")
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -156,7 +156,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
underTest.onAuthenticateKeyPressed() underTest.onAuthenticateKeyPressed()
assertThat(password).isEqualTo("") assertThat(password).isEqualTo("")
assertThat(message).isEqualTo(WRONG_PASSWORD) assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
} }
@@ -179,13 +179,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
underTest.onPasswordInputChanged("wrong") underTest.onPasswordInputChanged("wrong")
underTest.onAuthenticateKeyPressed() underTest.onAuthenticateKeyPressed()
assertThat(password).isEqualTo("") assertThat(password).isEqualTo("")
assertThat(message).isEqualTo(WRONG_PASSWORD) assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
// Enter the correct password: // Enter the correct password:
underTest.onPasswordInputChanged("password") underTest.onPasswordInputChanged("password")
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
underTest.onAuthenticateKeyPressed() underTest.onAuthenticateKeyPressed()

View File

@@ -89,7 +89,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
underTest.onShown() underTest.onShown()
assertThat(message).isEqualTo(ENTER_YOUR_PATTERN) assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
assertThat(selectedDots).isEmpty() assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull() assertThat(currentDot).isNull()
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
@@ -115,7 +115,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
underTest.onDragStart() underTest.onDragStart()
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
assertThat(selectedDots).isEmpty() assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull() assertThat(currentDot).isNull()
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
@@ -202,7 +202,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
assertThat(selectedDots).isEmpty() assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull() assertThat(currentDot).isNull()
assertThat(message).isEqualTo(WRONG_PATTERN) assertThat(message?.text).isEqualTo(WRONG_PATTERN)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
} }
@@ -235,7 +235,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
underTest.onDragEnd() underTest.onDragEnd()
assertThat(selectedDots).isEmpty() assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull() assertThat(currentDot).isNull()
assertThat(message).isEqualTo(WRONG_PATTERN) assertThat(message?.text).isEqualTo(WRONG_PATTERN)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))

View File

@@ -94,7 +94,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown() underTest.onShown()
assertThat(message).isEqualTo(ENTER_YOUR_PIN) assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
assertThat(pinLengths).isEqualTo(0 to 0) assertThat(pinLengths).isEqualTo(0 to 0)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -116,7 +116,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(1) underTest.onPinButtonClicked(1)
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
assertThat(pinLengths).isEqualTo(0 to 1) assertThat(pinLengths).isEqualTo(0 to 1)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -140,7 +140,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onBackspaceButtonClicked() underTest.onBackspaceButtonClicked()
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
assertThat(pinLengths).isEqualTo(1 to 0) assertThat(pinLengths).isEqualTo(1 to 0)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -170,7 +170,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS) advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS)
} }
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
assertThat(pinLengths).isEqualTo(1 to 0) assertThat(pinLengths).isEqualTo(1 to 0)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -220,7 +220,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onAuthenticateButtonClicked() underTest.onAuthenticateButtonClicked()
assertThat(pinLengths).isEqualTo(0 to 0) assertThat(pinLengths).isEqualTo(0 to 0)
assertThat(message).isEqualTo(WRONG_PIN) assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
} }
@@ -244,7 +244,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(4) underTest.onPinButtonClicked(4)
underTest.onPinButtonClicked(5) // PIN is now wrong! underTest.onPinButtonClicked(5) // PIN is now wrong!
underTest.onAuthenticateButtonClicked() underTest.onAuthenticateButtonClicked()
assertThat(message).isEqualTo(WRONG_PIN) assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(pinLengths).isEqualTo(0 to 0) assertThat(pinLengths).isEqualTo(0 to 0)
assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isFalse()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -254,7 +254,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(2) underTest.onPinButtonClicked(2)
underTest.onPinButtonClicked(3) underTest.onPinButtonClicked(3)
underTest.onPinButtonClicked(4) underTest.onPinButtonClicked(4)
assertThat(message).isEmpty() assertThat(message?.text).isEmpty()
underTest.onAuthenticateButtonClicked() underTest.onAuthenticateButtonClicked()