[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:
@@ -16,6 +16,7 @@
|
||||
|
||||
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.viewmodel.BouncerViewModel
|
||||
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.shade.ui.composable.ShadeScene
|
||||
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
|
||||
import com.android.systemui.statusbar.phone.SystemUIDialog
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Named
|
||||
@@ -57,6 +59,7 @@ object SceneModule {
|
||||
@SysUISingleton
|
||||
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
|
||||
fun bouncerScene(
|
||||
@Application context: Context,
|
||||
viewModelFactory: BouncerViewModel.Factory,
|
||||
): BouncerScene {
|
||||
return BouncerScene(
|
||||
@@ -64,6 +67,7 @@ object SceneModule {
|
||||
viewModelFactory.create(
|
||||
containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
|
||||
),
|
||||
dialogFactory = { SystemUIDialog(context) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
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.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.BouncerViewModel
|
||||
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. */
|
||||
class BouncerScene(
|
||||
private val viewModel: BouncerViewModel,
|
||||
private val dialogFactory: () -> AlertDialog,
|
||||
) : ComposableScene {
|
||||
override val key = SceneKey.Bouncer
|
||||
|
||||
@@ -68,16 +81,19 @@ class BouncerScene(
|
||||
override fun Content(
|
||||
containerName: String,
|
||||
modifier: Modifier,
|
||||
) = BouncerScene(viewModel, modifier)
|
||||
) = BouncerScene(viewModel, dialogFactory, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BouncerScene(
|
||||
viewModel: BouncerViewModel,
|
||||
dialogFactory: () -> AlertDialog,
|
||||
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 dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
|
||||
var dialog: Dialog? by remember { mutableStateOf(null) }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -88,9 +104,10 @@ private fun BouncerScene(
|
||||
Crossfade(
|
||||
targetState = message,
|
||||
label = "Bouncer message",
|
||||
) {
|
||||
animationSpec = if (message.isUpdateAnimated) tween() else snap(),
|
||||
) { message ->
|
||||
Text(
|
||||
text = it,
|
||||
text = message.text,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
@@ -132,5 +149,26 @@ private fun BouncerScene(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ internal fun PasswordBouncer(
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val password: String by viewModel.password.collectAsState()
|
||||
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// When the UI comes up, request focus on the TextField to bring up the software keyboard.
|
||||
@@ -71,6 +72,7 @@ internal fun PasswordBouncer(
|
||||
TextField(
|
||||
value = password,
|
||||
onValueChange = viewModel::onPasswordInputChanged,
|
||||
enabled = isInputEnabled,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
||||
|
||||
@@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.android.internal.R
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
|
||||
import com.android.systemui.compose.modifiers.thenIf
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
@@ -82,6 +83,8 @@ internal fun PatternBouncer(
|
||||
val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState()
|
||||
// The dots selected so far, if the user is currently dragging.
|
||||
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.
|
||||
val dotScalingAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } }
|
||||
@@ -96,16 +99,24 @@ internal fun PatternBouncer(
|
||||
val view = LocalView.current
|
||||
|
||||
// When the current dot is changed, we need to update our animations.
|
||||
LaunchedEffect(currentDot) {
|
||||
LaunchedEffect(currentDot, isAnimationEnabled) {
|
||||
view.performHapticFeedback(
|
||||
HapticFeedbackConstants.VIRTUAL_KEY,
|
||||
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) ->
|
||||
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)
|
||||
if (isSelected) {
|
||||
animatable.animateTo(1f)
|
||||
@@ -116,14 +127,18 @@ internal fun PatternBouncer(
|
||||
selectedDots.forEach { dot ->
|
||||
lineFadeOutAnimatables[dot]?.let { line ->
|
||||
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 {
|
||||
if (dot == currentDot) {
|
||||
// Reset the fade-out animation for the current dot. When the current
|
||||
// dot is switched, this entire code block runs again for the newly
|
||||
// selected dot.
|
||||
// Reset the fade-out animation for the current dot. When the
|
||||
// current dot is switched, this entire code block runs again for
|
||||
// the newly selected dot.
|
||||
line.snapTo(1f)
|
||||
} 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(
|
||||
targetValue = 0f,
|
||||
animationSpec =
|
||||
@@ -148,27 +163,34 @@ internal fun PatternBouncer(
|
||||
// when it leaves the bounds of the dot grid.
|
||||
.clipToBounds()
|
||||
.onSizeChanged { containerSize = it }
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { start ->
|
||||
inputPosition = start
|
||||
viewModel.onDragStart()
|
||||
},
|
||||
onDragEnd = {
|
||||
inputPosition = null
|
||||
lineFadeOutAnimatables.values.forEach { animatable ->
|
||||
scope.launch { animatable.animateTo(1f) }
|
||||
}
|
||||
viewModel.onDragEnd()
|
||||
},
|
||||
) { change, _ ->
|
||||
inputPosition = change.position
|
||||
viewModel.onDrag(
|
||||
xPx = change.position.x,
|
||||
yPx = change.position.y,
|
||||
containerSizePx = containerSize.width,
|
||||
verticalOffsetPx = verticalOffset,
|
||||
)
|
||||
.thenIf(isInputEnabled) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { start ->
|
||||
inputPosition = start
|
||||
viewModel.onDragStart()
|
||||
},
|
||||
onDragEnd = {
|
||||
inputPosition = null
|
||||
if (isAnimationEnabled) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
viewModel.onDragEnd()
|
||||
},
|
||||
) { change, _ ->
|
||||
inputPosition = change.position
|
||||
viewModel.onDrag(
|
||||
xPx = change.position.x,
|
||||
yPx = change.position.y,
|
||||
containerSizePx = containerSize.width,
|
||||
verticalOffsetPx = verticalOffset,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
@@ -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.Icon
|
||||
import com.android.systemui.common.ui.compose.Icon
|
||||
import com.android.systemui.compose.modifiers.thenIf
|
||||
import kotlin.math.max
|
||||
|
||||
@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.
|
||||
val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState()
|
||||
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -116,6 +118,7 @@ internal fun PinBouncer(
|
||||
val digit = index + 1
|
||||
PinButton(
|
||||
onClicked = { viewModel.onPinButtonClicked(digit) },
|
||||
isEnabled = isInputEnabled,
|
||||
) { contentColor ->
|
||||
PinDigit(digit, contentColor)
|
||||
}
|
||||
@@ -124,6 +127,7 @@ internal fun PinBouncer(
|
||||
PinButton(
|
||||
onClicked = { viewModel.onBackspaceButtonClicked() },
|
||||
onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
|
||||
isEnabled = isInputEnabled,
|
||||
isHighlighted = true,
|
||||
) { contentColor ->
|
||||
PinIcon(
|
||||
@@ -138,6 +142,7 @@ internal fun PinBouncer(
|
||||
|
||||
PinButton(
|
||||
onClicked = { viewModel.onPinButtonClicked(0) },
|
||||
isEnabled = isInputEnabled,
|
||||
) { contentColor ->
|
||||
PinDigit(0, contentColor)
|
||||
}
|
||||
@@ -145,6 +150,7 @@ internal fun PinBouncer(
|
||||
PinButton(
|
||||
onClicked = { viewModel.onAuthenticateButtonClicked() },
|
||||
isHighlighted = true,
|
||||
isEnabled = isInputEnabled,
|
||||
) { contentColor ->
|
||||
PinIcon(
|
||||
Icon.Resource(
|
||||
@@ -187,6 +193,7 @@ private fun PinIcon(
|
||||
@Composable
|
||||
private fun PinButton(
|
||||
onClicked: () -> Unit,
|
||||
isEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongPressed: (() -> Unit)? = null,
|
||||
isHighlighted: Boolean = false,
|
||||
@@ -228,16 +235,18 @@ private fun PinButton(
|
||||
cornerRadius = CornerRadius(cornerRadius.toPx()),
|
||||
)
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
isPressed = true
|
||||
tryAwaitRelease()
|
||||
isPressed = false
|
||||
},
|
||||
onTap = { onClicked() },
|
||||
onLongPress = onLongPressed?.let { { onLongPressed() } },
|
||||
)
|
||||
.thenIf(isEnabled) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
isPressed = true
|
||||
tryAwaitRelease()
|
||||
isPressed = false
|
||||
},
|
||||
onTap = { onClicked() },
|
||||
onLongPress = onLongPressed?.let { { onLongPressed() } },
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
content(contentColor)
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Context
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
@@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -45,21 +47,6 @@ constructor(
|
||||
) {
|
||||
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> =
|
||||
interactor.throttling
|
||||
.map { it == null }
|
||||
@@ -104,13 +91,21 @@ constructor(
|
||||
)
|
||||
|
||||
/** The user-facing message to show in the bouncer. */
|
||||
val message: StateFlow<String> =
|
||||
interactor.message
|
||||
.map { it ?: "" }
|
||||
val message: StateFlow<MessageViewModel> =
|
||||
combine(
|
||||
interactor.message,
|
||||
interactor.throttling,
|
||||
) { message, throttling ->
|
||||
toMessageViewModel(message, throttling)
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = interactor.message.value ?: "",
|
||||
initialValue =
|
||||
toMessageViewModel(
|
||||
message = interactor.message.value,
|
||||
throttling = interactor.throttling.value,
|
||||
),
|
||||
)
|
||||
|
||||
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
|
||||
interface Factory {
|
||||
fun create(
|
||||
|
||||
@@ -93,22 +93,21 @@ class BouncerViewModelTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMessageUpdateAnimationsEnabled() =
|
||||
fun message() =
|
||||
testScope.runTest {
|
||||
val isMessageUpdateAnimationsEnabled by
|
||||
collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
|
||||
val message by collectLastValue(underTest.message)
|
||||
val throttling by collectLastValue(bouncerInteractor.throttling)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
assertThat(isMessageUpdateAnimationsEnabled).isTrue()
|
||||
assertThat(message?.isUpdateAnimated).isTrue()
|
||||
|
||||
repeat(BouncerInteractor.THROTTLE_EVERY) {
|
||||
// Wrong PIN.
|
||||
bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
|
||||
}
|
||||
assertThat(isMessageUpdateAnimationsEnabled).isFalse()
|
||||
assertThat(message?.isUpdateAnimated).isFalse()
|
||||
|
||||
throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
|
||||
assertThat(isMessageUpdateAnimationsEnabled).isTrue()
|
||||
assertThat(message?.isUpdateAnimated).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -85,7 +85,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onShown()
|
||||
|
||||
assertThat(message).isEqualTo(ENTER_YOUR_PASSWORD)
|
||||
assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
|
||||
assertThat(password).isEqualTo("")
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -109,7 +109,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onPasswordInputChanged("password")
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
assertThat(password).isEqualTo("password")
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -156,7 +156,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
|
||||
assertThat(password).isEqualTo("")
|
||||
assertThat(message).isEqualTo(WRONG_PASSWORD)
|
||||
assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
@@ -179,13 +179,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
||||
underTest.onPasswordInputChanged("wrong")
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
assertThat(password).isEqualTo("")
|
||||
assertThat(message).isEqualTo(WRONG_PASSWORD)
|
||||
assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
// Enter the correct password:
|
||||
underTest.onPasswordInputChanged("password")
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onShown()
|
||||
|
||||
assertThat(message).isEqualTo(ENTER_YOUR_PATTERN)
|
||||
assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(isUnlocked).isFalse()
|
||||
@@ -115,7 +115,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onDragStart()
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(isUnlocked).isFalse()
|
||||
@@ -202,7 +202,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(message).isEqualTo(WRONG_PATTERN)
|
||||
assertThat(message?.text).isEqualTo(WRONG_PATTERN)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
@@ -235,7 +235,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
||||
underTest.onDragEnd()
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(message).isEqualTo(WRONG_PATTERN)
|
||||
assertThat(message?.text).isEqualTo(WRONG_PATTERN)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onShown()
|
||||
|
||||
assertThat(message).isEqualTo(ENTER_YOUR_PIN)
|
||||
assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
|
||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -116,7 +116,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onPinButtonClicked(1)
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
assertThat(pinLengths).isEqualTo(0 to 1)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -140,7 +140,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
underTest.onBackspaceButtonClicked()
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
assertThat(pinLengths).isEqualTo(1 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -170,7 +170,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS)
|
||||
}
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
assertThat(pinLengths).isEqualTo(1 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -220,7 +220,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
|
||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||
assertThat(message).isEqualTo(WRONG_PIN)
|
||||
assertThat(message?.text).isEqualTo(WRONG_PIN)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
@@ -244,7 +244,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
underTest.onPinButtonClicked(4)
|
||||
underTest.onPinButtonClicked(5) // PIN is now wrong!
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
assertThat(message).isEqualTo(WRONG_PIN)
|
||||
assertThat(message?.text).isEqualTo(WRONG_PIN)
|
||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
@@ -254,7 +254,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
underTest.onPinButtonClicked(2)
|
||||
underTest.onPinButtonClicked(3)
|
||||
underTest.onPinButtonClicked(4)
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(message?.text).isEmpty()
|
||||
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user