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

Commit 22c7ef4d authored by Josh Tsuji's avatar Josh Tsuji Committed by Android (Google) Code Review
Browse files

Merge "Add currentKeyguardState and related tests." into main

parents d8f54b2f f1dc0fe4
Loading
Loading
Loading
Loading
+947 −384

File changed.

Preview size limit exceeded, changes collapsed.

+112 −19
Original line number Diff line number Diff line
@@ -37,17 +37,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
@@ -192,29 +194,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].
@@ -304,13 +398,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()
    }