diff options
| author | 2024-09-11 09:13:40 -0700 | |
|---|---|---|
| committer | 2024-09-11 11:02:14 -0700 | |
| commit | 43ba03fddffdddb87b2f6c6a900ccf9be137b712 (patch) | |
| tree | 57197ced983fc31a374c18b5f9a7564117428031 | |
| parent | 79e3576e2d345f56637856d28f47053fcdf88f4b (diff) | |
[flexiglass] isDeviceEntered also checks back stack
isDeviceEntered should always reflect whether the user has "passed the
lockscreen". Up until this point, this has only happened if the user
visited the Gone scene. This CL changes that for cases when the
lockscreen is "passed" by the user but without yet visiting the Gone
scene.
For example, in the case of
unlocking while looking at the Shade scene (when clicking a notification
that requires unlocking to continue).
In cases like the one above, the current scene is Bouncer and the
back stack is (top to bottom): [Shade, Lockscreen].
This CL contains two main changes:
1. Replace the Lockscreen at the bottom of the back stack with Gone,
when unlocking in this case
2. Make DeviceEntryInteractor.isDeviceEntered watch the bottom of the
back stack for a Lockscreen -> Gone swap in addition to the classic
visiting of the Gone or Lockscreen scenes as it did before
Bug: 359530769
Test: unit test added for the new case
Test: existing unit and integration tests modified to better model
what's going on and make them pass
Test: manually verified no harm done (a) normal unlock with pattern
bouncer + re-lock + visiting all scenes in both locked and unlocked
states (b) locked -> shade -> notification click -> unlock via alternate
bouncer / fingerprint (c) locked -> shade -> notification click ->
dismiss alternate bouncer -> bouncer -> unlock via bouncer (d) double
tap power to bring up camera app while locked -> click on gallery button
-> unlock via alternate bouncer / primary bouncer
Flag: com.android.systemui.scene_container
Change-Id: I9e8b6a34afd574352d01f25819b71e1702af3d15
9 files changed, 233 insertions, 180 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 3253edfb5fca..d90d58b8d25c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -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() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 2d42c4247ab7..a0cafcbd5ad1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -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). */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt index 1f3454de14d7..405cfd38d49a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt @@ -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) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index d1804608d130..763a1a943bf8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -33,7 +33,9 @@ import com.android.internal.policy.IKeyguardDismissCallback import com.android.keyguard.AuthInteractionProperties import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -82,7 +84,9 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.scene.data.model.asIterable import com.android.systemui.scene.data.repository.Transition +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.scene.shared.model.fakeSceneDataSource @@ -131,6 +135,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val testScope = kosmos.testScope private val deviceEntryHapticsInteractor by lazy { kosmos.deviceEntryHapticsInteractor } private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val sceneBackInteractor by lazy { kosmos.sceneBackInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository } private val bouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } @@ -237,17 +242,14 @@ class SceneContainerStartableTest : SysuiTestCase() { fun hydrateVisibility_basedOnOcclusion() = testScope.runTest { val isVisible by collectLastValue(sceneInteractor.isVisible) - prepareState( - isDeviceUnlocked = true, - initialSceneKey = Scenes.Lockscreen, - ) + prepareState(isDeviceUnlocked = true, initialSceneKey = Scenes.Lockscreen) underTest.start() assertThat(isVisible).isTrue() kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( true, - mock() + mock(), ) assertThat(isVisible).isFalse() @@ -259,10 +261,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun hydrateVisibility_basedOnAlternateBouncer() = testScope.runTest { val isVisible by collectLastValue(sceneInteractor.isVisible) - prepareState( - isDeviceUnlocked = false, - initialSceneKey = Scenes.Lockscreen, - ) + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Lockscreen) underTest.start() assertThat(isVisible).isTrue() @@ -270,7 +269,7 @@ class SceneContainerStartableTest : SysuiTestCase() { // WHEN the device is occluded, kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( true, - mock() + mock(), ) // THEN scenes are not visible assertThat(isVisible).isFalse() @@ -393,6 +392,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchFromBouncerToQuickSettingsWhenDeviceUnlocked_whenLeaveOpenShade() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val backStack by collectLastValue(sceneBackInteractor.backStack) kosmos.sysuiStatusBarStateController.leaveOpen = true // leave shade open val transitionState = @@ -414,12 +414,14 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionState.value = ObservableTransitionState.Idle(Scenes.Bouncer) runCurrent() assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer) + assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Lockscreen) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) ) assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings) + assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Gone) } @Test @@ -478,10 +480,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun stayOnLockscreenWhenDeviceUnlocksWithBypassOff() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) - prepareState( - isBypassEnabled = false, - initialSceneKey = Scenes.Lockscreen, - ) + prepareState(isBypassEnabled = false, initialSceneKey = Scenes.Lockscreen) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) underTest.start() @@ -520,10 +519,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToGoneWhenDeviceIsUnlockedAndUserIsOnBouncerWithBypassDisabled() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) - prepareState( - isBypassEnabled = false, - initialSceneKey = Scenes.Bouncer, - ) + prepareState(isBypassEnabled = false, initialSceneKey = Scenes.Bouncer) assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer) underTest.start() @@ -539,10 +535,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val alternateBouncerVisible by collectLastValue(bouncerRepository.alternateBouncerVisible) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) - prepareState( - isDeviceUnlocked = false, - initialSceneKey = Scenes.Shade, - ) + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade) assertThat(currentSceneKey).isEqualTo(Scenes.Shade) bouncerRepository.setAlternateVisible(true) underTest.start() @@ -564,10 +557,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToLockscreenWhenDeviceSleepsLocked() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) - prepareState( - isDeviceUnlocked = false, - initialSceneKey = Scenes.Shade, - ) + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade) assertThat(currentSceneKey).isEqualTo(Scenes.Shade) underTest.start() powerInteractor.setAsleepForTest() @@ -583,10 +573,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val currentTransitionInfo by collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal) val transitionState = - prepareState( - isDeviceUnlocked = false, - initialSceneKey = Scenes.Shade, - ) + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade) kosmos.keyguardRepository.setAodAvailable(true) runCurrent() assertThat(asleepState).isEqualTo(KeyguardState.AOD) @@ -615,10 +602,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val currentTransitionInfo by collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal) val transitionState = - prepareState( - isDeviceUnlocked = false, - initialSceneKey = Scenes.Shade, - ) + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade) kosmos.keyguardRepository.setAodAvailable(false) runCurrent() assertThat(asleepState).isEqualTo(KeyguardState.DOZING) @@ -1078,16 +1062,14 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun hydrateSystemUiState_onLockscreen_basedOnOcclusion() = testScope.runTest { - prepareState( - initialSceneKey = Scenes.Lockscreen, - ) + prepareState(initialSceneKey = Scenes.Lockscreen) underTest.start() runCurrent() clearInvocations(sysUiState) kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( true, - mock() + mock(), ) runCurrent() assertThat( @@ -1210,7 +1192,7 @@ class SceneContainerStartableTest : SysuiTestCase() { initialSceneKey = Scenes.Lockscreen, authenticationMethod = AuthenticationMethodModel.Pin, isDeviceUnlocked = false, - startsAwake = false + startsAwake = false, ) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) underTest.start() @@ -1228,11 +1210,14 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun collectFalsingSignals_onSuccessfulUnlock() = testScope.runTest { - prepareState( - initialSceneKey = Scenes.Lockscreen, - authenticationMethod = AuthenticationMethodModel.Pin, - isDeviceUnlocked = false, - ) + val currentScene by collectLastValue(sceneInteractor.currentScene) + + val transitionStateFlow = + prepareState( + initialSceneKey = Scenes.Lockscreen, + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + ) underTest.start() runCurrent() verify(falsingCollector, never()).onSuccessfulUnlock() @@ -1247,36 +1232,46 @@ class SceneContainerStartableTest : SysuiTestCase() { ) .forEach { sceneKey -> sceneInteractor.changeScene(sceneKey, "reason") + transitionStateFlow.value = ObservableTransitionState.Idle(sceneKey) runCurrent() verify(falsingCollector, never()).onSuccessfulUnlock() } // Changing to the Gone scene should report a successful unlock. - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN) runCurrent() - sceneInteractor.changeScene(Scenes.Gone, "reason") + // Make sure that the startable changed the scene to Gone because the device unlocked. + assertThat(currentScene).isEqualTo(Scenes.Gone) + // Make the transition state match the current state + transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) runCurrent() verify(falsingCollector).onSuccessfulUnlock() // Move around scenes without changing back to Lockscreen, shouldn't report another // unlock. - listOf( - Scenes.Shade, - Scenes.QuickSettings, - Scenes.Shade, - Scenes.Gone, - ) - .forEach { sceneKey -> - sceneInteractor.changeScene(sceneKey, "reason") - runCurrent() - verify(falsingCollector, times(1)).onSuccessfulUnlock() - } - - // Changing to the Lockscreen scene shouldn't report a successful unlock. - sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + listOf(Scenes.Shade, Scenes.QuickSettings, Scenes.Shade, Scenes.Gone).forEach { sceneKey + -> + sceneInteractor.changeScene(sceneKey, "reason") + transitionStateFlow.value = ObservableTransitionState.Idle(sceneKey) + runCurrent() + verify(falsingCollector, times(1)).onSuccessfulUnlock() + } + + // Putting the device to sleep to lock it again, which shouldn't report another + // successful unlock. + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + // Verify that the startable changed the scene to Lockscreen because the device locked + // following the sleep. + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + // Make the transition state match the current state + transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + // Wake up the device again before continuing with the test. + kosmos.powerInteractor.setAwakeForTest() runCurrent() + // Verify that the current scene is still the Lockscreen scene, now that the device is + // still locked. + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) verify(falsingCollector, times(1)).onSuccessfulUnlock() // Move around scenes without unlocking. @@ -1289,12 +1284,17 @@ class SceneContainerStartableTest : SysuiTestCase() { ) .forEach { sceneKey -> sceneInteractor.changeScene(sceneKey, "reason") + transitionStateFlow.value = ObservableTransitionState.Idle(sceneKey) runCurrent() verify(falsingCollector, times(1)).onSuccessfulUnlock() } - // Changing to the Gone scene should report a second successful unlock. - sceneInteractor.changeScene(Scenes.Gone, "reason") + kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN) + runCurrent() + // Make sure that the startable changed the scene to Gone because the device unlocked. + assertThat(currentScene).isEqualTo(Scenes.Gone) + // Make the transition state match the current scene. + transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) runCurrent() verify(falsingCollector, times(2)).onSuccessfulUnlock() } @@ -1608,7 +1608,7 @@ class SceneContainerStartableTest : SysuiTestCase() { kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( true, - mock() + mock(), ) runCurrent() verify(notificationShadeWindowController, times(1)).setKeyguardOccluded(true) @@ -1623,10 +1623,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun hydrateInteractionState_whileLocked() = testScope.runTest { - val transitionStateFlow = - prepareState( - initialSceneKey = Scenes.Lockscreen, - ) + val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen) underTest.start() runCurrent() verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true) @@ -1643,10 +1640,7 @@ class SceneContainerStartableTest : SysuiTestCase() { }, verifyAfterTransition = { verify(centralSurfaces) - .setInteracting( - StatusBarManager.WINDOW_STATUS_BAR, - false, - ) + .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false) }, ) @@ -1661,11 +1655,7 @@ class SceneContainerStartableTest : SysuiTestCase() { verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) }, verifyAfterTransition = { - verify(centralSurfaces) - .setInteracting( - StatusBarManager.WINDOW_STATUS_BAR, - true, - ) + verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true) }, ) @@ -1681,10 +1671,7 @@ class SceneContainerStartableTest : SysuiTestCase() { }, verifyAfterTransition = { verify(centralSurfaces) - .setInteracting( - StatusBarManager.WINDOW_STATUS_BAR, - false, - ) + .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false) }, ) @@ -1699,11 +1686,7 @@ class SceneContainerStartableTest : SysuiTestCase() { verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) }, verifyAfterTransition = { - verify(centralSurfaces) - .setInteracting( - StatusBarManager.WINDOW_STATUS_BAR, - true, - ) + verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true) }, ) @@ -1881,9 +1864,7 @@ class SceneContainerStartableTest : SysuiTestCase() { testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) val transitionStateFlow = - prepareState( - authenticationMethod = AuthenticationMethodModel.None, - ) + prepareState(authenticationMethod = AuthenticationMethodModel.None) underTest.start() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) // Swipe to Gone, more than halfway @@ -1949,9 +1930,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToGone_whenKeyguardBecomesDisabled_whenOnShadeScene() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - prepareState( - initialSceneKey = Scenes.Shade, - ) + prepareState(initialSceneKey = Scenes.Shade) assertThat(currentScene).isEqualTo(Scenes.Shade) underTest.start() @@ -1981,10 +1960,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun doesNotSwitchToGone_whenKeyguardBecomesDisabled_whenDeviceEntered() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - prepareState( - isDeviceUnlocked = true, - initialSceneKey = Scenes.Gone, - ) + prepareState(isDeviceUnlocked = true, initialSceneKey = Scenes.Gone) assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isTrue() underTest.start() @@ -2097,10 +2073,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun refreshLockscreenEnabled() = testScope.runTest { val transitionState = - prepareState( - isDeviceUnlocked = true, - initialSceneKey = Scenes.Gone, - ) + prepareState(isDeviceUnlocked = true, initialSceneKey = Scenes.Gone) underTest.start() val isLockscreenEnabled by collectLastValue(kosmos.deviceEntryInteractor.isLockscreenEnabled) @@ -2174,10 +2147,7 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verifyDuringTransition?.invoke() - transitionStateFlow.value = - ObservableTransitionState.Idle( - currentScene = toScene, - ) + transitionStateFlow.value = ObservableTransitionState.Idle(currentScene = toScene) runCurrent() verifyAfterTransition?.invoke() } @@ -2262,7 +2232,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private fun TestScope.allowHapticsOnSfps( isPowerButtonDown: Boolean = false, - lastPowerPress: Long = 10000 + lastPowerPress: Long = 10000, ) { kosmos.fakeKeyEventRepository.setPowerButtonDown(isPowerButtonDown) @@ -2287,7 +2257,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private fun TestScope.setupBiometricAuth( hasSfps: Boolean = false, hasUdfps: Boolean = false, - hasFace: Boolean = false + hasFace: Boolean = false, ) { if (hasSfps) { setFingerprintSensorType(FingerprintSensorType.POWER_BUTTON) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 7018f9dc8556..dbd7f0739f6c 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -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> = - 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". - deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked } - true - } else { - false - } + 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". + 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), diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt index 2d40845df802..afb72f03b28d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt @@ -71,6 +71,12 @@ constructor( logger.logSceneBackStack(backStack.value.asIterable()) } + /** Applies the given [transform] to the back stack. */ + fun updateBackStack(transform: (SceneStack) -> SceneStack) { + _backStack.update { stack -> transform(stack) } + logger.logSceneBackStack(backStack.value.asIterable()) + } + private fun stackOperation(from: SceneKey, to: SceneKey, stack: SceneStack): StackOperation? { val fromDistance = checkNotNull(sceneContainerConfig.navigationDistances[from]) { diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 98907b037d85..20c5142c6197 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -45,7 +45,6 @@ import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor import com.android.systemui.model.SceneContainerPlugin import com.android.systemui.model.SysUiState @@ -55,6 +54,7 @@ import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.data.model.asIterable +import com.android.systemui.scene.data.model.sceneStackOf import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -118,7 +118,6 @@ constructor( private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val bouncerInteractor: BouncerInteractor, private val keyguardInteractor: KeyguardInteractor, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val sysUiState: SysUiState, @DisplayId private val displayId: Int, private val sceneLogger: SceneLogger, @@ -205,7 +204,7 @@ constructor( is ObservableTransitionState.Transition -> state.fromContent }.let { it == Scenes.Shade || it == Scenes.QuickSettings } } - .distinctUntilChanged() + .distinctUntilChanged(), ) { inBackStack, isCurrentScene -> inBackStack || isCurrentScene } @@ -248,8 +247,7 @@ constructor( visibilityForTransitionState, isHeadsUpOrAnimatingAway, invisibleDueToOcclusion, - isAlternateBouncerVisible, - -> + isAlternateBouncerVisible -> when { isHeadsUpOrAnimatingAway -> true to "showing a HUN" isAlternateBouncerVisible -> true to "showing alternate bouncer" @@ -322,7 +320,7 @@ constructor( switchToScene( // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Bouncer, - loggingReason = "Need to authenticate locked SIM card." + loggingReason = "Need to authenticate locked SIM card.", ) } unlockStatus.isUnlocked && @@ -332,7 +330,7 @@ constructor( targetSceneKey = Scenes.Gone, loggingReason = "All SIM cards unlocked and device already unlocked and " + - "lockscreen doesn't require a swipe to dismiss." + "lockscreen doesn't require a swipe to dismiss.", ) } else -> { @@ -341,7 +339,7 @@ constructor( targetSceneKey = Scenes.Lockscreen, loggingReason = "All SIM cards unlocked and device still locked" + - " or lockscreen still requires a swipe to dismiss." + " or lockscreen still requires a swipe to dismiss.", ) } } @@ -363,10 +361,7 @@ constructor( when (val transitionState = sceneInteractor.transitionState.value) { is ObservableTransitionState.Idle -> setOf(transitionState.currentScene) is ObservableTransitionState.Transition -> - setOf( - transitionState.fromContent, - transitionState.toContent, - ) + setOf(transitionState.fromContent, transitionState.toContent) } val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen) val isAlternateBouncerVisible = alternateBouncerInteractor.isVisibleState() @@ -423,7 +418,20 @@ constructor( " didn't need to be left open" } else { val prevScene = previousScene.value - (prevScene ?: Scenes.Gone) to + val targetScene = prevScene ?: Scenes.Gone + if (targetScene != Scenes.Gone) { + 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." + } + list[list.size - 1] = Scenes.Gone + sceneStackOf(*list.toTypedArray()) + } + } + targetScene to "device was unlocked with primary bouncer showing," + " from sceneKey=$prevScene" } @@ -451,10 +459,7 @@ constructor( } } .collect { (targetSceneKey, loggingReason) -> - switchToScene( - targetSceneKey = targetSceneKey, - loggingReason = loggingReason, - ) + switchToScene(targetSceneKey = targetSceneKey, loggingReason = loggingReason) } } } @@ -812,7 +817,7 @@ constructor( private fun switchToScene( targetSceneKey: SceneKey, loggingReason: String, - sceneState: Any? = null + sceneState: Any? = null, ) { sceneInteractor.changeScene( toScene = targetSceneKey, @@ -831,10 +836,9 @@ constructor( private fun notifyKeyguardDismissCancelledCallbacks() { applicationScope.launch { - combine( - deviceEntryInteractor.isUnlocked, - sceneInteractor.currentScene.pairwise(), - ) { isUnlocked, (from, to) -> + combine(deviceEntryInteractor.isUnlocked, sceneInteractor.currentScene.pairwise()) { + isUnlocked, + (from, to) -> when { from != Scenes.Bouncer -> false to != Scenes.Gone && !isUnlocked -> true diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index 13116e7fd46f..096022ce1507 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.scene.domain.interactor.sceneBackInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -36,5 +37,6 @@ val Kosmos.deviceEntryInteractor by deviceUnlockedInteractor = deviceUnlockedInteractor, alternateBouncerInteractor = alternateBouncerInteractor, dismissCallbackRegistry = dismissCallbackRegistry, + sceneBackInteractor = sceneBackInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 9a5698cfb8ca..4228c3c0b110 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -32,13 +32,11 @@ import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.windowManagerLockscreenVisibilityInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope import com.android.systemui.model.sysUiState -import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneBackInteractor import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor @@ -62,7 +60,6 @@ val Kosmos.sceneContainerStartable by Fixture { deviceUnlockedInteractor = deviceUnlockedInteractor, bouncerInteractor = bouncerInteractor, keyguardInteractor = keyguardInteractor, - keyguardTransitionInteractor = keyguardTransitionInteractor, sysUiState = sysUiState, displayId = displayTracker.defaultDisplayId, sceneLogger = sceneLogger, |