Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7a47e2df authored by Mike Schneider's avatar Mike Schneider Committed by Automerger Merge Worker
Browse files

Add support for auto-confirmed PINs am: da01955a

parents e980b328 da01955a
Loading
Loading
Loading
Loading
+103 −80
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ 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
@@ -58,6 +59,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
@@ -67,6 +69,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
@@ -87,7 +90,6 @@ 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.
@@ -103,62 +105,8 @@ internal fun PinBouncer(
        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)
    }
}

@@ -305,38 +253,115 @@ 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()

    VerticalGrid(
        columns = 3,
        verticalSpacing = 12.dp,
        horizontalSpacing = 20.dp,
    ) {
        repeat(9) { index -> DigitButton(index + 1, isInputEnabled, viewModel::onPinButtonClicked) }

        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,
        )

        DigitButton(0, isInputEnabled, viewModel::onPinButtonClicked)

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

@Composable
private fun DigitButton(
    digit: Int,
    contentColor: Color,
    isInputEnabled: Boolean,
    onClicked: (Int) -> Unit,
) {
    // 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,
    ) { 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,
) {
    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 }
    ) { 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) }

@@ -370,18 +395,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
@@ -420,7 +443,7 @@ private fun PinButton(
                    }
                },
    ) {
        content(contentColor)
        content(contentColor::value)
    }
}

+3 −1
Original line number Diff line number Diff line
@@ -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()

+32 −14
Original line number Diff line number Diff line
@@ -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
@@ -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
        }

        /**
+12 −1
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.authentication.shared.model

import androidx.annotation.VisibleForTesting

/** Enumerates all known authentication methods. */
sealed class AuthenticationMethodModel(
    /**
@@ -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)

+13 −4
Original line number Diff line number Diff line
@@ -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