Loading packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +175 −92 Original line number Diff line number Diff line Loading @@ -21,11 +21,14 @@ package com.android.systemui.bouncer.ui.composable import android.view.HapticFeedbackConstants import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D 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.animateFloatAsState import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween Loading Loading @@ -58,6 +61,7 @@ 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.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalView Loading @@ -67,6 +71,7 @@ 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.ActionButtonAppearance import com.android.systemui.bouncer.ui.viewmodel.EnteredKey import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.common.shared.model.ContentDescription Loading @@ -76,6 +81,7 @@ import com.android.systemui.compose.modifiers.thenIf import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch Loading @@ -87,78 +93,13 @@ internal fun PinBouncer( // Report that the UI is shown to let the view-model run some logic. LaunchedEffect(Unit) { viewModel.onShown() } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() // Show the failure animation if the user entered the wrong input. LaunchedEffect(animateFailure) { if (animateFailure) { showFailureAnimation() viewModel.onFailureAnimationShown() } } Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { PinInputDisplay(viewModel) Spacer(Modifier.height(100.dp)) VerticalGrid( columns = 3, verticalSpacing = 12.dp, horizontalSpacing = 20.dp, ) { repeat(9) { index -> val digit = index + 1 PinButton( onClicked = { viewModel.onPinButtonClicked(digit) }, isEnabled = isInputEnabled, ) { contentColor -> PinDigit(digit, contentColor) } } PinButton( onClicked = { viewModel.onBackspaceButtonClicked() }, onLongPressed = { viewModel.onBackspaceButtonLongPressed() }, isEnabled = isInputEnabled, isIconButton = true, ) { contentColor -> PinIcon( Icon.Resource( res = R.drawable.ic_backspace_24dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_delete), ), contentColor, ) } PinButton( onClicked = { viewModel.onPinButtonClicked(0) }, isEnabled = isInputEnabled, ) { contentColor -> PinDigit(0, contentColor) } PinButton( onClicked = { viewModel.onAuthenticateButtonClicked() }, isEnabled = isInputEnabled, isIconButton = true, ) { contentColor -> PinIcon( Icon.Resource( res = R.drawable.ic_keyboard_tab_36dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_enter), ), contentColor, ) } } PinPad(viewModel) } } Loading Loading @@ -305,38 +246,153 @@ private fun ObscuredInputEntry(transition: Transition<EntryVisibility>) { } @Composable private fun PinDigit( private fun PinPad(viewModel: PinBouncerViewModel) { val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() val buttonScaleAnimatables = remember { List(12) { Animatable(1f) } } LaunchedEffect(animateFailure) { // Show the failure animation if the user entered the wrong input. if (animateFailure) { showFailureAnimation(buttonScaleAnimatables) viewModel.onFailureAnimationShown() } } VerticalGrid( columns = 3, verticalSpacing = 12.dp, horizontalSpacing = 20.dp, ) { repeat(9) { index -> DigitButton( index + 1, isInputEnabled, viewModel::onPinButtonClicked, buttonScaleAnimatables[index]::value, ) } ActionButton( icon = Icon.Resource( res = R.drawable.ic_backspace_24dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_delete), ), isInputEnabled = isInputEnabled, onClicked = viewModel::onBackspaceButtonClicked, onLongPressed = viewModel::onBackspaceButtonLongPressed, appearance = backspaceButtonAppearance, scaling = buttonScaleAnimatables[9]::value, ) DigitButton( 0, isInputEnabled, viewModel::onPinButtonClicked, buttonScaleAnimatables[10]::value, ) ActionButton( icon = Icon.Resource( res = R.drawable.ic_keyboard_tab_36dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_enter), ), isInputEnabled = isInputEnabled, onClicked = viewModel::onAuthenticateButtonClicked, appearance = confirmButtonAppearance, scaling = buttonScaleAnimatables[11]::value, ) } } @Composable private fun DigitButton( digit: Int, contentColor: Color, isInputEnabled: Boolean, onClicked: (Int) -> Unit, scaling: () -> Float, ) { // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes it // into Text, use that here, to animate more efficiently. PinPadButton( onClicked = { onClicked(digit) }, isEnabled = isInputEnabled, backgroundColor = MaterialTheme.colorScheme.surfaceVariant, foregroundColor = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.graphicsLayer { val scale = scaling() scaleX = scale scaleY = scale } ) { contentColor -> // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes // it into Text, use that here, to animate more efficiently. Text( text = digit.toString(), style = MaterialTheme.typography.headlineLarge, color = contentColor, color = contentColor(), ) } } @Composable private fun PinIcon( private fun ActionButton( icon: Icon, contentColor: Color, isInputEnabled: Boolean, onClicked: () -> Unit, onLongPressed: (() -> Unit)? = null, appearance: ActionButtonAppearance, scaling: () -> Float, ) { val isHidden = appearance == ActionButtonAppearance.Hidden val hiddenAlpha by animateFloatAsState(if (isHidden) 0f else 1f, label = "Action button alpha") val foregroundColor = when (appearance) { ActionButtonAppearance.Shown -> MaterialTheme.colorScheme.onSecondaryContainer else -> MaterialTheme.colorScheme.onSurface } val backgroundColor = when (appearance) { ActionButtonAppearance.Shown -> MaterialTheme.colorScheme.secondaryContainer else -> MaterialTheme.colorScheme.surface } PinPadButton( onClicked = onClicked, onLongPressed = onLongPressed, isEnabled = isInputEnabled && !isHidden, backgroundColor = backgroundColor, foregroundColor = foregroundColor, modifier = Modifier.graphicsLayer { alpha = hiddenAlpha val scale = scaling() scaleX = scale scaleY = scale } ) { contentColor -> Icon( icon = icon, tint = contentColor, tint = contentColor(), ) } } @Composable private fun PinButton( private fun PinPadButton( onClicked: () -> Unit, isEnabled: Boolean, backgroundColor: Color, foregroundColor: Color, modifier: Modifier = Modifier, onLongPressed: (() -> Unit)? = null, isIconButton: Boolean = false, content: @Composable (contentColor: Color) -> Unit, content: @Composable (contentColor: () -> Color) -> Unit, ) { var isPressed: Boolean by remember { mutableStateOf(false) } Loading Loading @@ -370,18 +426,16 @@ private fun PinButton( animateColorAsState( when { isPressed -> MaterialTheme.colorScheme.primary isIconButton -> MaterialTheme.colorScheme.secondaryContainer else -> MaterialTheme.colorScheme.surfaceVariant else -> backgroundColor }, label = "Pin button container color", animationSpec = colorAnimationSpec ) val contentColor: Color by val contentColor = animateColorAsState( when { isPressed -> MaterialTheme.colorScheme.onPrimary isIconButton -> MaterialTheme.colorScheme.onSecondaryContainer else -> MaterialTheme.colorScheme.onSurfaceVariant else -> foregroundColor }, label = "Pin button container color", animationSpec = colorAnimationSpec Loading Loading @@ -420,17 +474,46 @@ private fun PinButton( } }, ) { content(contentColor) content(contentColor::value) } } private fun showFailureAnimation() { // TODO(b/282730134): implement. private suspend fun showFailureAnimation( buttonScaleAnimatables: List<Animatable<Float, AnimationVector1D>> ) { coroutineScope { buttonScaleAnimatables.forEachIndexed { index, animatable -> launch { animatable.animateTo( targetValue = pinButtonErrorShrinkFactor, animationSpec = tween( durationMillis = pinButtonErrorShrinkMs, delayMillis = index * pinButtonErrorStaggerDelayMs, easing = Easings.Linear, ), ) animatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = pinButtonErrorRevertMs, easing = Easings.Legacy, ), ) } } } } private val entryShapeSize = 16.dp private val pinButtonSize = 84.dp private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize private const val pinButtonErrorShrinkMs = 50 private const val pinButtonErrorStaggerDelayMs = 33 private const val pinButtonErrorRevertMs = 617 // Pin button motion spec: http://shortn/_9TTIG6SoEa private val pinButtonPressedDuration = 100.milliseconds Loading packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +3 −1 Original line number Diff line number Diff line Loading @@ -77,7 +77,9 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow() private val _authenticationMethod = MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.Pin(1234)) MutableStateFlow<AuthenticationMethodModel>( AuthenticationMethodModel.Pin(listOf(1, 2, 3, 4), autoConfirm = false) ) override val authenticationMethod: StateFlow<AuthenticationMethodModel> = _authenticationMethod.asStateFlow() Loading packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +32 −14 Original line number Diff line number Diff line Loading @@ -122,14 +122,36 @@ constructor( /** * Attempts to authenticate the user and unlock the device. * * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method * supports auto-confirming, and the input's length is at least the code's length. Otherwise, * `null` is returned. * * @param input The input from the user to try to authenticate with. This can be a list of * different things, based on the current authentication method. * @return `true` if the authentication succeeded and the device is now unlocked; `false` * otherwise. * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit * request to validate. * @return `true` if the authentication succeeded and the device is now unlocked; `false` when * authentication failed, `null` if the check was not performed. */ fun authenticate(input: List<Any>): Boolean { fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? { val authMethod = this.authenticationMethod.value if (tryAutoConfirm) { if ((authMethod as? AuthenticationMethodModel.Pin)?.autoConfirm != true) { // Do not attempt to authenticate unless the PIN lock is set to auto-confirm. return null } if (input.size < authMethod.code.size) { // Do not attempt to authenticate if the PIN has not yet the required amount of // digits. This intentionally only skip for shorter PINs; if the PIN is longer, the // layer above might have throttled this check, and the PIN should be rejected via // the auth code below. return null } } val isSuccessful = when (val authMethod = this.authenticationMethod.value) { when (authMethod) { is AuthenticationMethodModel.Pin -> input.asCode() == authMethod.code is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates Loading Loading @@ -180,21 +202,17 @@ constructor( * Returns a PIN code from the given list. It's assumed the given list elements are all * [Int] in the range [0-9]. */ private fun List<Any>.asCode(): Long? { private fun List<Any>.asCode(): List<Int>? { if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) { return null } var code = 0L map { return 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 } /** Loading packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt +12 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.systemui.authentication.shared.model import androidx.annotation.VisibleForTesting /** Enumerates all known authentication methods. */ sealed class AuthenticationMethodModel( /** Loading @@ -38,7 +40,16 @@ sealed class AuthenticationMethodModel( * 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 Pin(val code: List<Int>, val autoConfirm: Boolean) : AuthenticationMethodModel(isSecure = true) { /** Convenience constructor for tests only. */ @VisibleForTesting constructor( code: Long, autoConfirm: Boolean = false ) : this(code.toString(10).map { it - '0' }, autoConfirm) {} } data class Password(val password: String) : AuthenticationMethodModel(isSecure = true) Loading packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +13 −4 Original line number Diff line number Diff line Loading @@ -149,19 +149,28 @@ constructor( * If the input is correct, the device will be unlocked and the lock screen and bouncer will be * dismissed and hidden. * * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method * supports auto-confirming, and the input's length is at least the code's length. Otherwise, * `null` is returned. * * @param input The input from the user to try to authenticate with. This can be a list of * different things, based on the current authentication method. * @return `true` if the authentication succeeded and the device is now unlocked; `false` * otherwise. * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit * request to validate. * @return `true` if the authentication succeeded and the device is now unlocked; `false` when * authentication failed, `null` if the check was not performed. */ fun authenticate( input: List<Any>, ): Boolean { tryAutoConfirm: Boolean = false, ): Boolean? { if (repository.throttling.value != null) { return false } val isAuthenticated = authenticationInteractor.authenticate(input) val isAuthenticated = authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value when { isAuthenticated -> { Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +175 −92 Original line number Diff line number Diff line Loading @@ -21,11 +21,14 @@ package com.android.systemui.bouncer.ui.composable import android.view.HapticFeedbackConstants import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D 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.animateFloatAsState import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween Loading Loading @@ -58,6 +61,7 @@ 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.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalView Loading @@ -67,6 +71,7 @@ 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.ActionButtonAppearance import com.android.systemui.bouncer.ui.viewmodel.EnteredKey import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.common.shared.model.ContentDescription Loading @@ -76,6 +81,7 @@ import com.android.systemui.compose.modifiers.thenIf import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch Loading @@ -87,78 +93,13 @@ internal fun PinBouncer( // Report that the UI is shown to let the view-model run some logic. LaunchedEffect(Unit) { viewModel.onShown() } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() // Show the failure animation if the user entered the wrong input. LaunchedEffect(animateFailure) { if (animateFailure) { showFailureAnimation() viewModel.onFailureAnimationShown() } } Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { PinInputDisplay(viewModel) Spacer(Modifier.height(100.dp)) VerticalGrid( columns = 3, verticalSpacing = 12.dp, horizontalSpacing = 20.dp, ) { repeat(9) { index -> val digit = index + 1 PinButton( onClicked = { viewModel.onPinButtonClicked(digit) }, isEnabled = isInputEnabled, ) { contentColor -> PinDigit(digit, contentColor) } } PinButton( onClicked = { viewModel.onBackspaceButtonClicked() }, onLongPressed = { viewModel.onBackspaceButtonLongPressed() }, isEnabled = isInputEnabled, isIconButton = true, ) { contentColor -> PinIcon( Icon.Resource( res = R.drawable.ic_backspace_24dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_delete), ), contentColor, ) } PinButton( onClicked = { viewModel.onPinButtonClicked(0) }, isEnabled = isInputEnabled, ) { contentColor -> PinDigit(0, contentColor) } PinButton( onClicked = { viewModel.onAuthenticateButtonClicked() }, isEnabled = isInputEnabled, isIconButton = true, ) { contentColor -> PinIcon( Icon.Resource( res = R.drawable.ic_keyboard_tab_36dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_enter), ), contentColor, ) } } PinPad(viewModel) } } Loading Loading @@ -305,38 +246,153 @@ private fun ObscuredInputEntry(transition: Transition<EntryVisibility>) { } @Composable private fun PinDigit( private fun PinPad(viewModel: PinBouncerViewModel) { val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() val buttonScaleAnimatables = remember { List(12) { Animatable(1f) } } LaunchedEffect(animateFailure) { // Show the failure animation if the user entered the wrong input. if (animateFailure) { showFailureAnimation(buttonScaleAnimatables) viewModel.onFailureAnimationShown() } } VerticalGrid( columns = 3, verticalSpacing = 12.dp, horizontalSpacing = 20.dp, ) { repeat(9) { index -> DigitButton( index + 1, isInputEnabled, viewModel::onPinButtonClicked, buttonScaleAnimatables[index]::value, ) } ActionButton( icon = Icon.Resource( res = R.drawable.ic_backspace_24dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_delete), ), isInputEnabled = isInputEnabled, onClicked = viewModel::onBackspaceButtonClicked, onLongPressed = viewModel::onBackspaceButtonLongPressed, appearance = backspaceButtonAppearance, scaling = buttonScaleAnimatables[9]::value, ) DigitButton( 0, isInputEnabled, viewModel::onPinButtonClicked, buttonScaleAnimatables[10]::value, ) ActionButton( icon = Icon.Resource( res = R.drawable.ic_keyboard_tab_36dp, contentDescription = ContentDescription.Resource(R.string.keyboardview_keycode_enter), ), isInputEnabled = isInputEnabled, onClicked = viewModel::onAuthenticateButtonClicked, appearance = confirmButtonAppearance, scaling = buttonScaleAnimatables[11]::value, ) } } @Composable private fun DigitButton( digit: Int, contentColor: Color, isInputEnabled: Boolean, onClicked: (Int) -> Unit, scaling: () -> Float, ) { // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes it // into Text, use that here, to animate more efficiently. PinPadButton( onClicked = { onClicked(digit) }, isEnabled = isInputEnabled, backgroundColor = MaterialTheme.colorScheme.surfaceVariant, foregroundColor = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.graphicsLayer { val scale = scaling() scaleX = scale scaleY = scale } ) { contentColor -> // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes // it into Text, use that here, to animate more efficiently. Text( text = digit.toString(), style = MaterialTheme.typography.headlineLarge, color = contentColor, color = contentColor(), ) } } @Composable private fun PinIcon( private fun ActionButton( icon: Icon, contentColor: Color, isInputEnabled: Boolean, onClicked: () -> Unit, onLongPressed: (() -> Unit)? = null, appearance: ActionButtonAppearance, scaling: () -> Float, ) { val isHidden = appearance == ActionButtonAppearance.Hidden val hiddenAlpha by animateFloatAsState(if (isHidden) 0f else 1f, label = "Action button alpha") val foregroundColor = when (appearance) { ActionButtonAppearance.Shown -> MaterialTheme.colorScheme.onSecondaryContainer else -> MaterialTheme.colorScheme.onSurface } val backgroundColor = when (appearance) { ActionButtonAppearance.Shown -> MaterialTheme.colorScheme.secondaryContainer else -> MaterialTheme.colorScheme.surface } PinPadButton( onClicked = onClicked, onLongPressed = onLongPressed, isEnabled = isInputEnabled && !isHidden, backgroundColor = backgroundColor, foregroundColor = foregroundColor, modifier = Modifier.graphicsLayer { alpha = hiddenAlpha val scale = scaling() scaleX = scale scaleY = scale } ) { contentColor -> Icon( icon = icon, tint = contentColor, tint = contentColor(), ) } } @Composable private fun PinButton( private fun PinPadButton( onClicked: () -> Unit, isEnabled: Boolean, backgroundColor: Color, foregroundColor: Color, modifier: Modifier = Modifier, onLongPressed: (() -> Unit)? = null, isIconButton: Boolean = false, content: @Composable (contentColor: Color) -> Unit, content: @Composable (contentColor: () -> Color) -> Unit, ) { var isPressed: Boolean by remember { mutableStateOf(false) } Loading Loading @@ -370,18 +426,16 @@ private fun PinButton( animateColorAsState( when { isPressed -> MaterialTheme.colorScheme.primary isIconButton -> MaterialTheme.colorScheme.secondaryContainer else -> MaterialTheme.colorScheme.surfaceVariant else -> backgroundColor }, label = "Pin button container color", animationSpec = colorAnimationSpec ) val contentColor: Color by val contentColor = animateColorAsState( when { isPressed -> MaterialTheme.colorScheme.onPrimary isIconButton -> MaterialTheme.colorScheme.onSecondaryContainer else -> MaterialTheme.colorScheme.onSurfaceVariant else -> foregroundColor }, label = "Pin button container color", animationSpec = colorAnimationSpec Loading Loading @@ -420,17 +474,46 @@ private fun PinButton( } }, ) { content(contentColor) content(contentColor::value) } } private fun showFailureAnimation() { // TODO(b/282730134): implement. private suspend fun showFailureAnimation( buttonScaleAnimatables: List<Animatable<Float, AnimationVector1D>> ) { coroutineScope { buttonScaleAnimatables.forEachIndexed { index, animatable -> launch { animatable.animateTo( targetValue = pinButtonErrorShrinkFactor, animationSpec = tween( durationMillis = pinButtonErrorShrinkMs, delayMillis = index * pinButtonErrorStaggerDelayMs, easing = Easings.Linear, ), ) animatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = pinButtonErrorRevertMs, easing = Easings.Legacy, ), ) } } } } private val entryShapeSize = 16.dp private val pinButtonSize = 84.dp private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize private const val pinButtonErrorShrinkMs = 50 private const val pinButtonErrorStaggerDelayMs = 33 private const val pinButtonErrorRevertMs = 617 // Pin button motion spec: http://shortn/_9TTIG6SoEa private val pinButtonPressedDuration = 100.milliseconds Loading
packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +3 −1 Original line number Diff line number Diff line Loading @@ -77,7 +77,9 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow() private val _authenticationMethod = MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.Pin(1234)) MutableStateFlow<AuthenticationMethodModel>( AuthenticationMethodModel.Pin(listOf(1, 2, 3, 4), autoConfirm = false) ) override val authenticationMethod: StateFlow<AuthenticationMethodModel> = _authenticationMethod.asStateFlow() Loading
packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +32 −14 Original line number Diff line number Diff line Loading @@ -122,14 +122,36 @@ constructor( /** * Attempts to authenticate the user and unlock the device. * * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method * supports auto-confirming, and the input's length is at least the code's length. Otherwise, * `null` is returned. * * @param input The input from the user to try to authenticate with. This can be a list of * different things, based on the current authentication method. * @return `true` if the authentication succeeded and the device is now unlocked; `false` * otherwise. * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit * request to validate. * @return `true` if the authentication succeeded and the device is now unlocked; `false` when * authentication failed, `null` if the check was not performed. */ fun authenticate(input: List<Any>): Boolean { fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? { val authMethod = this.authenticationMethod.value if (tryAutoConfirm) { if ((authMethod as? AuthenticationMethodModel.Pin)?.autoConfirm != true) { // Do not attempt to authenticate unless the PIN lock is set to auto-confirm. return null } if (input.size < authMethod.code.size) { // Do not attempt to authenticate if the PIN has not yet the required amount of // digits. This intentionally only skip for shorter PINs; if the PIN is longer, the // layer above might have throttled this check, and the PIN should be rejected via // the auth code below. return null } } val isSuccessful = when (val authMethod = this.authenticationMethod.value) { when (authMethod) { is AuthenticationMethodModel.Pin -> input.asCode() == authMethod.code is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates Loading Loading @@ -180,21 +202,17 @@ constructor( * Returns a PIN code from the given list. It's assumed the given list elements are all * [Int] in the range [0-9]. */ private fun List<Any>.asCode(): Long? { private fun List<Any>.asCode(): List<Int>? { if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) { return null } var code = 0L map { return 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 } /** Loading
packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt +12 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.systemui.authentication.shared.model import androidx.annotation.VisibleForTesting /** Enumerates all known authentication methods. */ sealed class AuthenticationMethodModel( /** Loading @@ -38,7 +40,16 @@ sealed class AuthenticationMethodModel( * 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 Pin(val code: List<Int>, val autoConfirm: Boolean) : AuthenticationMethodModel(isSecure = true) { /** Convenience constructor for tests only. */ @VisibleForTesting constructor( code: Long, autoConfirm: Boolean = false ) : this(code.toString(10).map { it - '0' }, autoConfirm) {} } data class Password(val password: String) : AuthenticationMethodModel(isSecure = true) Loading
packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +13 −4 Original line number Diff line number Diff line Loading @@ -149,19 +149,28 @@ constructor( * If the input is correct, the device will be unlocked and the lock screen and bouncer will be * dismissed and hidden. * * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method * supports auto-confirming, and the input's length is at least the code's length. Otherwise, * `null` is returned. * * @param input The input from the user to try to authenticate with. This can be a list of * different things, based on the current authentication method. * @return `true` if the authentication succeeded and the device is now unlocked; `false` * otherwise. * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit * request to validate. * @return `true` if the authentication succeeded and the device is now unlocked; `false` when * authentication failed, `null` if the check was not performed. */ fun authenticate( input: List<Any>, ): Boolean { tryAutoConfirm: Boolean = false, ): Boolean? { if (repository.throttling.value != null) { return false } val isAuthenticated = authenticationInteractor.authenticate(input) val isAuthenticated = authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value when { isAuthenticated -> { Loading