From 8c50659a7a2d88b652f5efc9ffe8be030f7d1033 Mon Sep 17 00:00:00 2001 From: Mike Schneider Date: Fri, 26 May 2023 08:54:34 +0200 Subject: [PATCH] [flexiglass] Pin display polish. This aligns the implementation closer to the go/android-u-bouncer-motion spec - dots overshoot - removing digits collapses them together with the shift animation - clear all produces a staggered collapse effect Bug: 282730134 Test: Manual verification. Please see video capture attached to b/282730134. Test: Model unit tests Change-Id: Ic3c2a257b4cac0fa81b0365e7da07798b342847b --- .../com/android/compose/animation/Easings.kt | 15 ++ .../bouncer/ui/composable/PinBouncer.kt | 200 ++++++++++++++---- .../ui/viewmodel/PinBouncerViewModel.kt | 70 +++--- .../ui/viewmodel/PinBouncerViewModelTest.kt | 66 ++++-- 4 files changed, 248 insertions(+), 103 deletions(-) diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt index 5d09de167dc56..4fe9f89830f64 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt @@ -59,5 +59,20 @@ object Easings { /** The linear interpolator. */ val Linear = fromInterpolator(InterpolatorsAndroidX.LINEAR) + /** The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN. */ + val Legacy = fromInterpolator(InterpolatorsAndroidX.LEGACY) + + /** + * The default legacy accelerating interpolator as defined in Material 1. Also known as + * FAST_OUT_LINEAR_IN. + */ + val LegacyAccelerate = fromInterpolator(InterpolatorsAndroidX.LEGACY_ACCELERATE) + + /** + * T The default legacy decelerating interpolator as defined in Material 1. Also known as + * LINEAR_OUT_SLOW_IN. + */ + val LegacyDecelerate = fromInterpolator(InterpolatorsAndroidX.LEGACY_DECELERATE) + private fun fromInterpolator(source: Interpolator) = Easing { x -> source.getInterpolation(x) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index 20b859a6a242f..f801434999287 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -19,23 +19,19 @@ package com.android.systemui.bouncer.ui.composable import android.view.HapticFeedbackConstants -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.background +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -43,35 +39,40 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.Easings import com.android.compose.grid.VerticalGrid import com.android.systemui.R +import com.android.systemui.bouncer.ui.viewmodel.EnteredKey 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 import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit import kotlinx.coroutines.async @@ -86,8 +87,6 @@ internal fun PinBouncer( // Report that the UI is shown to let the view-model run some logic. LaunchedEffect(Unit) { viewModel.onShown() } - // The length of the PIN input received so far, so we know how many dots to render. - val pinLength: Pair by viewModel.pinLengths.collectAsState() val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() @@ -103,30 +102,7 @@ internal fun PinBouncer( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier.heightIn(min = 16.dp).animateContentSize(), - ) { - // TODO(b/281871687): add support for dot shapes. - val (previousPinLength, currentPinLength) = pinLength - val dotCount = max(previousPinLength, currentPinLength) + 1 - repeat(dotCount) { index -> - AnimatedVisibility( - visible = index < currentPinLength, - enter = fadeIn() + scaleIn() + slideInHorizontally(), - exit = fadeOut() + scaleOut() + slideOutHorizontally(), - ) { - Box( - modifier = - Modifier.size(16.dp) - .background( - MaterialTheme.colorScheme.onSurfaceVariant, - CircleShape, - ) - ) - } - } - } + PinInputDisplay(viewModel) Spacer(Modifier.height(100.dp)) @@ -186,6 +162,148 @@ internal fun PinBouncer( } } +@Composable +private fun PinInputDisplay(viewModel: PinBouncerViewModel) { + val currentPinEntries: List by viewModel.pinEntries.collectAsState() + + // visiblePinEntries keeps pins removed from currentPinEntries in the composition until their + // disappear-animation completed. The list is sorted by the natural ordering of EnteredKey, + // which is guaranteed to produce the original edit order, since the model only modifies entries + // at the end. + val visiblePinEntries = remember { SnapshotStateList() } + currentPinEntries.forEach { + val index = visiblePinEntries.binarySearch(it) + if (index < 0) { + val insertionPoint = -(index + 1) + visiblePinEntries.add(insertionPoint, it) + } + } + + Row( + modifier = + Modifier.heightIn(min = entryShapeSize) + // Pins overflowing horizontally should still be shown as scrolling. + .wrapContentSize(unbounded = true), + ) { + visiblePinEntries.forEachIndexed { index, entry -> + key(entry) { + val visibility = remember { + MutableTransitionState(EntryVisibility.Hidden) + } + visibility.targetState = + when { + currentPinEntries.isEmpty() && visiblePinEntries.size > 1 -> + EntryVisibility.BulkHidden(index, visiblePinEntries.size) + currentPinEntries.contains(entry) -> EntryVisibility.Shown + else -> EntryVisibility.Hidden + } + + ObscuredInputEntry(updateTransition(visibility, label = "Pin Entry $entry")) + + LaunchedEffect(entry) { + // Remove entry from visiblePinEntries once the hide transition completed. + snapshotFlow { + visibility.currentState == visibility.targetState && + visibility.targetState != EntryVisibility.Shown + } + .collect { isRemoved -> + if (isRemoved) { + visiblePinEntries.remove(entry) + } + } + } + } + } + } +} + +private sealed class EntryVisibility { + object Shown : EntryVisibility() + + object Hidden : EntryVisibility() + + /** + * Same as [Hidden], but applies when multiple entries are hidden simultaneously, without + * collapsing during the hide. + */ + data class BulkHidden(val staggerIndex: Int, val totalEntryCount: Int) : EntryVisibility() +} + +@Composable +private fun ObscuredInputEntry(transition: Transition) { + // spec: http://shortn/_DEhE3Xl2bi + val shapePadding = 6.dp + val shapeOvershootSize = 22.dp + val dismissStaggerDelayMs = 33 + val dismissDurationMs = 450 + val expansionDurationMs = 250 + val shapeExpandDurationMs = 83 + val shapeRetractDurationMs = 167 + val shapeCollapseDurationMs = 200 + + val animatedEntryWidth by + transition.animateDp( + transitionSpec = { + when (val target = targetState) { + is EntryVisibility.BulkHidden -> + // only collapse horizontal space once all entries are removed + snap(dismissDurationMs + dismissStaggerDelayMs * target.totalEntryCount) + else -> tween(expansionDurationMs, easing = Easings.Standard) + } + }, + label = "entry space" + ) { state -> + if (state == EntryVisibility.Shown) entryShapeSize + (shapePadding * 2) else 0.dp + } + + val animatedShapeSize by + transition.animateDp( + transitionSpec = { + when { + EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> + keyframes { + durationMillis = shapeExpandDurationMs + shapeRetractDurationMs + 0.dp at 0 with Easings.Linear + shapeOvershootSize at shapeExpandDurationMs with Easings.Legacy + } + targetState is EntryVisibility.BulkHidden -> { + val target = targetState as EntryVisibility.BulkHidden + tween( + dismissDurationMs, + delayMillis = target.staggerIndex * dismissStaggerDelayMs, + easing = Easings.Legacy, + ) + } + else -> tween(shapeCollapseDurationMs, easing = Easings.StandardDecelerate) + } + }, + label = "shape size" + ) { state -> + when (state) { + EntryVisibility.Shown -> entryShapeSize + else -> 0.dp + } + } + + val dotColor = MaterialTheme.colorScheme.onSurfaceVariant + Layout( + content = { + // TODO(b/282730134): add support for dot shapes. + Canvas(Modifier) { drawCircle(dotColor) } + } + ) { measurables, _ -> + val shapeSizePx = animatedShapeSize.roundToPx() + val placeable = measurables.single().measure(Constraints.fixed(shapeSizePx, shapeSizePx)) + + layout(animatedEntryWidth.roundToPx(), entryShapeSize.roundToPx()) { + placeable.place( + ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(), + ((entryShapeSize - animatedShapeSize) / 2f).roundToPx() + ) + } + } +} + @Composable private fun PinDigit( digit: Int, @@ -310,11 +428,13 @@ private fun showFailureAnimation() { // TODO(b/282730134): implement. } +private val entryShapeSize = 16.dp + private val pinButtonSize = 84.dp // Pin button motion spec: http://shortn/_9TTIG6SoEa private val pinButtonPressedDuration = 100.milliseconds -private val pinButtonPressedEasing = LinearEasing +private val pinButtonPressedEasing = Easings.Linear private val pinButtonHoldTime = 33.milliseconds private val pinButtonReleasedDuration = 420.milliseconds private val pinButtonReleasedEasing = Easings.Standard 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 5c0fd92e72997..94d3d193e9cc2 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 @@ -16,18 +16,10 @@ package com.android.systemui.bouncer.ui.viewmodel -import androidx.annotation.VisibleForTesting import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.util.kotlin.pairwise import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** Holds UI state and handles user input for the PIN code bouncer UI. */ class PinBouncerViewModel( @@ -39,21 +31,8 @@ class PinBouncerViewModel( isInputEnabled = isInputEnabled, ) { - private val entered = MutableStateFlow>(emptyList()) - /** - * The length of the PIN digits that were input so far, two values are supplied the previous and - * the current. - */ - val pinLengths: StateFlow> = - entered - .pairwise() - .map { it.previousValue.size to it.newValue.size } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = 0 to 0, - ) - private var resetPinJob: Job? = null + private val mutablePinEntries = MutableStateFlow>(emptyList()) + val pinEntries: StateFlow> = mutablePinEntries /** Notifies that the UI has been shown to the user. */ fun onShown() { @@ -62,47 +41,48 @@ class PinBouncerViewModel( /** Notifies that the user clicked on a PIN button with the given digit value. */ fun onPinButtonClicked(input: Int) { - resetPinJob?.cancel() - resetPinJob = null - - if (entered.value.isEmpty()) { + if (mutablePinEntries.value.isEmpty()) { interactor.clearMessage() } - entered.value += input + mutablePinEntries.value += EnteredKey(input) } /** Notifies that the user clicked the backspace button. */ fun onBackspaceButtonClicked() { - if (entered.value.isEmpty()) { + if (mutablePinEntries.value.isEmpty()) { return } - - entered.value = entered.value.toMutableList().apply { removeLast() } + mutablePinEntries.value = mutablePinEntries.value.toMutableList().apply { removeLast() } } /** Notifies that the user long-pressed the backspace button. */ fun onBackspaceButtonLongPressed() { - resetPinJob?.cancel() - resetPinJob = - applicationScope.launch { - while (entered.value.isNotEmpty()) { - onBackspaceButtonClicked() - delay(BACKSPACE_LONG_PRESS_DELAY_MS) - } - } + mutablePinEntries.value = emptyList() } /** Notifies that the user clicked the "enter" button. */ fun onAuthenticateButtonClicked() { - if (!interactor.authenticate(entered.value)) { + if (!interactor.authenticate(mutablePinEntries.value.map { it.input })) { showFailureAnimation() } - entered.value = emptyList() - } - - companion object { - @VisibleForTesting const val BACKSPACE_LONG_PRESS_DELAY_MS = 80L + mutablePinEntries.value = emptyList() } } + +private var nextSequenceNumber = 1 + +/** + * The pin bouncer [input] as digits 0-9, together with a [sequenceNumber] to indicate the ordering. + * + * Since the model only allows appending/removing [EnteredKey]s from the end, the [sequenceNumber] + * is strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at + * a specific number. + */ +data class EnteredKey +internal constructor(val input: Int, val sequenceNumber: Int = nextSequenceNumber++) : + Comparable { + override fun compareTo(other: EnteredKey): Int = + compareValuesBy(this, other, EnteredKey::sequenceNumber) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 3bdaf05908888..886cb703c0e89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -25,12 +25,12 @@ 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.Correspondence import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -85,7 +85,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) val message by collectLastValue(bouncerViewModel.message) - val pinLengths by collectLastValue(underTest.pinLengths) + val entries by collectLastValue(underTest.pinEntries) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) @@ -95,7 +95,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onShown() assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN) - assertThat(pinLengths).isEqualTo(0 to 0) + assertThat(entries).hasSize(0) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -106,7 +106,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) val message by collectLastValue(bouncerViewModel.message) - val pinLengths by collectLastValue(underTest.pinLengths) + val entries by collectLastValue(underTest.pinEntries) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) @@ -117,7 +117,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(1) assertThat(message?.text).isEmpty() - assertThat(pinLengths).isEqualTo(0 to 1) + assertThat(entries).hasSize(1) + assertThat(entries?.map { it.input }).containsExactly(1) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -128,7 +129,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) val message by collectLastValue(bouncerViewModel.message) - val pinLengths by collectLastValue(underTest.pinLengths) + val entries by collectLastValue(underTest.pinEntries) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) @@ -136,23 +137,50 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPinButtonClicked(1) - assertThat(pinLengths).isEqualTo(0 to 1) + assertThat(entries).hasSize(1) underTest.onBackspaceButtonClicked() assertThat(message?.text).isEmpty() - assertThat(pinLengths).isEqualTo(1 to 0) + assertThat(entries).hasSize(0) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } + @Test + fun onPinEdit() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val entries by collectLastValue(underTest.pinEntries) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + + underTest.onPinButtonClicked(1) + underTest.onPinButtonClicked(2) + underTest.onPinButtonClicked(3) + underTest.onBackspaceButtonClicked() + underTest.onBackspaceButtonClicked() + underTest.onPinButtonClicked(4) + underTest.onPinButtonClicked(5) + + assertThat(entries).hasSize(3) + assertThat(entries?.map { it.input }).containsExactly(1, 4, 5).inOrder() + assertThat(entries?.map { it.sequenceNumber }).isInStrictOrder() + } + @Test fun onBackspaceButtonLongPressed() = testScope.runTest { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) val message by collectLastValue(bouncerViewModel.message) - val pinLengths by collectLastValue(underTest.pinLengths) + val entries by collectLastValue(underTest.pinEntries) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) @@ -165,13 +193,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(4) underTest.onBackspaceButtonLongPressed() - repeat(4) { index -> - assertThat(pinLengths).isEqualTo(4 - index to 3 - index) - advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS) - } assertThat(message?.text).isEmpty() - assertThat(pinLengths).isEqualTo(1 to 0) + assertThat(entries).hasSize(0) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -204,7 +228,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) val message by collectLastValue(bouncerViewModel.message) - val pinLengths by collectLastValue(underTest.pinLengths) + val entries by collectLastValue(underTest.pinEntries) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) @@ -219,7 +243,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateButtonClicked() - assertThat(pinLengths).isEqualTo(0 to 0) + assertThat(entries).hasSize(0) assertThat(message?.text).isEqualTo(WRONG_PIN) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -231,7 +255,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) val message by collectLastValue(bouncerViewModel.message) - val pinLengths by collectLastValue(underTest.pinLengths) + val entries by collectLastValue(underTest.pinEntries) authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) @@ -245,7 +269,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(5) // PIN is now wrong! underTest.onAuthenticateButtonClicked() assertThat(message?.text).isEqualTo(WRONG_PIN) - assertThat(pinLengths).isEqualTo(0 to 0) + assertThat(entries).hasSize(0) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -266,5 +290,11 @@ class PinBouncerViewModelTest : SysuiTestCase() { private const val CONTAINER_NAME = "container1" private const val ENTER_YOUR_PIN = "Enter your pin" private const val WRONG_PIN = "Wrong pin" + + val KEY_CODE = + Correspondence.transforming( + { it?.input }, + "has a eventId of", + ) } }