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

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

Merge "[flexiglass] isDeviceEntered also checks back stack" into main

parents 34f5e372 43ba03fd
Loading
Loading
Loading
Loading
+40 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
@@ -42,11 +43,16 @@ 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
import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -66,10 +72,14 @@ 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

    @Before
    fun setUp() {
        sceneContainerStartable.start()
        underTest = kosmos.deviceEntryInteractor
    }

@@ -423,8 +433,37 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
            assertThat(isUnlocked).isTrue()
        }

    private fun switchToScene(sceneKey: SceneKey) {
    @Test
    fun isDeviceEntered_unlockedWhileOnShade_emitsTrue() =
        testScope.runTest {
            val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
            assertThat(isDeviceEntered).isFalse()
            val currentScene by collectLastValue(sceneInteractor.currentScene)
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)

            // Navigate to shade and bouncer:
            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.
            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
            switchToScene(Scenes.Bouncer)
            assertThat(currentScene).isEqualTo(Scenes.Bouncer)

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

            assertThat(isDeviceEntered).isTrue()
        }

    private fun TestScope.switchToScene(sceneKey: SceneKey) {
        sceneInteractor.changeScene(sceneKey, "reason")
        sceneInteractor.setTransitionState(flowOf(ObservableTransitionState.Idle(sceneKey)))
        runCurrent()
    }

    private suspend fun givenCanShowAlternateBouncer() {
+19 −30
Original line number Diff line number Diff line
@@ -161,9 +161,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            val upDestinationSceneKey =
                (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
            kosmos.emulateUserDrivenTransition(
                to = upDestinationSceneKey,
            )
            kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey)

            kosmos.fakeSceneDataSource.pause()
            kosmos.enterPin()
@@ -226,16 +224,14 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
            assertThat(homeScene).isEqualTo(Scenes.Gone)
            kosmos.emulateUserDrivenTransition(
                to = homeScene,
            )
            kosmos.emulateUserDrivenTransition(to = homeScene)
        }

    @Test
    fun withAuthMethodNone_deviceWakeUp_skipsLockscreen() =
        testScope.runTest {
            kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = false)
            kosmos.putDeviceToSleep(instantlyLockDevice = false)
            kosmos.putDeviceToSleep()
            kosmos.assertCurrentScene(Scenes.Lockscreen)

            kosmos.wakeUpDevice()
