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

Commit 02f006e8 authored by Beverly's avatar Beverly Committed by Beverly Tai
Browse files

Delay showing BouncerContents if faceAuth or activeUnlock can run

Bug: 411414026
Flag: com.android.systemui.scene_container
Test: atest BouncerContentTest
Test: atest BouncerInteractorTest
Test: manually enable scene container and enroll face auth,
observe bouncer delay (and no bouncer delay w/o face auth enrollment
or active unlock running)

Change-Id: Iee9d454493fb8a1c94ba5a9a68306bff9e21a21a
parent 334474cd
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