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

Commit d907df2d authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Fixes WindowManagerLockscreenVisibilityInteractor.lockscreenVisibility

lockscreenVisibility was emitting a false when the device would get
unlocked while the user is idle on the shade scene. This caused the
shade to unnecessarily and incorrectly be collapsed.

The code responsible for that collapse of the shade is in
ShadeControllerSceneImpl.instantCollapseShade and it's triggered by
logic in StatusBarKeyguardViewManager.consumeShowStatusBarKeyguardView
that decides to hide the keyguard in response to its visibility being
false. This is based on the lockscreenVisibility flow that was emitting
a false at the wrong time.

The reason that false was being emitted was because isDeviceEntered was
emitting true because, even though we never leave the Shade scene, the
Lockscreen scene that's at the bottom of the navigation back stack is
actually getting replaced by a Gone scene when the device becomes
unlocked.

The solution was to separate the "directly" part of isDeviceEntered to
its own flow and make
WindowManagerLockscreenVisibilityInteractor.lockscreenVisibility depend
on that instead.

There were also some shenanigans in
WindowManagerLockscreenVisibilityInteractorTest where the transition
state from a previous test case was lingering and causing the next test
case to start from a random transition state. This CL cleans that up as
well.

Fix: 379838790
Test: unit test coverage added and expanded
Test: manually verified that face unlocking while looking at the shade
no longer collapses the shade
Test: manually verified that face or fingerprint unlocking after
pressing on a reply button in a notification on the shade correctly
stays on the shade, moves focus back to the reply text field of the
notification, and that, if the shade is then manually collapsed, the
launcher is shown; matching legacy behaviour
Flag: com.android.systemui.scene_container

Change-Id: I3eb158dfe7cd4c1ae8e96430ad5d7ab0bb756a2a
parent e3f4692e
Loading
Loading
Loading
Loading
+6 −5
Original line number Diff line number Diff line
@@ -43,7 +43,6 @@ import com.android.systemui.keyguard.data.repository.fakeTrustRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneBackInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.Scenes
@@ -72,7 +71,6 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
    private val trustRepository by lazy { kosmos.fakeTrustRepository }
    private val sceneInteractor by lazy { kosmos.sceneInteractor }
    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
    private val sceneBackInteractor by lazy { kosmos.sceneBackInteractor }
    private val sceneContainerStartable by lazy { kosmos.sceneContainerStartable }
    private val sysuiStatusBarStateController by lazy { kosmos.sysuiStatusBarStateController }
    private lateinit var underTest: DeviceEntryInteractor
@@ -437,7 +435,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
    fun isDeviceEntered_unlockedWhileOnShade_emitsTrue() =
        testScope.runTest {
            val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
            val isDeviceEnteredDirectly by collectLastValue(underTest.isDeviceEnteredDirectly)
            assertThat(isDeviceEntered).isFalse()
            assertThat(isDeviceEnteredDirectly).isFalse()
            val currentScene by collectLastValue(sceneInteractor.currentScene)
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)

@@ -445,19 +445,20 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
            switchToScene(Scenes.Shade)
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            // Simulating a "leave it open when the keyguard is hidden" which means the bouncer will
            // be
            // shown and successful authentication should take the user back to where they are, the
            // shade scene.
            // be shown and successful authentication should take the user back to where they are,
            // the shade scene.
            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
            switchToScene(Scenes.Bouncer)
            assertThat(currentScene).isEqualTo(Scenes.Bouncer)

            assertThat(isDeviceEntered).isFalse()
            assertThat(isDeviceEnteredDirectly).isFalse()
            // Authenticate with PIN to unlock and dismiss the lockscreen:
            authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
            runCurrent()

            assertThat(isDeviceEntered).isTrue()
            assertThat(isDeviceEnteredDirectly).isFalse()
        }

    private fun TestScope.switchToScene(sceneKey: SceneKey) {
+51 −12
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.systemui.authentication.data.repository.FakeAuthenticationRep
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
@@ -34,10 +35,12 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.model.asIterable
import com.android.systemui.scene.data.model.sceneStackOf
import com.android.systemui.scene.data.repository.Idle
import com.android.systemui.scene.data.repository.Transition
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.data.repository.setSceneTransition
import com.android.systemui.scene.domain.interactor.sceneBackInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
@@ -85,7 +88,7 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
    fun setUp() {
        // lazy value needs to be called here otherwise flow collection misbehaves
        underTest.value
        kosmos.sceneContainerRepository.setTransitionState(sceneTransitions)
        kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Lockscreen))
    }

    @Test
