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

Commit d12598e5 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Delay showing BouncerContents if faceAuth or activeUnlock can run" into main

parents 8a978f76 02f006e8
Loading
Loading
Loading
Loading
+100 −3
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import android.content.DialogInterface
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
@@ -40,6 +42,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -57,6 +60,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -80,6 +84,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -109,6 +114,8 @@ import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.fold.ui.composable.foldPosture
import com.android.systemui.fold.ui.helper.FoldPosture
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.composable.transitions.BOUNCER_INITIAL_TRANSLATION
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
@@ -117,7 +124,7 @@ import platform.test.motion.compose.values.MotionTestValues
import platform.test.motion.compose.values.motionTestValues

@Composable
fun BouncerContent(
fun ContentScope.BouncerContent(
    viewModel: BouncerOverlayContentViewModel,
    dialogFactory: BouncerDialogFactory,
    modifier: Modifier = Modifier,
@@ -125,12 +132,100 @@ fun BouncerContent(
    val isOneHandedModeSupported by viewModel.isOneHandedModeSupported.collectAsStateWithLifecycle()
    val layout = calculateLayout(isOneHandedModeSupported = isOneHandedModeSupported)

    BouncerContent(layout, viewModel, dialogFactory, modifier)
    fun isDraggingToBouncer(): Boolean {
        val currentTransition = layoutState.currentTransition
        return currentTransition != null &&
            currentTransition.isTransitioning(to = Overlays.Bouncer) &&
            currentTransition.gestureContext != null
    }

    // Custom handle the BouncerContent toBouncer transition here.
    // fromBouncer transitions are handled by the Scene transitions.

    // Give an extra delay for showing BouncerContent if face auth or active unlock may run.
    // This gives passive auth methods an opportunity to succeed before showing bouncer contents.
    val appearAnimationInterpolator = FastOutSlowInEasing
    val appearAnimationDuration = 250
    var appearAnimationDelay: Int by remember { mutableIntStateOf(0) }
    var startAppearAnimation: Boolean by remember { mutableStateOf(false) }
    val animatedAlpha: Float by
        animateFloatAsState(
            animationSpec =
                tween(
                    durationMillis = appearAnimationDuration,
                    delayMillis = appearAnimationDelay,
                    easing = appearAnimationInterpolator,
                ),
            targetValue =
                if (startAppearAnimation) {
                    1f
                } else {
                    // init alpha to 0f before anim begins
                    0f
                },
            label = "alpha",
        )

    val animatedOffsetY by
        animateDpAsState(
            animationSpec =
                tween(
                    durationMillis = appearAnimationDuration,
                    delayMillis = appearAnimationDelay,
                    easing = appearAnimationInterpolator,
                ),
            targetValue =
                if (startAppearAnimation) {
                    0.dp
                } else {
                    // init to BOUNCER_INITIAL_TRANSLATION before anim begins
                    BOUNCER_INITIAL_TRANSLATION
                },
            label = "offsetY",
        )

    LaunchedEffect(Unit) {
        appearAnimationDelay =
            BOUNCER_CONTENTS_PASSIVE_AUTH_DELAY.takeIf { viewModel.shouldDelayBouncerContent() }
                ?: 0
        startAppearAnimation = true
    }

    BouncerContent(
        layout,
        viewModel,
        dialogFactory,
        modifier =
            modifier
                .offset {
                    val yOffset =
                        if (isDraggingToBouncer()) {
                            ((1 -
                                    appearAnimationInterpolator.transform(
                                        layoutState.currentTransition!!.progress
                                    )) * BOUNCER_INITIAL_TRANSLATION)
                                .toPx()
                        } else {
                            animatedOffsetY.value
                        }
                    IntOffset(x = 0, y = yOffset.toInt())
                }
                .graphicsLayer {
                    alpha =
                        if (isDraggingToBouncer()) {
                            appearAnimationInterpolator.transform(
                                layoutState.currentTransition!!.progress
                            )
                        } else {
                            animatedAlpha
                        }
                },
    )
}

@Composable
@VisibleForTesting
fun BouncerContent(
fun ContentScope.BouncerContent(
    layout: BouncerOverlayLayout,
    viewModel: BouncerOverlayContentViewModel,
    dialogFactory: BouncerDialogFactory,
@@ -928,3 +1023,5 @@ private val SceneTransitions = transitions {
object BouncerMotionTestKeys {
    val swapAnimationEnd = MotionTestValueKey<Boolean>("swapAnimationEnd")
}

private const val BOUNCER_CONTENTS_PASSIVE_AUTH_DELAY = 500
+5 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import com.android.systemui.scene.ui.composable.transitions.dreamToBouncerTransi
import com.android.systemui.scene.ui.composable.transitions.dreamToCommunalTransition
import com.android.systemui.scene.ui.composable.transitions.dreamToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.dreamToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.fromBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
@@ -189,16 +190,19 @@ class SceneContainerTransitions : SceneContainerTransitionsBuilder {
            // Overlay transitions

            to(Overlays.Bouncer) { toBouncerTransition() }
            from(Overlays.Bouncer) { fromBouncerTransition() }
            from(Overlays.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() }
            from(Scenes.Dream, to = Overlays.Bouncer) { dreamToBouncerTransition() }
            from(Overlays.Bouncer, to = Scenes.Dream) { fromBouncerTransition() }
            from(Scenes.Lockscreen, to = Overlays.Bouncer) { lockscreenToBouncerTransition() }
            from(Overlays.Bouncer, to = Scenes.Lockscreen) { fromBouncerTransition() }
            from(
                Scenes.Lockscreen,
                to = Overlays.Bouncer,
                key = TransitionKey.PredictiveBack,
                reversePreview = { bouncerToLockscreenPreview() },
            ) {
                lockscreenToBouncerTransition()
                fromBouncerTransition()
            }
            from(Scenes.Communal, to = Overlays.Bouncer) { communalToBouncerTransition() }
            to(
+38 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.ui.composable.transitions

import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.TransitionBuilder
import com.android.compose.animation.scene.UserActionDistance
import com.android.systemui.bouncer.ui.composable.Bouncer

val BOUNCER_INITIAL_TRANSLATION = 300.dp

fun TransitionBuilder.fromBouncerTransition() {
    spec = tween(durationMillis = 500)

    distance = UserActionDistance { fromContent, _, _ ->
        val fromContentSize = checkNotNull(fromContent.targetSize())
        fromContentSize.height * TO_BOUNCER_SWIPE_DISTANCE_FRACTION
    }

    translate(Bouncer.Elements.Content, y = BOUNCER_INITIAL_TRANSLATION)
    fractionRange(end = TO_BOUNCER_FADE_FRACTION) { fade(Bouncer.Elements.Background) }
    fractionRange(start = TO_BOUNCER_FADE_FRACTION) { fade(Bouncer.Elements.Content) }
}
+3 −6
Original line number Diff line number Diff line
@@ -17,23 +17,20 @@
package com.android.systemui.scene.ui.composable.transitions

import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.TransitionBuilder
import com.android.compose.animation.scene.UserActionDistance
import com.android.systemui.bouncer.ui.composable.Bouncer

const val TO_BOUNCER_FADE_FRACTION = 0.5f
private const val TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f
const val TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f

fun TransitionBuilder.toBouncerTransition() {
    spec = tween(durationMillis = 500)
    spec = tween(durationMillis = 250)

    distance = UserActionDistance { fromContent, _, _ ->
        val fromContentSize = checkNotNull(fromContent.targetSize())
        fromContentSize.height * TO_BOUNCER_SWIPE_DISTANCE_FRACTION
    }

    translate(Bouncer.Elements.Content, y = 300.dp)
    fractionRange(end = TO_BOUNCER_FADE_FRACTION) { fade(Bouncer.Elements.Background) }
    fractionRange(start = TO_BOUNCER_FADE_FRACTION) { fade(Bouncer.Elements.Content) }
    fade(Bouncer.Elements.Background)
}
+41 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.fakeTrustRepository
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
@@ -89,6 +90,46 @@ class BouncerInteractorTest : SysuiTestCase() {
        overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
    }

    @Test
    fun simAuth_passiveAuthMaySucceedBeforeFullyShowingBouncer_false() =
        kosmos.runTest {
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Sim
            )
            assertThat(underTest.passiveAuthMaySucceedBeforeFullyShowingBouncer()).isFalse()
        }

    @Test
    fun noPassiveAuth_passiveAuthMaySucceedBeforeFullyShowingBouncer_false() =
        kosmos.runTest {
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pin
            )
            kosmos.fakeDeviceEntryFaceAuthRepository.canRunFaceAuth.value = false
            kosmos.fakeTrustRepository.setCurrentUserActiveUnlockAvailable(false)
            assertThat(underTest.passiveAuthMaySucceedBeforeFullyShowingBouncer()).isFalse()
        }

    @Test
    fun canRunFaceAuth_passiveAuthMaySucceedBeforeFullyShowingBouncer_true() =
        kosmos.runTest {
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pin
            )
            kosmos.fakeDeviceEntryFaceAuthRepository.canRunFaceAuth.value = true
            assertThat(underTest.passiveAuthMaySucceedBeforeFullyShowingBouncer()).isTrue()
        }

    @Test
    fun isCurrentUserActiveUnlockRunning_passiveAuthMaySucceedBeforeFullyShowingBouncer_true() =
        kosmos.runTest {
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pin
            )
            kosmos.fakeTrustRepository.setCurrentUserActiveUnlockAvailable(true)
            assertThat(underTest.passiveAuthMaySucceedBeforeFullyShowingBouncer()).isTrue()
        }

    @Test
    fun pinAuthMethod_sim_skipsAuthentication() =
        testScope.runTest {
Loading