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

Commit f1dc0fe4 authored by Josh Tsuji's avatar Josh Tsuji
Browse files

Add currentKeyguardState and related tests.

Bug: 278086361
Test: atest KeyguardTransitionInteractorTest
Flag: n/a
Change-Id: I207c6e160d8c0f62b5269b09897c5e3b9ddc4dc2
parent be1761a0
Loading
Loading
Loading
Loading
+947 −384

File changed.

Preview size limit exceeded, changes collapsed.

+112 −19
Original line number Diff line number Diff line
@@ -36,17 +36,19 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn

/** Encapsulates business-logic related to the keyguard transitions. */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardTransitionInteractor
@Inject
@@ -181,29 +183,121 @@ constructor(
    val finishedKeyguardTransitionStep: Flow<TransitionStep> =
        repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }

    /** The destination state of the last started transition. */
    /** The destination state of the last [TransitionState.STARTED] transition. */
    val startedKeyguardState: SharedFlow<KeyguardState> =
        startedKeyguardTransitionStep
            .map { step -> step.to }
            .shareIn(scope, SharingStarted.Eagerly, replay = 1)

    /** The last completed [KeyguardState] transition */
    /**
     * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition.
     *
     * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a
     * value when a subsequent transition is STARTED. It will *only* emit once we have finally
     * FINISHED in a state. This can have unintuitive implications.
     *
     * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in
     * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain
     * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition
     * finishes (at which point we'll be FINISHED in LOCKSCREEN).
     *
     * Since there's no real limit to how many consecutive transitions can be canceled, it's even
     * possible for the FINISHED state to be the same as the STARTED state while still
     * transitioning.
     *
     * For example:
     * 1. We're finished in GONE.
     * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still
     *    FINISHED in GONE.
     * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING ->
     *    LOCKSCREEN transition. We're still FINISHED in GONE.
     * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this
     *    starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also
     *    STARTED a transition *to* GONE.
     * 5. We'll emit KeyguardState.GONE again once the transition finishes.
     *
     * If you just need to know when we eventually settle into a state, this flow is likely
     * sufficient. However, if you're having issues with state *during* transitions started after
     * one or more canceled transitions, you probably need to use [currentKeyguardState].
     */
    val finishedKeyguardState: SharedFlow<KeyguardState> =
        finishedKeyguardTransitionStep
            .map { step -> step.to }
            .shareIn(scope, SharingStarted.Eagerly, replay = 1)

    /**
     * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed
     * it.
     * The [KeyguardState] we're currently in.
     *
     * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in
     * transition, this is the state we're transitioning *from*.
     *
     * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always
     * identical - if a transition FINISHES in a given state, the subsequent state we START a
     * transition *from* would always be that same previously FINISHED state.
     *
     * However, if a transition is CANCELED, the next transition will START from a state we never
     * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in
     * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never
     * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still
     * be GONE.
     *
     * In this example, if there was DOZING-related state that needs to be set up in order to
     * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were
     * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would
     * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state.
     *
     * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your
     * specific use case and how you want to handle cancellations. In general, if you're dealing
     * with state/UI present across multiple [KeyguardState]s, you probably want
     * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state,
     * you likely want [finishedKeyguardState].
     *
     * As an example, let's say you want to animate in a message on the lockscreen UI after waking
     * up, and that TextView is not involved in animations between states. You'd want to collect
     * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen.
     * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is
     * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible
     * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in
     * that case. That's likely not what you want.
     *
     * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during
     * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE
     * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation.
     * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is
     * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this
     * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace
     * during the LS -> GONE transition.
     *
     * If you need special-case handling for cancellations (such as conditional handling depending
     * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep]
     * directly.
     *
     * As a helpful footnote, here's the values of [finishedKeyguardState] and
     * [currentKeyguardState] during a sequence with two cancellations:
     * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE.
     * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE;
     *    finishedKeyguardState=GONE.
     * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN.
     *    currentKeyguardState=DOZING; finishedKeyguardState=GONE.
     * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE.
     *    currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE.
     * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE;
     *    finishedKeyguardState=GONE.
     */
    val isInTransitionToAnyState =
        combine(
            startedKeyguardTransitionStep,
            finishedKeyguardState,
        ) { startedStep, finishedState ->
            startedStep.to != finishedState
    val currentKeyguardState: SharedFlow<KeyguardState> =
        repository.transitions
            .mapLatest {
                if (it.transitionState == TransitionState.FINISHED) {
                    it.to
                } else {
                    it.from
                }
            }
            .distinctUntilChanged()
            .shareIn(scope, SharingStarted.Eagerly, replay = 1)

    /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */
    val isInTransitionToAnyState = isInTransitionWhere({ true }, { true })

    /**
     * The amount of transition into or out of the given [KeyguardState].
@@ -293,13 +387,12 @@ constructor(
        fromStatePredicate: (KeyguardState) -> Boolean,
        toStatePredicate: (KeyguardState) -> Boolean,
    ): Flow<Boolean> {
        return combine(
                startedKeyguardTransitionStep,
                finishedKeyguardState,
            ) { startedStep, finishedState ->
                fromStatePredicate(startedStep.from) &&
                    toStatePredicate(startedStep.to) &&
                    finishedState != startedStep.to
        return repository.transitions
            .filter { it.transitionState != TransitionState.CANCELED }
            .mapLatest {
                it.transitionState != TransitionState.FINISHED &&
                    fromStatePredicate(it.from) &&
                    toStatePredicate(it.to)
            }
            .distinctUntilChanged()
    }