@@ -965,6 +968,47 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
            assertThat(lockscreenVisibility).isTrue()
        }

    @Test
    @EnableSceneContainer
    fun lockscreenVisibilityWithScenes_staysTrue_despiteEnteringIndirectly() =
        testScope.runTest {
            val isDeviceUnlocked by
                collectLastValue(
                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
                )
            assertThat(isDeviceUnlocked).isFalse()

            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)

            val lockscreenVisibility by collectLastValue(underTest.value.lockscreenVisibility)
            assertThat(lockscreenVisibility).isTrue()

            kosmos.setSceneTransition(Idle(Scenes.Shade))
            kosmos.sceneInteractor.changeScene(Scenes.Shade, "")
            kosmos.sceneBackInteractor.onSceneChange(from = Scenes.Lockscreen, to = Scenes.Shade)
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            assertThat(lockscreenVisibility).isTrue()
            val sceneBackStack by collectLastValue(kosmos.sceneBackInteractor.backStack)
            assertThat(sceneBackStack?.asIterable()?.toList()).isEqualTo(listOf(Scenes.Lockscreen))

            val isDeviceEntered by collectLastValue(kosmos.deviceEntryInteractor.isDeviceEntered)
            val isDeviceEnteredDirectly by
                collectLastValue(kosmos.deviceEntryInteractor.isDeviceEnteredDirectly)
            runCurrent()
            assertThat(isDeviceEntered).isFalse()
            assertThat(isDeviceEnteredDirectly).isFalse()

            kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
            kosmos.sceneBackInteractor.updateBackStack { sceneStackOf(Scenes.Gone) }
            assertThat(sceneBackStack?.asIterable()?.toList()).isEqualTo(listOf(Scenes.Gone))

            assertThat(isDeviceEntered).isTrue()
            assertThat(isDeviceEnteredDirectly).isFalse()
            assertThat(isDeviceUnlocked).isTrue()
            assertThat(lockscreenVisibility).isTrue()
        }

    @Test
    @EnableSceneContainer
    fun sceneContainer_usingGoingAwayAnimation_duringTransitionToGone() =
@@ -972,10 +1016,10 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
            val usingKeyguardGoingAwayAnimation by
                collectLastValue(underTest.value.usingKeyguardGoingAwayAnimation)

            sceneTransitions.value = lsToGone
            kosmos.setSceneTransition(lsToGone)
            assertThat(usingKeyguardGoingAwayAnimation).isTrue()

            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
            assertThat(usingKeyguardGoingAwayAnimation).isFalse()
        }

@@ -986,14 +1030,14 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
            val usingKeyguardGoingAwayAnimation by
                collectLastValue(underTest.value.usingKeyguardGoingAwayAnimation)

            sceneTransitions.value = lsToGone
            kosmos.setSceneTransition(lsToGone)
            surfaceBehindIsAnimatingFlow.emit(true)
            assertThat(usingKeyguardGoingAwayAnimation).isTrue()

            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
            assertThat(usingKeyguardGoingAwayAnimation).isTrue()

            sceneTransitions.value = goneToLs
            kosmos.setSceneTransition(goneToLs)
            assertThat(usingKeyguardGoingAwayAnimation).isTrue()

            surfaceBehindIsAnimatingFlow.emit(false)
