Merge changes from topic "bouncer-scene-throttling-b280877228" into udc-dev am: 637fb7497b am: b06ba6f345
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23353743 Change-Id: I65b9d20c268f1607e529054a8a25c50fb20a7fe1 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -54,6 +54,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()
|
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||||
|
val animateFailure: Boolean by viewModel.animateFailure.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.
|
||||||
@@ -62,6 +63,13 @@ internal fun PasswordBouncer(
|
|||||||
viewModel.onShown()
|
viewModel.onShown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(animateFailure) {
|
||||||
|
if (animateFailure) {
|
||||||
|
// We don't currently have a failure animation for password, just consume it:
|
||||||
|
viewModel.onFailureAnimationShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package com.android.systemui.bouncer.ui.composable
|
|||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.AnimationVector1D
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
@@ -49,6 +50,7 @@ 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
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,6 +88,7 @@ internal fun PatternBouncer(
|
|||||||
val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
|
val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
|
||||||
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||||
val isAnimationEnabled: Boolean by viewModel.isPatternVisible.collectAsState()
|
val isAnimationEnabled: Boolean by viewModel.isPatternVisible.collectAsState()
|
||||||
|
val animateFailure: Boolean by viewModel.animateFailure.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) } }
|
||||||
@@ -174,6 +177,17 @@ internal fun PatternBouncer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the failure animation if the user entered the wrong input.
|
||||||
|
LaunchedEffect(animateFailure) {
|
||||||
|
if (animateFailure) {
|
||||||
|
showFailureAnimation(
|
||||||
|
dots = dots,
|
||||||
|
scalingAnimatables = dotScalingAnimatables,
|
||||||
|
)
|
||||||
|
viewModel.onFailureAnimationShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is the position of the input pointer.
|
// This is the position of the input pointer.
|
||||||
var inputPosition: Offset? by remember { mutableStateOf(null) }
|
var inputPosition: Offset? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
@@ -290,8 +304,61 @@ private fun lineAlpha(gridSpacing: Float, lineLength: Float = gridSpacing): Floa
|
|||||||
return ((lineLength / gridSpacing - 0.3f) * 4f).coerceIn(0f, 1f)
|
return ((lineLength / gridSpacing - 0.3f) * 4f).coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun showFailureAnimation(
|
||||||
|
dots: List<PatternDotViewModel>,
|
||||||
|
scalingAnimatables: Map<PatternDotViewModel, Animatable<Float, AnimationVector1D>>,
|
||||||
|
) {
|
||||||
|
val dotsByRow =
|
||||||
|
buildList<MutableList<PatternDotViewModel>> {
|
||||||
|
dots.forEach { dot ->
|
||||||
|
val rowIndex = dot.y
|
||||||
|
while (size <= rowIndex) {
|
||||||
|
add(mutableListOf())
|
||||||
|
}
|
||||||
|
get(rowIndex).add(dot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
dotsByRow.forEachIndexed { rowIndex, rowDots ->
|
||||||
|
rowDots.forEach { dot ->
|
||||||
|
scalingAnimatables[dot]?.let { dotScaleAnimatable ->
|
||||||
|
launch {
|
||||||
|
dotScaleAnimatable.animateTo(
|
||||||
|
targetValue =
|
||||||
|
FAILURE_ANIMATION_DOT_DIAMETER_DP / DOT_DIAMETER_DP.toFloat(),
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
FAILURE_ANIMATION_DOT_SHRINK_ANIMATION_DURATION_MS,
|
||||||
|
delayMillis =
|
||||||
|
rowIndex * FAILURE_ANIMATION_DOT_SHRINK_STAGGER_DELAY_MS,
|
||||||
|
easing = Easings.LinearEasing,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
dotScaleAnimatable.animateTo(
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
FAILURE_ANIMATION_DOT_REVERT_ANIMATION_DURATION,
|
||||||
|
easing = Easings.StandardEasing,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const val DOT_DIAMETER_DP = 16
|
private const val DOT_DIAMETER_DP = 16
|
||||||
private const val SELECTED_DOT_DIAMETER_DP = 24
|
private const val SELECTED_DOT_DIAMETER_DP = 24
|
||||||
private const val SELECTED_DOT_REACTION_ANIMATION_DURATION_MS = 83
|
private const val SELECTED_DOT_REACTION_ANIMATION_DURATION_MS = 83
|
||||||
private const val SELECTED_DOT_RETRACT_ANIMATION_DURATION_MS = 750
|
private const val SELECTED_DOT_RETRACT_ANIMATION_DURATION_MS = 750
|
||||||
private const val LINE_STROKE_WIDTH_DP = 16
|
private const val LINE_STROKE_WIDTH_DP = 16
|
||||||
|
private const val FAILURE_ANIMATION_DOT_DIAMETER_DP = 13
|
||||||
|
private const val FAILURE_ANIMATION_DOT_SHRINK_ANIMATION_DURATION_MS = 50
|
||||||
|
private const val FAILURE_ANIMATION_DOT_SHRINK_STAGGER_DELAY_MS = 33
|
||||||
|
private const val FAILURE_ANIMATION_DOT_REVERT_ANIMATION_DURATION = 617
|
||||||
|
|||||||
@@ -89,6 +89,15 @@ 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()
|
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||||
|
val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
|
||||||
|
|
||||||
|
// Show the failure animation if the user entered the wrong input.
|
||||||
|
LaunchedEffect(animateFailure) {
|
||||||
|
if (animateFailure) {
|
||||||
|
showFailureAnimation()
|
||||||
|
viewModel.onFailureAnimationShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|||||||
@@ -148,12 +148,17 @@ constructor(
|
|||||||
*
|
*
|
||||||
* If the input is correct, the device will be unlocked and the lock screen and bouncer will be
|
* If the input is correct, the device will be unlocked and the lock screen and bouncer will be
|
||||||
* dismissed and hidden.
|
* dismissed and hidden.
|
||||||
|
*
|
||||||
|
* @param input The input from the user to try to authenticate with. This can be a list of
|
||||||
|
* different things, based on the current authentication method.
|
||||||
|
* @return `true` if the authentication succeeded and the device is now unlocked; `false`
|
||||||
|
* otherwise.
|
||||||
*/
|
*/
|
||||||
fun authenticate(
|
fun authenticate(
|
||||||
input: List<Any>,
|
input: List<Any>,
|
||||||
) {
|
): Boolean {
|
||||||
if (repository.throttling.value != null) {
|
if (repository.throttling.value != null) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val isAuthenticated = authenticationInteractor.authenticate(input)
|
val isAuthenticated = authenticationInteractor.authenticate(input)
|
||||||
@@ -186,6 +191,8 @@ constructor(
|
|||||||
}
|
}
|
||||||
else -> repository.setMessage(errorMessage(authenticationMethod.value))
|
else -> repository.setMessage(errorMessage(authenticationMethod.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isAuthenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun promptMessage(authMethod: AuthenticationMethodModel): String {
|
private fun promptMessage(authMethod: AuthenticationMethodModel): String {
|
||||||
|
|||||||
@@ -16,14 +16,37 @@
|
|||||||
|
|
||||||
package com.android.systemui.bouncer.ui.viewmodel
|
package com.android.systemui.bouncer.ui.viewmodel
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
sealed interface AuthMethodBouncerViewModel {
|
sealed class AuthMethodBouncerViewModel(
|
||||||
/**
|
/**
|
||||||
* Whether user input is enabled.
|
* Whether user input is enabled.
|
||||||
*
|
*
|
||||||
* If `false`, user input should be completely ignored in the UI as the user is "locked out" of
|
* If `false`, user input should be completely ignored in the UI as the user is "locked out" of
|
||||||
* being able to attempt to unlock the device.
|
* being able to attempt to unlock the device.
|
||||||
*/
|
*/
|
||||||
val isInputEnabled: StateFlow<Boolean>
|
val isInputEnabled: StateFlow<Boolean>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _animateFailure = MutableStateFlow(false)
|
||||||
|
/**
|
||||||
|
* Whether a failure animation should be shown. Once consumed, the UI must call
|
||||||
|
* [onFailureAnimationShown] to consume this state.
|
||||||
|
*/
|
||||||
|
val animateFailure: StateFlow<Boolean> = _animateFailure.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the failure animation has been shown. This should be called to consume a `true`
|
||||||
|
* value in [animateFailure].
|
||||||
|
*/
|
||||||
|
fun onFailureAnimationShown() {
|
||||||
|
_animateFailure.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ask the UI to show the failure animation. */
|
||||||
|
protected fun showFailureAnimation() {
|
||||||
|
_animateFailure.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
/** Holds UI state and handles user input for the password bouncer UI. */
|
/** Holds UI state and handles user input for the password bouncer UI. */
|
||||||
class PasswordBouncerViewModel(
|
class PasswordBouncerViewModel(
|
||||||
private val interactor: BouncerInteractor,
|
private val interactor: BouncerInteractor,
|
||||||
override val isInputEnabled: StateFlow<Boolean>,
|
isInputEnabled: StateFlow<Boolean>,
|
||||||
) : AuthMethodBouncerViewModel {
|
) :
|
||||||
|
AuthMethodBouncerViewModel(
|
||||||
|
isInputEnabled = isInputEnabled,
|
||||||
|
) {
|
||||||
|
|
||||||
private val _password = MutableStateFlow("")
|
private val _password = MutableStateFlow("")
|
||||||
/** The password entered so far. */
|
/** The password entered so far. */
|
||||||
@@ -47,7 +50,10 @@ class PasswordBouncerViewModel(
|
|||||||
|
|
||||||
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
|
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
|
||||||
fun onAuthenticateKeyPressed() {
|
fun onAuthenticateKeyPressed() {
|
||||||
interactor.authenticate(password.value.toCharArray().toList())
|
if (!interactor.authenticate(password.value.toCharArray().toList())) {
|
||||||
|
showFailureAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
_password.value = ""
|
_password.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ class PatternBouncerViewModel(
|
|||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
applicationScope: CoroutineScope,
|
applicationScope: CoroutineScope,
|
||||||
private val interactor: BouncerInteractor,
|
private val interactor: BouncerInteractor,
|
||||||
override val isInputEnabled: StateFlow<Boolean>,
|
isInputEnabled: StateFlow<Boolean>,
|
||||||
) : AuthMethodBouncerViewModel {
|
) :
|
||||||
|
AuthMethodBouncerViewModel(
|
||||||
|
isInputEnabled = isInputEnabled,
|
||||||
|
) {
|
||||||
|
|
||||||
/** The number of columns in the dot grid. */
|
/** The number of columns in the dot grid. */
|
||||||
val columnCount = 3
|
val columnCount = 3
|
||||||
@@ -150,7 +153,11 @@ class PatternBouncerViewModel(
|
|||||||
|
|
||||||
/** Notifies that the user has ended the drag gesture across the dot grid. */
|
/** Notifies that the user has ended the drag gesture across the dot grid. */
|
||||||
fun onDragEnd() {
|
fun onDragEnd() {
|
||||||
|
val isSuccessfullyAuthenticated =
|
||||||
interactor.authenticate(_selectedDots.value.map { it.toCoordinate() })
|
interactor.authenticate(_selectedDots.value.map { it.toCoordinate() })
|
||||||
|
if (!isSuccessfullyAuthenticated) {
|
||||||
|
showFailureAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
_dots.value = defaultDots()
|
_dots.value = defaultDots()
|
||||||
_currentDot.value = null
|
_currentDot.value = null
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ import kotlinx.coroutines.launch
|
|||||||
class PinBouncerViewModel(
|
class PinBouncerViewModel(
|
||||||
private val applicationScope: CoroutineScope,
|
private val applicationScope: CoroutineScope,
|
||||||
private val interactor: BouncerInteractor,
|
private val interactor: BouncerInteractor,
|
||||||
override val isInputEnabled: StateFlow<Boolean>,
|
isInputEnabled: StateFlow<Boolean>,
|
||||||
) : AuthMethodBouncerViewModel {
|
) :
|
||||||
|
AuthMethodBouncerViewModel(
|
||||||
|
isInputEnabled = isInputEnabled,
|
||||||
|
) {
|
||||||
|
|
||||||
private val entered = MutableStateFlow<List<Int>>(emptyList())
|
private val entered = MutableStateFlow<List<Int>>(emptyList())
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +95,10 @@ class PinBouncerViewModel(
|
|||||||
|
|
||||||
/** Notifies that the user clicked the "enter" button. */
|
/** Notifies that the user clicked the "enter" button. */
|
||||||
fun onAuthenticateButtonClicked() {
|
fun onAuthenticateButtonClicked() {
|
||||||
interactor.authenticate(entered.value)
|
if (!interactor.authenticate(entered.value)) {
|
||||||
|
showFailureAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
entered.value = emptyList()
|
entered.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
||||||
|
|
||||||
// Wrong input.
|
// Wrong input.
|
||||||
underTest.authenticate(listOf(9, 8, 7))
|
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
|
||||||
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
|
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
||||||
|
|
||||||
// Correct input.
|
// Correct input.
|
||||||
underTest.authenticate(listOf(1, 2, 3, 4))
|
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
||||||
|
|
||||||
// Wrong input.
|
// Wrong input.
|
||||||
underTest.authenticate("alohamora".toList())
|
assertThat(underTest.authenticate("alohamora".toList())).isFalse()
|
||||||
assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
|
assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
||||||
|
|
||||||
// Correct input.
|
// Correct input.
|
||||||
underTest.authenticate("password".toList())
|
assertThat(underTest.authenticate("password".toList())).isTrue()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +146,12 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
||||||
|
|
||||||
// Wrong input.
|
// Wrong input.
|
||||||
|
assertThat(
|
||||||
underTest.authenticate(
|
underTest.authenticate(
|
||||||
listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4))
|
listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4))
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.isFalse()
|
||||||
assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
|
assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
|
|
||||||
@@ -156,7 +159,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
||||||
|
|
||||||
// Correct input.
|
// Correct input.
|
||||||
underTest.authenticate(emptyList())
|
assertThat(underTest.authenticate(emptyList())).isTrue()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +217,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
|
repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
|
||||||
// Wrong PIN.
|
// Wrong PIN.
|
||||||
underTest.authenticate(listOf(6, 7, 8, 9))
|
assertThat(underTest.authenticate(listOf(6, 7, 8, 9))).isFalse()
|
||||||
if (times < BouncerInteractor.THROTTLE_EVERY - 1) {
|
if (times < BouncerInteractor.THROTTLE_EVERY - 1) {
|
||||||
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
|
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
|
||||||
}
|
}
|
||||||
@@ -223,7 +226,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
|
assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
|
||||||
|
|
||||||
// Correct PIN, but throttled, so doesn't unlock:
|
// Correct PIN, but throttled, so doesn't unlock:
|
||||||
underTest.authenticate(listOf(1, 2, 3, 4))
|
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isFalse()
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
|
assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
|
||||||
|
|
||||||
@@ -241,7 +244,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
|
||||||
// Correct PIN and no longer throttled so unlocks:
|
// Correct PIN and no longer throttled so unlocks:
|
||||||
underTest.authenticate(listOf(1, 2, 3, 4))
|
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
|
||||||
assertThat(isUnlocked).isTrue()
|
assertThat(isUnlocked).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.bouncer.ui.viewmodel
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||||
|
import com.android.systemui.coroutines.collectLastValue
|
||||||
|
import com.android.systemui.scene.SceneTestUtils
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class AuthMethodBouncerViewModelTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
private val testScope = TestScope()
|
||||||
|
private val utils = SceneTestUtils(this, testScope)
|
||||||
|
private val authenticationInteractor =
|
||||||
|
utils.authenticationInteractor(
|
||||||
|
utils.authenticationRepository(),
|
||||||
|
)
|
||||||
|
private val underTest =
|
||||||
|
PinBouncerViewModel(
|
||||||
|
applicationScope = testScope.backgroundScope,
|
||||||
|
interactor =
|
||||||
|
utils.bouncerInteractor(
|
||||||
|
authenticationInteractor = authenticationInteractor,
|
||||||
|
sceneInteractor = utils.sceneInteractor(),
|
||||||
|
),
|
||||||
|
isInputEnabled = MutableStateFlow(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun animateFailure() =
|
||||||
|
testScope.runTest {
|
||||||
|
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||||
|
val animateFailure by collectLastValue(underTest.animateFailure)
|
||||||
|
assertThat(animateFailure).isFalse()
|
||||||
|
|
||||||
|
// Wrong PIN:
|
||||||
|
underTest.onPinButtonClicked(3)
|
||||||
|
underTest.onPinButtonClicked(4)
|
||||||
|
underTest.onPinButtonClicked(5)
|
||||||
|
underTest.onPinButtonClicked(6)
|
||||||
|
underTest.onAuthenticateButtonClicked()
|
||||||
|
assertThat(animateFailure).isTrue()
|
||||||
|
|
||||||
|
underTest.onFailureAnimationShown()
|
||||||
|
assertThat(animateFailure).isFalse()
|
||||||
|
|
||||||
|
// Correct PIN:
|
||||||
|
underTest.onPinButtonClicked(1)
|
||||||
|
underTest.onPinButtonClicked(2)
|
||||||
|
underTest.onPinButtonClicked(3)
|
||||||
|
underTest.onPinButtonClicked(4)
|
||||||
|
underTest.onAuthenticateButtonClicked()
|
||||||
|
assertThat(animateFailure).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user