Loading packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +947 −384 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +112 −19 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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]. Loading Loading @@ -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() } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +947 −384 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +112 −19 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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]. Loading Loading @@ -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() } Loading