Merge "[flexiglass] Bouncer throttling - view models." into udc-dev
This commit is contained in:
@@ -16,4 +16,14 @@
|
||||
|
||||
package com.android.systemui.bouncer.ui.viewmodel
|
||||
|
||||
sealed interface AuthMethodBouncerViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
sealed interface AuthMethodBouncerViewModel {
|
||||
/**
|
||||
* Whether user input is enabled.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
val isInputEnabled: StateFlow<Boolean>
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.systemui.bouncer.ui.viewmodel
|
||||
|
||||
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.dagger.qualifiers.Application
|
||||
@@ -24,10 +25,14 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Holds UI state and handles user input on bouncer UIs. */
|
||||
class BouncerViewModel
|
||||
@@ -40,16 +45,42 @@ 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 }
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = interactor.throttling.value == null,
|
||||
)
|
||||
|
||||
private val pin: PinBouncerViewModel by lazy {
|
||||
PinBouncerViewModel(
|
||||
applicationScope = applicationScope,
|
||||
interactor = interactor,
|
||||
isInputEnabled = isInputEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
private val password: PasswordBouncerViewModel by lazy {
|
||||
PasswordBouncerViewModel(
|
||||
interactor = interactor,
|
||||
isInputEnabled = isInputEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,6 +89,7 @@ constructor(
|
||||
applicationContext = applicationContext,
|
||||
applicationScope = applicationScope,
|
||||
interactor = interactor,
|
||||
isInputEnabled = isInputEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,11 +113,59 @@ constructor(
|
||||
initialValue = interactor.message.value ?: "",
|
||||
)
|
||||
|
||||
private val _throttlingDialogMessage = MutableStateFlow<String?>(null)
|
||||
/**
|
||||
* A message for a throttling dialog to show when the user has attempted the wrong credential
|
||||
* too many times and now must wait a while before attempting again.
|
||||
*
|
||||
* If `null`, no dialog should be shown.
|
||||
*
|
||||
* Once the dialog is shown, the UI should call [onThrottlingDialogDismissed] when the user
|
||||
* dismisses this dialog.
|
||||
*/
|
||||
val throttlingDialogMessage: StateFlow<String?> = _throttlingDialogMessage.asStateFlow()
|
||||
|
||||
init {
|
||||
applicationScope.launch {
|
||||
interactor.throttling
|
||||
.map { model ->
|
||||
model?.let {
|
||||
when (interactor.authenticationMethod.value) {
|
||||
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
|
||||
is AuthenticationMethodModel.Pattern ->
|
||||
R.string.kg_too_many_failed_pattern_attempts_dialog_message
|
||||
else -> null
|
||||
}?.let { stringResourceId ->
|
||||
applicationContext.getString(
|
||||
stringResourceId,
|
||||
model.failedAttemptCount,
|
||||
model.totalDurationSec,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { dialogMessageOrNull ->
|
||||
if (dialogMessageOrNull != null) {
|
||||
_throttlingDialogMessage.value = dialogMessageOrNull
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Notifies that the emergency services button was clicked. */
|
||||
fun onEmergencyServicesButtonClicked() {
|
||||
// TODO(b/280877228): implement this
|
||||
}
|
||||
|
||||
/** Notifies that a throttling dialog has been dismissed by the user. */
|
||||
fun onThrottlingDialogDismissed() {
|
||||
_throttlingDialogMessage.value = null
|
||||
}
|
||||
|
||||
private fun toViewModel(
|
||||
authMethod: AuthenticationMethodModel,
|
||||
): AuthMethodBouncerViewModel? {
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
/** Holds UI state and handles user input for the password bouncer UI. */
|
||||
class PasswordBouncerViewModel(
|
||||
private val interactor: BouncerInteractor,
|
||||
override val isInputEnabled: StateFlow<Boolean>,
|
||||
) : AuthMethodBouncerViewModel {
|
||||
|
||||
private val _password = MutableStateFlow("")
|
||||
|
||||
@@ -37,6 +37,7 @@ class PatternBouncerViewModel(
|
||||
private val applicationContext: Context,
|
||||
applicationScope: CoroutineScope,
|
||||
private val interactor: BouncerInteractor,
|
||||
override val isInputEnabled: StateFlow<Boolean>,
|
||||
) : AuthMethodBouncerViewModel {
|
||||
|
||||
/** The number of columns in the dot grid. */
|
||||
@@ -63,6 +64,16 @@ class PatternBouncerViewModel(
|
||||
/** All dots on the grid. */
|
||||
val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
|
||||
|
||||
/** Whether the pattern itself should be rendered visibly. */
|
||||
val isPatternVisible: StateFlow<Boolean> =
|
||||
interactor.authenticationMethod
|
||||
.map { authMethod -> isPatternVisible(authMethod) }
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = isPatternVisible(interactor.authenticationMethod.value),
|
||||
)
|
||||
|
||||
/** Notifies that the UI has been shown to the user. */
|
||||
fun onShown() {
|
||||
interactor.resetMessage()
|
||||
@@ -146,6 +157,10 @@ class PatternBouncerViewModel(
|
||||
_selectedDots.value = linkedSetOf()
|
||||
}
|
||||
|
||||
private fun isPatternVisible(authMethodModel: AuthenticationMethodModel): Boolean {
|
||||
return (authMethodModel as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false
|
||||
}
|
||||
|
||||
private fun defaultDots(): List<PatternDotViewModel> {
|
||||
return buildList {
|
||||
(0 until columnCount).forEach { x ->
|
||||
|
||||
@@ -33,6 +33,7 @@ import kotlinx.coroutines.launch
|
||||
class PinBouncerViewModel(
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val interactor: BouncerInteractor,
|
||||
override val isInputEnabled: StateFlow<Boolean>,
|
||||
) : AuthMethodBouncerViewModel {
|
||||
|
||||
private val entered = MutableStateFlow<List<Int>>(emptyList())
|
||||
|
||||
@@ -19,11 +19,15 @@ 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.bouncer.domain.interactor.BouncerInteractor
|
||||
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.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -40,13 +44,12 @@ class BouncerViewModelTest : SysuiTestCase() {
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val underTest =
|
||||
utils.bouncerViewModel(
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = utils.sceneInteractor(),
|
||||
)
|
||||
private val bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = utils.sceneInteractor(),
|
||||
)
|
||||
private val underTest = utils.bouncerViewModel(bouncerInteractor)
|
||||
|
||||
@Test
|
||||
fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
|
||||
@@ -89,6 +92,65 @@ class BouncerViewModelTest : SysuiTestCase() {
|
||||
.isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMessageUpdateAnimationsEnabled() =
|
||||
testScope.runTest {
|
||||
val isMessageUpdateAnimationsEnabled by
|
||||
collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
|
||||
val throttling by collectLastValue(bouncerInteractor.throttling)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
assertThat(isMessageUpdateAnimationsEnabled).isTrue()
|
||||
|
||||
repeat(BouncerInteractor.THROTTLE_EVERY) {
|
||||
// Wrong PIN.
|
||||
bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
|
||||
}
|
||||
assertThat(isMessageUpdateAnimationsEnabled).isFalse()
|
||||
|
||||
throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
|
||||
assertThat(isMessageUpdateAnimationsEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isInputEnabled() =
|
||||
testScope.runTest {
|
||||
val isInputEnabled by
|
||||
collectLastValue(
|
||||
underTest.authMethod.flatMapLatest { authViewModel ->
|
||||
authViewModel?.isInputEnabled ?: emptyFlow()
|
||||
}
|
||||
)
|
||||
val throttling by collectLastValue(bouncerInteractor.throttling)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
assertThat(isInputEnabled).isTrue()
|
||||
|
||||
repeat(BouncerInteractor.THROTTLE_EVERY) {
|
||||
// Wrong PIN.
|
||||
bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
|
||||
}
|
||||
assertThat(isInputEnabled).isFalse()
|
||||
|
||||
throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
|
||||
assertThat(isInputEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throttlingDialogMessage() =
|
||||
testScope.runTest {
|
||||
val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
|
||||
repeat(BouncerInteractor.THROTTLE_EVERY) {
|
||||
// Wrong PIN.
|
||||
assertThat(throttlingDialogMessage).isNull()
|
||||
bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
|
||||
}
|
||||
assertThat(throttlingDialogMessage).isNotEmpty()
|
||||
|
||||
underTest.onThrottlingDialogDismissed()
|
||||
assertThat(throttlingDialogMessage).isNull()
|
||||
}
|
||||
|
||||
private fun authMethodsToTest(): List<AuthenticationMethodModel> {
|
||||
return listOf(
|
||||
AuthenticationMethodModel.None,
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
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.runTest
|
||||
import org.junit.Before
|
||||
@@ -57,6 +59,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
||||
private val underTest =
|
||||
PasswordBouncerViewModel(
|
||||
interactor = bouncerInteractor,
|
||||
isInputEnabled = MutableStateFlow(true).asStateFlow(),
|
||||
)
|
||||
|
||||
@Before
|
||||
|
||||
@@ -27,6 +27,8 @@ import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
@@ -60,6 +62,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
||||
applicationContext = context,
|
||||
applicationScope = testScope.backgroundScope,
|
||||
interactor = bouncerInteractor,
|
||||
isInputEnabled = MutableStateFlow(true).asStateFlow(),
|
||||
)
|
||||
|
||||
@Before
|
||||
|
||||
@@ -27,6 +27,8 @@ import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
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
|
||||
@@ -68,6 +70,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
PinBouncerViewModel(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
interactor = bouncerInteractor,
|
||||
isInputEnabled = MutableStateFlow(true).asStateFlow(),
|
||||
)
|
||||
|
||||
@Before
|
||||
|
||||
Reference in New Issue
Block a user