@@ -1003,11 +1047,6 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
    companion object {
        private val progress = MutableStateFlow(0f)

        private val sceneTransitions =
            MutableStateFlow<ObservableTransitionState>(
                ObservableTransitionState.Idle(Scenes.Lockscreen)
            )

        private val lsToGone =
            ObservableTransitionState.Transition(
                Scenes.Lockscreen,
+37 −14
Original line number Diff line number Diff line
@@ -84,16 +84,16 @@ constructor(
            )

    /**
     * Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method).
     * This can be `false` when the device is unlocked, e.g. when the user still needs to swipe away
     * the non-secure lockscreen, even though they've already authenticated.
     * Emits `true` when the current scene switches to [Scenes.Gone] for the first time after having
     * been on [Scenes.Lockscreen].
     *
     * Note: This does not imply that the lockscreen is visible or not.
     * Different from [isDeviceEntered] such that the current scene must actually go through
     * [Scenes.Gone] to produce a `true`. [isDeviceEntered] also takes into account the navigation
     * back stack and will produce a `true` value even when the current scene is still not
     * [Scenes.Gone] but the bottommost entry of the navigation back stack switched from
     * [Scenes.Lockscreen] to [Scenes.Gone] while the user is staring at another scene.
     */
    val isDeviceEntered: StateFlow<Boolean> =
        combine(
                // This flow emits true when the currentScene switches to Gone for the first time
                // after having been on Lockscreen.
    val isDeviceEnteredDirectly: StateFlow<Boolean> =
        sceneInteractor.currentScene
            .filter { currentScene ->
                currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen
@@ -107,7 +107,30 @@ constructor(
                } else {
                    false
                }
                    },
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                initialValue = false,
            )

    /**
     * Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method).
     * This can be `false` when the device is unlocked, e.g. when the user still needs to swipe away
     * the non-secure lockscreen, even though they've already authenticated.
     *
     * Note: This does not imply that the lockscreen is visible or not.
     *
     * Different from [isDeviceEnteredDirectly] such that the current scene doesn't actually have to
     * go through [Scenes.Gone] to produce a `true`. [isDeviceEnteredDirectly] doesn't take the
     * navigation back stack into account and will only produce a `true` value even when the current
     * scene is actually [Scenes.Gone].
     */
    val isDeviceEntered: StateFlow<Boolean> =
        combine(
                // This flow emits true when the currentScene switches to Gone for the first time
                // after having been on Lockscreen.
                isDeviceEnteredDirectly,
                // This flow emits true only if the bottom of the navigation back stack has been
                // switched from Lockscreen to Gone. In other words, only if the device was unlocked
                // while visiting at least one scene "above" the Lockscreen scene.
+7 −5
Original line number Diff line number Diff line
@@ -112,8 +112,10 @@ constructor(
            }
            .distinctUntilChanged()

    private val isDeviceEntered by lazy { deviceEntryInteractor.get().isDeviceEntered }
    private val isDeviceNotEntered by lazy { isDeviceEntered.map { !it } }
    private val isDeviceEnteredDirectly by lazy {
        deviceEntryInteractor.get().isDeviceEnteredDirectly
    }
    private val isDeviceNotEnteredDirectly by lazy { isDeviceEnteredDirectly.map { !it } }

    /**
     * Surface visibility, which is either determined by the default visibility when not
@@ -126,7 +128,7 @@ constructor(
                sceneInteractor.get().transitionState.flatMapLatestConflated { state ->
                    when {
                        state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Gone) ->
                            isDeviceEntered
                            isDeviceEnteredDirectly
                        state.isTransitioning(from = Scenes.Bouncer, to = Scenes.Gone) ->
                            (state as Transition).progress.map { progress ->
                                progress >
@@ -210,7 +212,7 @@ constructor(
                            when (it.currentScene) {
                                in keyguardScenes -> flowOf(true)
                                in nonKeyguardScenes -> flowOf(false)
                                in keyguardAgnosticScenes -> isDeviceNotEntered
                                in keyguardAgnosticScenes -> isDeviceNotEnteredDirectly
                                else ->
                                    throw IllegalStateException("Unknown scene: ${it.currentScene}")
                            }
@@ -220,7 +222,7 @@ constructor(
                                it.isTransitioningSets(from = keyguardScenes) -> flowOf(true)
                                it.isTransitioningSets(from = nonKeyguardScenes) -> flowOf(false)
                                it.isTransitioningSets(from = keyguardAgnosticScenes) ->
                                    isDeviceNotEntered
                                    isDeviceNotEnteredDirectly
                                else ->
                                    throw IllegalStateException("Unknown scene: ${it.fromContent}")
                            }