@@ -246,7 +242,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    fun withAuthMethodSwipe_deviceWakeUp_doesNotSkipLockscreen() =
        testScope.runTest {
            kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
            kosmos.putDeviceToSleep(instantlyLockDevice = false)
            kosmos.putDeviceToSleep()
            kosmos.assertCurrentScene(Scenes.Lockscreen)

            kosmos.wakeUpDevice()
@@ -302,7 +298,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        testScope.runTest {
            kosmos.unlockDevice()
            kosmos.assertCurrentScene(Scenes.Gone)
            kosmos.putDeviceToSleep(instantlyLockDevice = false)
            kosmos.putDeviceToSleep()
            kosmos.assertCurrentScene(Scenes.Lockscreen)

            // Pretend like the timeout elapsed and now lock the device.
@@ -318,9 +314,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            val upDestinationSceneKey =
                (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
            kosmos.emulateUserDrivenTransition(
                to = upDestinationSceneKey,
            )
            kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey)

            kosmos.fakeSceneDataSource.pause()
            kosmos.dismissIme()
@@ -388,7 +382,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            kosmos.emulatePendingTransitionProgress(expectedVisible = true)
            kosmos.enterSimPin(
                authMethodAfterSimUnlock = AuthenticationMethodModel.None,
                enableLockscreen = false
                enableLockscreen = false,
            )

            kosmos.assertCurrentScene(Scenes.Gone)
@@ -434,7 +428,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    /** Updates the current authentication method and related states in the data layer. */
    private fun Kosmos.setAuthMethod(
        authMethod: AuthenticationMethodModel,
        enableLockscreen: Boolean = true
        enableLockscreen: Boolean = true,
    ) {
        if (authMethod.isSecure) {
            assert(enableLockscreen) {
@@ -538,24 +532,27 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        kosmos.fakeSceneDataSource.pause()
        sceneInteractor.changeScene(to, "reason")

        emulatePendingTransitionProgress(
            expectedVisible = to != Scenes.Gone,
        )
        emulatePendingTransitionProgress(expectedVisible = to != Scenes.Gone)
    }

    /**
     * Locks the device immediately (without delay).
     * Locks the device.
     *
     * Asserts the device to be lockable (e.g. that the current authentication is secure).
     *
     * Not to be confused with [putDeviceToSleep], which may also instantly lock the device.
     * Internally emulates a power button press that puts the device to sleep, followed by another
     * power button press that wakes up the device but is then expected to be in the locked state.
     */
    private suspend fun Kosmos.lockDevice() {
        val authMethod = authenticationInteractor.getAuthenticationMethod()
        assertWithMessage("The authentication method of $authMethod is not secure, cannot lock!")
            .that(authMethod.isSecure)
            .isTrue()
        sceneInteractor.changeScene(Scenes.Lockscreen, "")

        powerInteractor.setAsleepForTest()
        testScope.runCurrent()

        powerInteractor.setAwakeForTest()
        testScope.runCurrent()
    }

@@ -569,9 +566,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        fakeSceneDataSource.pause()
        enterPin()

        emulatePendingTransitionProgress(
            expectedVisible = false,
        )
        emulatePendingTransitionProgress(expectedVisible = false)
    }

    /**
@@ -645,9 +640,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    }

    /** Changes device wakefulness state from awake to asleep, going through intermediary states. */
    private suspend fun Kosmos.putDeviceToSleep(
        instantlyLockDevice: Boolean = true,
    ) {
    private suspend fun Kosmos.putDeviceToSleep() {
        val wakefulnessModel = powerInteractor.detailedWakefulness.value
        assertWithMessage("Cannot put device to sleep as it's already asleep!")
            .that(wakefulnessModel.isAwake())
@@ -655,10 +648,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {

        powerInteractor.setAsleepForTest()
        testScope.runCurrent()

        if (instantlyLockDevice) {
            lockDevice()
        }
    }

    /** Emulates the dismissal of the IME (soft keyboard). */
+22 −0
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ import com.android.systemui.authentication.domain.interactor.authenticationInter
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
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.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
@@ -173,12 +175,32 @@ class SceneBackInteractorTest : SysuiTestCase() {
            )
        }

    @Test
    @EnableSceneContainer
    fun updateBackStack() =
        testScope.runTest {
            underTest.onSceneChange(from = Scenes.Lockscreen, to = Scenes.Shade)
            underTest.onSceneChange(from = Scenes.Shade, to = Scenes.QuickSettings)
            underTest.onSceneChange(from = Scenes.QuickSettings, to = Scenes.Bouncer)
            assertThat(underTest.backStack.value.asIterable().toList())
                .isEqualTo(listOf(Scenes.QuickSettings, Scenes.Shade, Scenes.Lockscreen))

            underTest.updateBackStack { stack ->
                // Reverse the stack, just to see if it can be done:
                sceneStackOf(*stack.asIterable().reversed().toTypedArray())
            }

            assertThat(underTest.backStack.value.asIterable().toList())
                .isEqualTo(listOf(Scenes.Lockscreen, Scenes.Shade, Scenes.QuickSettings))
        }

    private suspend fun TestScope.assertRoute(vararg route: RouteNode) {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        val backScene by collectLastValue(underTest.backScene)

        route.forEachIndexed { index, node ->
            sceneInteractor.changeScene(node.changeSceneTo, "")
            runCurrent()
            assertWithMessage("node at index $index currentScene mismatch")
                .that(currentScene)
                .isEqualTo(node.changeSceneTo)
+77 −107

File changed.

Preview size limit exceeded, changes collapsed.

+41 −17
Original line number Diff line number Diff line
@@ -24,8 +24,11 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.scene.data.model.asIterable
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.util.kotlin.pairwise
import com.android.systemui.utils.coroutines.flow.mapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -34,6 +37,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -59,6 +63,7 @@ constructor(
    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
    private val alternateBouncerInteractor: AlternateBouncerInteractor,
    private val dismissCallbackRegistry: DismissCallbackRegistry,
    sceneBackInteractor: SceneBackInteractor,
) {
    /**
     * Whether the device is unlocked.
@@ -86,19 +91,40 @@ constructor(
     * Note: This does not imply that the lockscreen is visible or not.
     */
    val isDeviceEntered: StateFlow<Boolean> =
        combine(
                // This flow emits true when the currentScene switches to Gone for the first time
                // after having been on Lockscreen.
                sceneInteractor.currentScene
                    .filter { currentScene ->
                        currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen
                    }
                    .mapLatestConflated { scene ->
                        if (scene == Scenes.Gone) {
                    // Make sure device unlock status is definitely unlocked before we consider the
                    // device "entered".
                            // Make sure device unlock status is definitely unlocked before we
                            // consider the device "entered".
                            deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
                            true
                        } else {
                            false
                        }
                    },
                // 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.
                sceneBackInteractor.backStack
                    // The bottom of the back stack, which is Lockscreen, Gone, or null if empty.
                    .map { it.asIterable().lastOrNull() }
                    // Filter out cases where the stack changes but the bottom remains unchanged.
                    .distinctUntilChanged()
                    // Detect changes of the bottom of the stack, start with null, so the first
                    // update emits a value and the logic doesn't need to wait for a second value
                    // before emitting something.
                    .pairwise(initialValue = null)
                    // Replacing a bottom of the stack that was Lockscreen with Gone constitutes a
                    // "device entered" event.
                    .map { (from, to) -> from == Scenes.Lockscreen && to == Scenes.Gone },
            ) { enteredDirectly, enteredOnBackStack ->
                enteredOnBackStack || enteredDirectly
            }
            .stateIn(
                scope = applicationScope,
@@ -129,7 +155,7 @@ constructor(
                },
                isLockscreenEnabled,
                deviceUnlockedInteractor.deviceUnlockStatus,
                isDeviceEntered
                isDeviceEntered,
            ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered ->
                val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled
                (isSwipeAuthMethod ||
@@ -155,9 +181,7 @@ constructor(
     *   canceled
     */
    @JvmOverloads
    fun attemptDeviceEntry(
        callback: IKeyguardDismissCallback? = null,
    ) {
    fun attemptDeviceEntry(callback: IKeyguardDismissCallback? = null) {
        callback?.let { dismissCallbackRegistry.addCallback(it) }

        // TODO (b/307768356),
Loading