diff options
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, |