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

Commit 1315dd96 authored by Ale Nijamkin's avatar Ale Nijamkin
Browse files

[flexiglass] Refactor scene switching logic to selectively hide overlays

When unlocking from the bouncer, if the shade is meant to stay open
(leaveOpenOnKeyguardHide is true), we should only dismiss the bouncer
and not all overlays. The previous logic always hid all overlays,
causing the attached bug (where the QS shade would be closed after
authenticating).

This change makes the device unlock logic only hide the shade or QS overlays if leaveOpen is false.

Fix: 434645851
Test: Manually verified by unlocking the device by tapping on the edit
button in the QS shade overlay or from a notification in the
notification shade overlay and confirming the shade remains visible
after unlocking
Flag: com.android.systemui.scene_container

Change-Id: Icede3c05d21fff5337890a52e54c2d003869c51a
parent f48b9369
Loading
Loading
Loading
Loading
+106 −37
Original line number Diff line number Diff line
@@ -54,7 +54,6 @@ import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.falsingCollector
import com.android.systemui.classifier.falsingManager
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryBypassRepository
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor
@@ -135,8 +134,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -427,16 +424,17 @@ class SceneContainerStartableTest : SysuiTestCase() {
        kosmos.runTest {
            val isVisible by collectLastValue(sceneInteractor.isVisible)

            val transitionState = prepareState(
                isDeviceUnlocked = true,
                initialSceneKey = Scenes.Lockscreen,
            )
            val transitionState =
                prepareState(isDeviceUnlocked = true, initialSceneKey = Scenes.Lockscreen)
            underTest.start()
            assertThat(isVisible).isTrue()

            // Unlock, leaving the surface behind animation running even after the transition ends.
            sceneInteractor.changeScene(
                Scenes.Gone, "unlocking for test", forceSettleToTargetScene = true)
                Scenes.Gone,
                "unlocking for test",
                forceSettleToTargetScene = true,
            )
            transitionState.value = ObservableTransitionState.Idle(Scenes.Gone)
            keyguardSurfaceBehindInteractor.setAnimatingSurface(true)
            runCurrent()
@@ -556,9 +554,74 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    fun switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade() =
    fun switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade_singleShade() =
        kosmos.runTest {
            enableSingleShade()
            switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade(
                switchToQs = {
                    sceneInteractor.changeScene(Scenes.QuickSettings, "switching to qs for test")
                    ObservableTransitionState.Idle(currentScene = Scenes.QuickSettings)
                },
                expectedSceneWhileBouncerIsShowing = Scenes.QuickSettings,
                expectedLastBackStackSceneWhileBouncerIsShowing = Scenes.Lockscreen,
                expectedSceneAfterUnlock = Scenes.QuickSettings,
                expectedOverlaysAfterUnlock = emptySet(),
                expectedLastBackStackSceneAfterUnlock = Scenes.Gone,
            )
        }

    @Test
    fun switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade_dualShade() =
        kosmos.runTest {
            enableDualShade()
            switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade(
                switchToQs = {
                    sceneInteractor.showOverlay(
                        Overlays.QuickSettingsShade,
                        "switching to qs for test",
                    )
                    ObservableTransitionState.Idle(
                        currentScene = Scenes.Lockscreen,
                        currentOverlays = setOf(Overlays.QuickSettingsShade),
                    )
                },
                expectedSceneWhileBouncerIsShowing = Scenes.Lockscreen,
                expectedLastBackStackSceneWhileBouncerIsShowing = null,
                expectedSceneAfterUnlock = Scenes.Gone,
                expectedOverlaysAfterUnlock = setOf(Overlays.QuickSettingsShade),
                expectedLastBackStackSceneAfterUnlock = null,
            )
        }

    /**
     * Runs through the scenario where the bouncer is accessed while QS is being shown and the
     * device gets unlocked. This is a helper that can help multiple scenarios.
     *
     * @param switchToQs A function that switches to the QS scene or overlay and returns the `Idle`
     *   representation of the expected current scene and overlays
     * @param expectedSceneWhileBouncerIsShowing The expected scene while the bouncer is showing.
     *   The helper function will check that the current _scene_ is this while the bouncer is
     *   showing
     * @param expectedLastBackStackSceneWhileBouncerIsShowing The expected last back stack scene
     *   while the bouncer is showing. The helper function will check that this is the last scene on
     *   the back stack while the bouncer is showing
     * @param expectedSceneAfterUnlock The expected scene once the device is unlocked. The helper
     *   function will check that this is the current _scene_ once the device is unlocked
     * @param expectedOverlaysAfterUnlock The expected overlays once the device is unlocked. The
     *   helper function will check that these are the current _overlays_ once the device is
     *   unlocked
     * @param expectedLastBackStackSceneAfterUnlock The expected last back stack scene after the
     *   device is unlocked. The helper function will check that this is the last scene on the back
     *   stack once the device is unlocked
     */
    private fun Kosmos.switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade(
        switchToQs: Kosmos.() -> ObservableTransitionState.Idle,
        expectedSceneWhileBouncerIsShowing: SceneKey,
        expectedLastBackStackSceneWhileBouncerIsShowing: SceneKey?,
        expectedSceneAfterUnlock: SceneKey,
        expectedOverlaysAfterUnlock: Set<OverlayKey>,
        expectedLastBackStackSceneAfterUnlock: SceneKey?,
    ) {
        val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
        val backStack by collectLastValue(sceneBackInteractor.backStack)
@@ -574,22 +637,28 @@ class SceneContainerStartableTest : SysuiTestCase() {
        underTest.start()
        runCurrent()

            sceneInteractor.changeScene(Scenes.QuickSettings, "switching to qs for test")
            transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings)
        val idleOnQs = switchToQs()
        transitionState.value = idleOnQs
        runCurrent()
            assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
        assertThat(currentSceneKey).isEqualTo(idleOnQs.currentScene)
        assertThat(currentOverlays).isEqualTo(idleOnQs.currentOverlays)

        sceneInteractor.showOverlay(Overlays.Bouncer, "showing bouncer for test")
        transitionState.value =
                ObservableTransitionState.Idle(Scenes.QuickSettings, setOf(Overlays.Bouncer))
            ObservableTransitionState.Idle(
                expectedSceneWhileBouncerIsShowing,
                setOf(Overlays.Bouncer),
            )
        runCurrent()
        assertThat(currentOverlays).contains(Overlays.Bouncer)
            assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Lockscreen)
        assertThat(backStack?.asIterable()?.lastOrNull())
            .isEqualTo(expectedLastBackStackSceneWhileBouncerIsShowing)

        updateFingerprintAuthStatus(isSuccess = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
            assertThat(currentOverlays).doesNotContain(Overlays.Bouncer)
            assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Gone)
        assertThat(currentSceneKey).isEqualTo(expectedSceneAfterUnlock)
        assertThat(currentOverlays).isEqualTo(expectedOverlaysAfterUnlock)
        assertThat(backStack?.asIterable()?.lastOrNull())
            .isEqualTo(expectedLastBackStackSceneAfterUnlock)
    }

    @Test
