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

Commit 967518d7 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Remove Lockscreen scene from backstack when face unlocked

...even if the alternate bouncer becomes invisible earlier than when the
device becomes unlocked.

There's a race condition between face unlock and alternate bouncer
visibility which basically causes the successful device unlock to hide
the alternate bouncer before the code in SceneContainerStartable is hit.
This leads to leaving the Lockscreen scene in the navigation back stack
even though it shouldn't be there.

This CL fixes that issue by just replacing the Lockscreen scene in the
bottom position of the navigation back stack when the device is
unlocked, regardless of whether the alternate bouncer was visible.

Fix: 375191368
Test: unit test added; older tests pass
Test: manually verified that the bug could no longer reproduce on a
UDFPS device
Flag: com.android.systemui.scene_container

Change-Id: Ie34dd753ef947c197282c9e794aebcd5102c66d8
parent b214d151
Loading
Loading
Loading
Loading
+68 −9
Original line number Diff line number Diff line
@@ -2411,6 +2411,64 @@ class SceneContainerStartableTest : SysuiTestCase() {
            assertThat(isAlternateBouncerVisible).isFalse()
        }

    @Test
    fun replacesLockscreenSceneOnBackStack_whenFaceUnlocked_fromShade_noAlternateBouncer() =
        testScope.runTest {
            val transitionState =
                prepareState(
                    isDeviceUnlocked = false,
                    initialSceneKey = Scenes.Lockscreen,
                    authenticationMethod = AuthenticationMethodModel.Pin,
                )
            underTest.start()

            val isUnlocked by
                collectLastValue(
                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
                )
            val currentScene by collectLastValue(sceneInteractor.currentScene)
            val backStack by collectLastValue(sceneBackInteractor.backStack)
            val isAlternateBouncerVisible by
                collectLastValue(kosmos.alternateBouncerInteractor.isVisible)
            assertThat(isUnlocked).isFalse()
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(isAlternateBouncerVisible).isFalse()

            // Change to shade.
            sceneInteractor.changeScene(Scenes.Shade, "")
            transitionState.value = ObservableTransitionState.Idle(Scenes.Shade)
            runCurrent()
            assertThat(isUnlocked).isFalse()
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen)
            assertThat(isAlternateBouncerVisible).isFalse()

            // Show the alternate bouncer.
            kosmos.alternateBouncerInteractor.forceShow()
            kosmos.sysuiStatusBarStateController.leaveOpen = true // leave shade open
            runCurrent()
            assertThat(isUnlocked).isFalse()
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen)
            assertThat(isAlternateBouncerVisible).isTrue()

            // Simulate race condition by hiding the alternate bouncer *before* the face unlock:
            kosmos.alternateBouncerInteractor.hide()
            runCurrent()
            assertThat(isUnlocked).isFalse()
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen)
            assertThat(isAlternateBouncerVisible).isFalse()

            // Trigger a face unlock.
            updateFaceAuthStatus(isSuccess = true)
            runCurrent()
            assertThat(isUnlocked).isTrue()
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Gone)
            assertThat(isAlternateBouncerVisible).isFalse()
        }

    private fun TestScope.emulateSceneTransition(
        transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
        toScene: SceneKey,
@@ -2647,15 +2705,16 @@ class SceneContainerStartableTest : SysuiTestCase() {
    }

    private fun updateFaceAuthStatus(isSuccess: Boolean) {
        with(kosmos.fakeDeviceEntryFaceAuthRepository) {
            isAuthenticated.value = isSuccess
            setAuthenticationStatus(
                if (isSuccess) {
            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
                    SuccessFaceAuthenticationStatus(
                        successResult = Mockito.mock(FaceManager.AuthenticationResult::class.java)
                    )
            )
                } else {
            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
                    FailedFaceAuthenticationStatus()
                }
            )
        }
    }
+16 −9
Original line number Diff line number Diff line
@@ -417,8 +417,13 @@ constructor(
                                            "mechanism: ${deviceUnlockStatus.deviceUnlockSource}"
                                else -> null
                            }
                        // Not on lockscreen or bouncer, so remain in the current scene.
                        else -> null
                        // 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 -> {
                            replaceLockscreenSceneOnBackStack()
                            null
                        }
                    }
                }
                .collect { (targetSceneKey, loggingReason) ->
@@ -427,17 +432,19 @@ constructor(
        }
    }

    /** If the [Scenes.Lockscreen] is on the backstack, replaces it with [Scenes.Gone]. */
    /**
     * If the [Scenes.Lockscreen] is on the bottom of the navigation backstack, replaces it with
     * [Scenes.Gone].
     */
    private fun replaceLockscreenSceneOnBackStack() {
        sceneBackInteractor.updateBackStack { stack ->
            val list = stack.asIterable().toMutableList()
            check(list.last() == Scenes.Lockscreen) {
                "The bottommost/last SceneKey of the back stack isn't" +
                    " the Lockscreen scene like expected. The back" +
                    " stack is $stack."
            }
            if (list.lastOrNull() == Scenes.Lockscreen) {
                list[list.size - 1] = Scenes.Gone
                sceneStackOf(*list.toTypedArray())
            } else {
                stack
            }
        }
    }