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

Commit cab720e5 authored by Mike Schneider's avatar Mike Schneider Committed by Android (Google) Code Review
Browse files

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.
parents af608153 cb71190b
Loading
Loading
Loading
Loading
+15 −0
Original line number Original line Diff line number Diff line
@@ -59,5 +59,20 @@ object Easings {
    /** The linear interpolator. */
    /** The linear interpolator. */
    val Linear = fromInterpolator(InterpolatorsAndroidX.LINEAR)
    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) }
    private fun fromInterpolator(source: Interpolator) = Easing { x -> source.getInterpolation(x) }
}
}
+160 −40
Original line number Original line Diff line number Diff line
@@ -19,23 +19,19 @@
package com.android.systemui.bouncer.ui.composable
package com.android.systemui.bouncer.ui.composable


import android.view.HapticFeedbackConstants
import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.AnimationSpec
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.animateDpAsState
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Canvas
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.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalView
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 androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid
import com.android.compose.grid.VerticalGrid
import com.android.systemui.R
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.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.thenIf
import com.android.systemui.compose.modifiers.thenIf
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlin.time.DurationUnit
import kotlinx.coroutines.async
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.
    // Report that the UI is shown to let the view-model run some logic.
    LaunchedEffect(Unit) { viewModel.onShown() }
    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 isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
    val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
    val animateFailure: Boolean by viewModel.animateFailure.collectAsState()


@@ -103,30 +102,7 @@ internal fun PinBouncer(
        horizontalAlignment = Alignment.CenterHorizontally,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = modifier,
        modifier = modifier,
    ) {
    ) {
        Row(
        PinInputDisplay(viewModel)
            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,
                                )
                    )
                }
            }
        }


        Spacer(Modifier.height(100.dp))
        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
@Composable
private fun PinDigit(
private fun PinDigit(
    digit: Int,
    digit: Int,
@@ -310,11 +428,13 @@ private fun showFailureAnimation() {
    // TODO(b/282730134): implement.
    // TODO(b/282730134): implement.
}
}


private val entryShapeSize = 16.dp

private val pinButtonSize = 84.dp
private val pinButtonSize = 84.dp


// Pin button motion spec: http://shortn/_9TTIG6SoEa
// Pin button motion spec: http://shortn/_9TTIG6SoEa
private val pinButtonPressedDuration = 100.milliseconds
private val pinButtonPressedDuration = 100.milliseconds
private val pinButtonPressedEasing = LinearEasing
private val pinButtonPressedEasing = Easings.Linear
private val pinButtonHoldTime = 33.milliseconds
private val pinButtonHoldTime = 33.milliseconds
private val pinButtonReleasedDuration = 420.milliseconds
private val pinButtonReleasedDuration = 420.milliseconds
private val pinButtonReleasedEasing = Easings.Standard
private val pinButtonReleasedEasing = Easings.Standard
+1 −1
Original line number Original line Diff line number Diff line
@@ -77,7 +77,7 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit
    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()


    private val _authenticationMethod =
    private val _authenticationMethod =
        MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
        MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.Pin(1234))
    override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
    override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
        _authenticationMethod.asStateFlow()
        _authenticationMethod.asStateFlow()


+13 −6
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.systemui.authentication.domain.interactor
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.data.repository.AuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
@@ -129,7 +130,7 @@ constructor(
    fun authenticate(input: List<Any>): Boolean {
    fun authenticate(input: List<Any>): Boolean {
        val isSuccessful =
        val isSuccessful =
            when (val authMethod = this.authenticationMethod.value) {
            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.Password -> input.asPassword() == authMethod.password
                is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
                is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
                else -> true
                else -> true
@@ -177,15 +178,21 @@ constructor(


        /**
        /**
         * Returns a PIN code from the given list. It's assumed the given list elements are all
         * 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? {
        private fun List<Any>.asCode(): Long? {
            if (isEmpty()) {
            if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) {
                return null
                return null
            }
            }


            var code = 0
            var code = 0L
            map { it as Int }.forEach { integer -> code = code * 10 + integer }
            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
            return code
        }
        }
+7 −1
Original line number Original line Diff line number Diff line
@@ -32,7 +32,13 @@ sealed class AuthenticationMethodModel(
    /** The most basic authentication method. The lock screen can be swiped away when displayed. */
    /** The most basic authentication method. The lock screen can be swiped away when displayed. */
    object Swipe : AuthenticationMethodModel(isSecure = false)
    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)
    data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)


Loading