+106 −37
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInte
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
import com.android.systemui.kairos.internal.util.fastForEach
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -439,8 +440,8 @@ constructor(
                    initialValue = null,
                )
            deviceUnlockedInteractor.deviceUnlockStatus
                .mapNotNull { deviceUnlockStatus ->
                    val (renderedScenes: List<SceneKey>, renderedOverlays) =
                .map { deviceUnlockStatus ->
                    val (renderedScenes: List<SceneKey>, renderedOverlays: Set<OverlayKey>) =
                        when (val transitionState = sceneInteractor.transitionState.value) {
                            is ObservableTransitionState.Idle ->
                                listOf(transitionState.currentScene) to
@@ -451,31 +452,34 @@ constructor(
                            is ObservableTransitionState.Transition.OverlayTransition ->
                                listOf(transitionState.currentScene) to
                                    setOfNotNull(
                                        transitionState.toContent.takeIf { it is OverlayKey },
                                        transitionState.fromContent.takeIf { it is OverlayKey },
                                        transitionState.toContent as? OverlayKey,
                                        transitionState.fromContent as? OverlayKey,
                                    )
                        }
                    val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen)
                    val isAlternateBouncerVisible = alternateBouncerInteractor.isVisibleState()
                    val isOnPrimaryBouncer = Overlays.Bouncer in renderedOverlays
                    if (!deviceUnlockStatus.isUnlocked) {
                        return@mapNotNull if (
                        return@map if (
                            renderedScenes.any { it in keyguardScenes } ||
                                Overlays.Bouncer in renderedOverlays
                        ) {
                            // Already on a keyguard scene or bouncer, no need to change scenes.
                            null
                            SwitchSceneCommand.NoOp
                        } else {
                            // The device locked while on a scene that's not a keyguard scene, go
                            // to Lockscreen.
                            Scenes.Lockscreen to "device locked in a non-keyguard scene"
                            SwitchSceneCommand.SwitchToScene(
                                targetSceneKey = Scenes.Lockscreen,
                                loggingReason = "device locked in a non-keyguard scene",
                            )
                        }
                    }

                    if (powerInteractor.detailedWakefulness.value.isAsleep()) {
                        // The logic below is for when the device becomes unlocked. That must be a
                        // no-op if the device is not awake.
                        return@mapNotNull null
                        return@map SwitchSceneCommand.NoOp
                    }

                    if (
@@ -484,6 +488,7 @@ constructor(
                    ) {
                        uiEventLogger.log(BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS)
                    }
                    val leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
                    when {
                        isAlternateBouncerVisible -> {
                            // When the device becomes unlocked when the alternate bouncer is
@@ -491,16 +496,16 @@ constructor(
                            alternateBouncerInteractor.hide()

                            // ... and go to Gone or stay on the current scene
                            if (
                                isOnLockscreen ||
                                    !statusBarStateController.leaveOpenOnKeyguardHide()
                            ) {
                                Scenes.Gone to
                                    "device was unlocked with alternate bouncer showing" +
                                        " and shade didn't need to be left open"
                            if (isOnLockscreen || !leaveShadeOpen) {
                                SwitchSceneCommand.SwitchToScene(
                                    targetSceneKey = Scenes.Gone,
                                    loggingReason =
                                        "device was unlocked while alternate bouncer" +
                                            " was showing and shade didn't need to be left open",
                                )
                            } else {
                                sceneBackInteractor.replaceLockscreenSceneOnBackStack()
                                null
                                SwitchSceneCommand.NoOp
                            }
                        }
                        isOnPrimaryBouncer -> {
@@ -508,20 +513,40 @@ constructor(
                            // Gone or remain in the current scene. If transition is a scene change,
                            // take the destination scene.
                            val targetScene = renderedScenes.last()
                            if (
                                targetScene == Scenes.Lockscreen ||
                                    !statusBarStateController.leaveOpenOnKeyguardHide()
                            ) {
                                Scenes.Gone to
                                    "device was unlocked with bouncer showing and shade" +
                                        " didn't need to be left open"
                            if (targetScene == Scenes.Lockscreen || !leaveShadeOpen) {
                                val loggingReason = buildString {
                                    append(
                                        "device was unlocked while the primary bouncer was showing"
                                    )
                                    if (leaveShadeOpen) {
                                        append(" and shade needed to be left open")
                                    } else {
                                        append(" and shade didn't need to be left open")
                                    }
                                }
                                SwitchSceneCommand.SwitchToScene(
                                    targetSceneKey = Scenes.Gone,
                                    hideOverlays =
                                        if (leaveShadeOpen) {
                                            // Only hide the bouncer overlay, leaving any other
                                            // overlay (right now the only other overlays are
                                            // shades) visible.
                                            HideOverlayCommand.HideSome(Overlays.Bouncer)
                                        } else {
                                            HideOverlayCommand.HideAll
                                        },
                                    loggingReason = loggingReason,
                                )
                            } else {
                                if (previousScene.value != Scenes.Gone) {
                                    sceneBackInteractor.replaceLockscreenSceneOnBackStack()
                                }
                                targetScene to
                                    "device was unlocked with primary bouncer showing," +
                                        " from sceneKey=$targetScene"
                                SwitchSceneCommand.SwitchToScene(
                                    targetSceneKey = targetScene,
                                    loggingReason =
                                        "device was unlocked with primary bouncer" +
                                            " showing, from sceneKey=${targetScene.debugName}",
                                )
                            }
                        }
                        isOnLockscreen ->
@@ -536,23 +561,36 @@ constructor(
                            when {
                                deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen ==
                                    true ->
                                    Scenes.Gone to
                                        "device has been unlocked on lockscreen with bypass " +
                                            "enabled or using an active authentication " +
                                            "mechanism: ${deviceUnlockStatus.deviceUnlockSource}"
                                else -> null
                                    SwitchSceneCommand.SwitchToScene(
                                        targetSceneKey = Scenes.Gone,
                                        loggingReason =
                                            "device was unlocked while lockscreen" +
                                                " with bypass enabled or using an active" +
                                                " authentication mechanism:" +
                                                " ${deviceUnlockStatus.deviceUnlockSource}",
                                    )
                                else -> SwitchSceneCommand.NoOp
                            }
                        // Not on lockscreen or bouncer, so remain in the current scene but since
                        // unlocked, replace the Lockscreen scene from the bottom of the navigation
                        // back stack with the Gone scene.
                        else -> {
                            sceneBackInteractor.replaceLockscreenSceneOnBackStack()
                            null
                            SwitchSceneCommand.NoOp
                        }
                    }
                }
                .collect { command: SwitchSceneCommand ->
                    when (command) {
                        is SwitchSceneCommand.SwitchToScene -> {
                            switchToScene(
                                targetSceneKey = command.targetSceneKey,
                                hideOverlays = command.hideOverlays,
                                loggingReason = command.loggingReason,
                            )
                        }
                        is SwitchSceneCommand.NoOp -> Unit
                    }
                .collect { (targetSceneKey, loggingReason) ->
                    switchToScene(targetSceneKey = targetSceneKey, loggingReason = loggingReason)
                }
        }
    }
@@ -642,7 +680,12 @@ constructor(
                        switchToScene(
                            targetSceneKey = SceneFamilies.Home,
                            loggingReason = "dream stopped",
                            hideAllOverlays = deviceUnlockedInteractor.isUnlocked,
                            hideOverlays =
                                if (deviceUnlockedInteractor.isUnlocked) {
                                    HideOverlayCommand.HideAll
                                } else {
                                    HideOverlayCommand.HideNone
                                },
                        )
                    }
                }
@@ -985,14 +1028,20 @@ constructor(
        loggingReason: String,
        sceneState: Any? = null,
        freezeAndAnimateToCurrentState: Boolean = false,
        hideAllOverlays: Boolean = true,
        hideOverlays: HideOverlayCommand = HideOverlayCommand.HideAll,
    ) {
        if (hideOverlays is HideOverlayCommand.HideSome) {
            hideOverlays.overlays.fastForEach { overlay ->
                sceneInteractor.hideOverlay(overlay, loggingReason)
            }
        }

        sceneInteractor.changeScene(
            toScene = targetSceneKey,
            loggingReason = loggingReason,
            sceneState = sceneState,
            forceSettleToTargetScene = freezeAndAnimateToCurrentState,
            hideAllOverlays = hideAllOverlays,
            hideAllOverlays = hideOverlays == HideOverlayCommand.HideAll,
        )
    }

@@ -1085,6 +1134,26 @@ constructor(
        }
    }

    sealed interface SwitchSceneCommand {
        data object NoOp : SwitchSceneCommand

        data class SwitchToScene(
            val targetSceneKey: SceneKey,
            val loggingReason: String,
            val hideOverlays: HideOverlayCommand = HideOverlayCommand.HideAll,
        ) : SwitchSceneCommand
    }

    sealed interface HideOverlayCommand {
        data object HideAll : HideOverlayCommand

        data object HideNone : HideOverlayCommand

        class HideSome(val overlays: List<OverlayKey>) : HideOverlayCommand {
            constructor(overlay: OverlayKey) : this(listOf(overlay))
        }
    }

    companion object {
        private const val TAG = "SceneContainerStartable"
    }