diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 719edd7e853593b981bd49dda1e4b07fc19608cd..37b331cd84553689bf303208fab1363ccee53c34 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -35,6 +35,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,6 +47,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn /** Encapsulates business-logic related to the keyguard transitions. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -205,6 +207,21 @@ constructor( .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) + /** + * A pair of the most recent STARTED step, and the transition step immediately preceding it. The + * transition framework enforces that the previous step is either a CANCELED or FINISHED step, + * and that the previous step was *to* the state the STARTED step is *from*. + * + * This flow can be used to access the previous step to determine whether it was CANCELED or + * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming + * from when we were canceled. + */ + val startedStepWithPrecedingStep = + transitions + .pairwise() + .filter { it.newValue.transitionState == TransitionState.STARTED } + .stateIn(scope, SharingStarted.Eagerly, null) + /** * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. * diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index b81793ecec642a2c4719891e80891bfe9f9f6f10..cff74b333530d2f1d2e9cf5ea12be878a4c0bd6d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -19,7 +19,9 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -121,19 +123,27 @@ constructor( * want to know if the AOD/clock/notifs/etc. are visible. */ val lockscreenVisibility: Flow<Boolean> = - combine( - transitionInteractor.startedKeyguardTransitionStep, - transitionInteractor.finishedKeyguardState, - ) { startedStep, finishedState -> - // If we finished the transition, use the finished state. If we're running a - // transition, use the state we're transitioning FROM. This can be different from - // the last finished state if a transition is interrupted. For example, if we were - // transitioning from GONE to AOD and then started AOD -> LOCKSCREEN mid-transition, - // we want to immediately use the visibility for AOD (lockscreenVisibility=true) - // even though the lastFinishedState is still GONE (lockscreenVisibility=false). - if (finishedState == startedStep.to) finishedState else startedStep.from + transitionInteractor.currentKeyguardState + .sample(transitionInteractor.startedStepWithPrecedingStep, ::Pair) + .map { (currentState, startedWithPrev) -> + val startedFromStep = startedWithPrev?.previousValue + val startedStep = startedWithPrev?.newValue + val returningToGoneAfterCancellation = + startedStep?.to == KeyguardState.GONE && + startedFromStep?.transitionState == TransitionState.CANCELED && + startedFromStep.from == KeyguardState.GONE + + if (!returningToGoneAfterCancellation) { + // By default, apply the lockscreen visibility of the current state. + KeyguardState.lockscreenVisibleInState(currentState) + } else { + // If we're transitioning to GONE after a prior canceled transition from GONE, + // then this is the camera launch transition from an asleep state back to GONE. + // We don't want to show the lockscreen since we're aborting the lock and going + // back to GONE. + KeyguardState.lockscreenVisibleInState(KeyguardState.GONE) + } } - .map(KeyguardState::lockscreenVisibleInState) .distinctUntilChanged() /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt index a4483bdac467a3a7cf6ad6f37a5d8b9cb4da6f58..6d605a564022ca31effb7a2572348b12e1e30271 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -375,4 +375,323 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { values ) } + + @Test + fun testLockscreenVisibility_usesFromState_ifCanceled() = + testScope.runTest { + val values by collectValues(underTest.lockscreenVisibility) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope + ) + + runCurrent() + + assertEquals( + listOf( + // Initially should be true, as we start in LOCKSCREEN. + true, + // Then, false, since we finish in GONE. + false, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + // Should remain false as we transition from GONE. + false, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.CANCELED, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + assertEquals( + listOf( + true, + false, + // If we cancel and then go from LS -> GONE, we should immediately flip to the + // visibility of the from state (LS). + true, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + assertEquals( + listOf( + true, + false, + true, + ), + values + ) + } + + /** + * Tests the special case for insecure camera launch. CANCELING a transition from GONE and then + * STARTING a transition back to GONE should never show the lockscreen, even though the current + * state during the AOD/isAsleep -> GONE transition is AOD (where lockscreen visibility = true). + */ + @Test + fun testLockscreenVisibility_falseDuringTransitionToGone_fromCanceledGone() = + testScope.runTest { + val values by collectValues(underTest.lockscreenVisibility) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope + ) + + runCurrent() + assertEquals( + listOf( + true, + // Not visible since we're GONE. + false, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.CANCELED, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.AOD, + to = KeyguardState.GONE, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + assertEquals( + listOf( + true, + // Remains not visible from GONE -> AOD (canceled) -> AOD since we never + // FINISHED in AOD, and special-case handling for the insecure camera launch + // ensures that we use the lockscreen visibility for GONE (false) if we're + // STARTED to GONE after a CANCELED from GONE. + false, + ), + values + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + testScope, + ) + + assertEquals( + listOf( + true, + false, + // Make sure there's no stuck overrides or something - we should make lockscreen + // visible again once we're finished in LOCKSCREEN. + true, + ), + values + ) + } + + /** */ + @Test + fun testLockscreenVisibility_trueDuringTransitionToGone_fromNotCanceledGone() = + testScope.runTest { + val values by collectValues(underTest.lockscreenVisibility) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope + ) + + runCurrent() + assertEquals( + listOf( + true, + // Not visible when finished in GONE. + false, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + // Still not visible during GONE -> AOD. + false, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.GONE, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + false, + // Visible now that we're FINISHED in AOD. + true + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.AOD, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + false, + // Remains visible from AOD during transition. + true + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + assertEquals( + listOf( + true, + false, + true, + // Until we're finished in GONE again. + false + ), + values + ) + } }