Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +11 −1 Original line number Diff line number Diff line Loading @@ -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> } packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +80 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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, ) } Loading @@ -58,6 +89,7 @@ constructor( applicationContext = applicationContext, applicationScope = applicationScope, interactor = interactor, isInputEnabled = isInputEnabled, ) } Loading @@ -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? { Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +1 −0 Original line number Diff line number Diff line Loading @@ -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("") Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +15 −0 Original line number Diff line number Diff line Loading @@ -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. */ Loading @@ -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() Loading Loading @@ -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 -> Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +1 −0 Original line number Diff line number Diff line Loading @@ -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()) Loading Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +11 −1 Original line number Diff line number Diff line Loading @@ -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> }
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +80 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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, ) } Loading @@ -58,6 +89,7 @@ constructor( applicationContext = applicationContext, applicationScope = applicationScope, interactor = interactor, isInputEnabled = isInputEnabled, ) } Loading @@ -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? { Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +1 −0 Original line number Diff line number Diff line Loading @@ -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("") Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +15 −0 Original line number Diff line number Diff line Loading @@ -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. */ Loading @@ -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() Loading Loading @@ -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 -> Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +1 −0 Original line number Diff line number Diff line Loading @@ -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()) Loading