Merge changes from topics "naming", "pinlength" into udc-qpr-dev

* changes:
  Make sure pins up to the max length of 16 digits are supported.
  Fix naming of Pin per style guide https://developer.android.com/kotlin/style-guide#naming_2
  [flexiglass] Pin display polish.
This commit is contained in:
Mike Schneider
2023-05-30 19:58:09 +00:00
committed by Android (Google) Code Review
17 changed files with 351 additions and 147 deletions

View File

@@ -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) }
}

View File

@@ -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<Int, Int> 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<EnteredKey> 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<EnteredKey>() }
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>(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<EntryVisibility>) {
// 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

View File

@@ -77,7 +77,7 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit
override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
private val _authenticationMethod =
MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.Pin(1234))
override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
_authenticationMethod.asStateFlow()

View File

@@ -16,6 +16,7 @@
package com.android.systemui.authentication.domain.interactor
import android.app.admin.DevicePolicyManager
import com.android.systemui.authentication.data.repository.AuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.dagger.SysUISingleton
@@ -129,7 +130,7 @@ constructor(
fun authenticate(input: List<Any>): Boolean {
val isSuccessful =
when (val authMethod = this.authenticationMethod.value) {
is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code
is AuthenticationMethodModel.Pin -> input.asCode() == authMethod.code
is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password
is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
else -> true
@@ -177,15 +178,21 @@ constructor(
/**
* Returns a PIN code from the given list. It's assumed the given list elements are all
* [Int].
* [Int] in the range [0-9].
*/
private fun List<Any>.asCode(): Int? {
if (isEmpty()) {
private fun List<Any>.asCode(): Long? {
if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) {
return null
}
var code = 0
map { it as Int }.forEach { integer -> code = code * 10 + integer }
var code = 0L
map {
require(it is Int && it in 0..9) {
"Pin is required to be Int in range [0..9], but got $it"
}
it
}
.forEach { integer -> code = code * 10 + integer }
return code
}

View File

@@ -32,7 +32,13 @@ sealed class AuthenticationMethodModel(
/** The most basic authentication method. The lock screen can be swiped away when displayed. */
object Swipe : AuthenticationMethodModel(isSecure = false)
data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true)
/**
* Authentication method using a PIN.
*
* In practice, a pin is restricted to 16 decimal digits , see
* [android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH]
*/
data class Pin(val code: Long) : AuthenticationMethodModel(isSecure = true)
data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)

View File

@@ -197,7 +197,7 @@ constructor(
private fun promptMessage(authMethod: AuthenticationMethodModel): String {
return when (authMethod) {
is AuthenticationMethodModel.PIN ->
is AuthenticationMethodModel.Pin ->
applicationContext.getString(R.string.keyguard_enter_your_pin)
is AuthenticationMethodModel.Password ->
applicationContext.getString(R.string.keyguard_enter_your_password)
@@ -209,7 +209,7 @@ constructor(
private fun errorMessage(authMethod: AuthenticationMethodModel): String {
return when (authMethod) {
is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin)
is AuthenticationMethodModel.Pin -> applicationContext.getString(R.string.kg_wrong_pin)
is AuthenticationMethodModel.Password ->
applicationContext.getString(R.string.kg_wrong_password)
is AuthenticationMethodModel.Pattern ->

View File

@@ -126,7 +126,7 @@ constructor(
.map { model ->
model?.let {
when (interactor.authenticationMethod.value) {
is AuthenticationMethodModel.PIN ->
is AuthenticationMethodModel.Pin ->
R.string.kg_too_many_failed_pin_attempts_dialog_message
is AuthenticationMethodModel.Password ->
R.string.kg_too_many_failed_password_attempts_dialog_message
@@ -165,7 +165,7 @@ constructor(
authMethod: AuthenticationMethodModel,
): AuthMethodBouncerViewModel? {
return when (authMethod) {
is AuthenticationMethodModel.PIN -> pin
is AuthenticationMethodModel.Pin -> pin
is AuthenticationMethodModel.Password -> password
is AuthenticationMethodModel.Pattern -> pattern
else -> null

View File

@@ -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<List<Int>>(emptyList())
/**
* The length of the PIN digits that were input so far, two values are supplied the previous and
* the current.
*/
val pinLengths: StateFlow<Pair<Int, Int>> =
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<List<EnteredKey>>(emptyList())
val pinEntries: StateFlow<List<EnteredKey>> = 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<EnteredKey> {
override fun compareTo(other: EnteredKey): Int =
compareValuesBy(this, other, EnteredKey::sequenceNumber)
}

View File

@@ -16,6 +16,7 @@
package com.android.systemui.authentication.domain.interactor
import android.app.admin.DevicePolicyManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.AuthenticationRepository
@@ -48,7 +49,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun authMethod() =
testScope.runTest {
val authMethod by collectLastValue(underTest.authenticationMethod)
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234))
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin(1234))
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password"))
@@ -147,7 +148,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
@@ -160,7 +161,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
@@ -168,6 +169,51 @@ class AuthenticationInteractorTest : SysuiTestCase() {
assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withEmptyPin_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf())).isFalse()
assertThat(isUnlocked).isFalse()
assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withCorrectMaxLengthPin_returnsTrueAndUnlocksDevice() =
testScope.runTest {
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(9999999999999999))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(List(16) { 9 })).isTrue()
assertThat(isUnlocked).isTrue()
assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withCorrectTooLongPin_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
// Max pin length is 16 digits. To avoid issues with overflows, this test ensures
// that all pins > 16 decimal digits are rejected.
// If the policy changes, there is work to do in SysUI.
assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17)
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(99999999999999999))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(List(17) { 9 })).isFalse()
assertThat(isUnlocked).isFalse()
assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
testScope.runTest {

View File

@@ -69,7 +69,7 @@ class BouncerInteractorTest : SysuiTestCase() {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
val message by collectLastValue(underTest.message)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
underTest.showOrUnlockDevice("container1")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -167,7 +167,7 @@ class BouncerInteractorTest : SysuiTestCase() {
fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.unlockDevice()
runCurrent()
@@ -211,7 +211,7 @@ class BouncerInteractorTest : SysuiTestCase() {
val throttling by collectLastValue(underTest.throttling)
val message by collectLastValue(underTest.message)
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(throttling).isNull()
assertThat(message).isEqualTo("")
assertThat(isUnlocked).isFalse()

View File

@@ -55,7 +55,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() {
@Test
fun animateFailure() =
testScope.runTest {
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
val animateFailure by collectLastValue(underTest.animateFailure)
assertThat(animateFailure).isFalse()

View File

@@ -97,7 +97,7 @@ class BouncerViewModelTest : SysuiTestCase() {
testScope.runTest {
val message by collectLastValue(underTest.message)
val throttling by collectLastValue(bouncerInteractor.throttling)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(message?.isUpdateAnimated).isTrue()
repeat(BouncerInteractor.THROTTLE_EVERY) {
@@ -120,7 +120,7 @@ class BouncerViewModelTest : SysuiTestCase() {
}
)
val throttling by collectLastValue(bouncerInteractor.throttling)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(isInputEnabled).isTrue()
repeat(BouncerInteractor.THROTTLE_EVERY) {
@@ -137,7 +137,7 @@ class BouncerViewModelTest : SysuiTestCase() {
fun throttlingDialogMessage() =
testScope.runTest {
val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
repeat(BouncerInteractor.THROTTLE_EVERY) {
// Wrong PIN.
@@ -154,7 +154,7 @@ class BouncerViewModelTest : SysuiTestCase() {
return listOf(
AuthenticationMethodModel.None,
AuthenticationMethodModel.Swipe,
AuthenticationMethodModel.PIN(1234),
AuthenticationMethodModel.Pin(1234),
AuthenticationMethodModel.Password("password"),
AuthenticationMethodModel.Pattern(
listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1))

View File

@@ -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,8 +85,8 @@ 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)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
val entries by collectLastValue(underTest.pinEntries)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
assertThat(isUnlocked).isFalse()
@@ -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,8 +106,8 @@ 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)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
val entries by collectLastValue(underTest.pinEntries)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
assertThat(isUnlocked).isFalse()
@@ -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,32 +129,59 @@ 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)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
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)
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)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
val entries by collectLastValue(underTest.pinEntries)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
assertThat(isUnlocked).isFalse()
@@ -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))
}
@@ -181,7 +205,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
testScope.runTest {
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
assertThat(isUnlocked).isFalse()
@@ -204,8 +228,8 @@ 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)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
val entries by collectLastValue(underTest.pinEntries)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
assertThat(isUnlocked).isFalse()
@@ -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,8 +255,8 @@ 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)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
val entries by collectLastValue(underTest.pinEntries)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
assertThat(isUnlocked).isFalse()
@@ -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<EnteredKey, Int>(
{ it?.input },
"has a eventId of",
)
}
}

View File

@@ -95,7 +95,7 @@ class LockscreenSceneInteractorTest : SysuiTestCase() {
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.lockDevice()
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
underTest.dismissLockscreen()
@@ -108,7 +108,7 @@ class LockscreenSceneInteractorTest : SysuiTestCase() {
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.unlockDevice()
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
underTest.dismissLockscreen()
@@ -195,7 +195,7 @@ class LockscreenSceneInteractorTest : SysuiTestCase() {
testScope.runTest {
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
assertThat(isUnlocked).isFalse()
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))

View File

@@ -109,7 +109,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
@@ -119,7 +119,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
runCurrent()
@@ -132,7 +132,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.unlockDevice()
runCurrent()
@@ -145,7 +145,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
runCurrent()
@@ -158,7 +158,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.unlockDevice()
runCurrent()

View File

@@ -70,7 +70,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.unlockDevice()
runCurrent()
@@ -83,7 +83,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
runCurrent()

View File

@@ -71,7 +71,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
fun upTransitionSceneKey_deviceLocked_lockScreen() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen)
@@ -81,7 +81,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
fun upTransitionSceneKey_deviceUnlocked_gone() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.unlockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
@@ -91,7 +91,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.unlockDevice()
runCurrent()
@@ -104,7 +104,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234))
authenticationInteractor.lockDevice()
runCurrent()