• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  *  Copyright (C) 2022 The Android Open Source Project
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *       http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  *
16  */
17 
18 package com.android.systemui.keyguard.domain.interactor
19 
20 import android.annotation.SuppressLint
21 import android.util.Log
22 import com.android.app.tracing.coroutines.flow.filterTraced
23 import com.android.app.tracing.coroutines.flow.shareInTraced
24 import com.android.app.tracing.coroutines.flow.stateInTraced
25 import com.android.app.tracing.coroutines.flow.traceAs
26 import com.android.app.tracing.coroutines.launchTraced as launch
27 import com.android.app.tracing.coroutines.traceCoroutine
28 import com.android.compose.animation.scene.ContentKey
29 import com.android.compose.animation.scene.ObservableTransitionState
30 import com.android.compose.animation.scene.SceneKey
31 import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff
32 import com.android.systemui.dagger.SysUISingleton
33 import com.android.systemui.dagger.qualifiers.Application
34 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
35 import com.android.systemui.keyguard.shared.model.Edge
36 import com.android.systemui.keyguard.shared.model.KeyguardState
37 import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
38 import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED
39 import com.android.systemui.keyguard.shared.model.TransitionState
40 import com.android.systemui.keyguard.shared.model.TransitionStep
41 import com.android.systemui.power.domain.interactor.PowerInteractor
42 import com.android.systemui.power.shared.model.ScreenPowerState
43 import com.android.systemui.scene.domain.interactor.SceneInteractor
44 import com.android.systemui.scene.shared.flag.SceneContainerFlag
45 import com.android.systemui.scene.shared.model.Scenes
46 import com.android.systemui.util.kotlin.WithPrev
47 import com.android.systemui.util.kotlin.pairwise
48 import javax.inject.Inject
49 import kotlinx.coroutines.CoroutineScope
50 import kotlinx.coroutines.Job
51 import kotlinx.coroutines.cancelAndJoin
52 import kotlinx.coroutines.channels.BufferOverflow
53 import kotlinx.coroutines.coroutineScope
54 import kotlinx.coroutines.flow.Flow
55 import kotlinx.coroutines.flow.MutableSharedFlow
56 import kotlinx.coroutines.flow.SharedFlow
57 import kotlinx.coroutines.flow.SharingStarted
58 import kotlinx.coroutines.flow.StateFlow
59 import kotlinx.coroutines.flow.channelFlow
60 import kotlinx.coroutines.flow.combine
61 import kotlinx.coroutines.flow.distinctUntilChanged
62 import kotlinx.coroutines.flow.emitAll
63 import kotlinx.coroutines.flow.filter
64 import kotlinx.coroutines.flow.flow
65 import kotlinx.coroutines.flow.flowOf
66 import kotlinx.coroutines.flow.map
67 import kotlinx.coroutines.flow.mapLatest
68 import kotlinx.coroutines.flow.onStart
69 
70 /** Encapsulates business-logic related to the keyguard transitions. */
71 @SysUISingleton
72 class KeyguardTransitionInteractor
73 @Inject
74 constructor(
75     @Application val scope: CoroutineScope,
76     private val repository: KeyguardTransitionRepository,
77     private val sceneInteractor: SceneInteractor,
78     private val powerInteractor: PowerInteractor,
79 ) {
80     private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>()
81 
82     /**
83      * Numerous flows are derived from, or care directly about, the transition value in and out of a
84      * single state. This prevent the redundant filters from running.
85      */
86     private val transitionValueCache = mutableMapOf<KeyguardState, MutableSharedFlow<Float>>()
87 
88     @SuppressLint("SharedFlowCreation")
89     private fun getTransitionValueFlow(state: KeyguardState): MutableSharedFlow<Float> {
90         return transitionValueCache.getOrPut(state) {
91             MutableSharedFlow<Float>(
92                     replay = 1,
93                     extraBufferCapacity = 2,
94                     onBufferOverflow = BufferOverflow.DROP_OLDEST,
95                 )
96                 .also { it.tryEmit(0f) }
97                 .traceAs("KTF-${state.name}")
98         }
99     }
100 
101     @Deprecated("Not performant - Use something else in this class")
102     val transitions = repository.transitions
103 
104     val transitionState: StateFlow<TransitionStep> =
105         transitions.stateInTraced(
106             "KTF-transitionState",
107             scope,
108             SharingStarted.Eagerly,
109             TransitionStep(),
110         )
111 
112     private val sceneTransitionPair =
113         sceneInteractor.transitionState
114             .pairwise()
115             .stateInTraced(
116                 "KTF-sceneTransitionPair",
117                 scope,
118                 SharingStarted.Eagerly,
119                 WithPrev(
120                     sceneInteractor.transitionState.value,
121                     sceneInteractor.transitionState.value,
122                 ),
123             )
124 
125     /**
126      * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
127      * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
128      * and that the previous step was *to* the state the STARTED step is *from*.
129      *
130      * This flow can be used to access the previous step to determine whether it was CANCELED or
131      * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming
132      * from when we were canceled.
133      */
134     @SuppressLint("SharedFlowCreation")
135     val startedStepWithPrecedingStep =
136         repository.transitions
137             .pairwise()
138             .filter { it.newValue.transitionState == TransitionState.STARTED }
139             .shareInTraced(
140                 "KTF-startedStepWithPrecedingStep",
141                 scope,
142                 SharingStarted.Eagerly,
143                 replay = 1,
144             )
145 
146     init {
147         // Collect non-canceled steps and emit transition values.
148         scope.launch("KTF-update-non-canceled") {
149             repository.transitions
150                 .filter { it.transitionState != TransitionState.CANCELED }
151                 .collect { step ->
152                     val value =
153                         if (step.transitionState == TransitionState.FINISHED) 1f else step.value
154                     getTransitionValueFlow(step.from).emit(1f - value)
155                     getTransitionValueFlow(step.to).emit(value)
156                 }
157         }
158 
159         scope.launch("KTF-update-transitionMap") {
160             repository.transitions.collect {
161                 // FROM->TO
162                 transitionMap[Edge.create(it.from, it.to)]?.emit(it)
163                 // FROM->(ANY)
164                 transitionMap[Edge.create(it.from, null)]?.emit(it)
165                 // (ANY)->TO
166                 transitionMap[Edge.create(null, it.to)]?.emit(it)
167             }
168         }
169 
170         // If a transition from state A -> B is canceled in favor of a transition from B -> C, we
171         // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted
172         // where the from or to states are A. This would leave transitionValue(A) stuck at an
173         // arbitrary non-zero value.
174         scope.launch("KTF-update-canceled") {
175             startedStepWithPrecedingStep.collect { (prevStep, startedStep) ->
176                 if (
177                     prevStep.transitionState == TransitionState.CANCELED &&
178                         startedStep.to != prevStep.from
179                 ) {
180                     getTransitionValueFlow(prevStep.from).emit(0f)
181                 } else if (prevStep.transitionState == TransitionState.RUNNING) {
182                     Log.e(
183                         TAG,
184                         "STARTED step ($startedStep) was preceded by a RUNNING step " +
185                             "($prevStep), which should never happen. Things could go badly here.",
186                     )
187                 }
188             }
189         }
190 
191         // Safety: When any transition is FINISHED, ensure all other transitionValue flows other
192         // than the FINISHED state are reset to a value of 0f. There have been rare but severe
193         // bugs that get the device stuck in a bad state when these are not properly reset.
194         scope.launch("KTF-update-finished") {
195             repository.transitions
196                 .filter { it.transitionState == TransitionState.FINISHED }
197                 .collect {
198                     for (state in KeyguardState.entries) {
199                         if (state != it.to) {
200                             val flow = getTransitionValueFlow(state)
201                             val replayCache = flow.replayCache
202                             if (!replayCache.isEmpty() && replayCache.last() != 0f) {
203                                 flow.emit(0f)
204                             }
205                         }
206                     }
207                 }
208         }
209 
210         if (keyguardTransitionForceFinishOnScreenOff()) {
211             /**
212              * If the screen is turning off, finish the current transition immediately. Further
213              * frames won't be visible anyway.
214              */
215             scope.launch("KTF-force-finish") {
216                 powerInteractor.screenPowerState
217                     .filter { it == ScreenPowerState.SCREEN_TURNING_OFF }
218                     .collect { repository.forceFinishCurrentTransition() }
219             }
220         }
221     }
222 
223     fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> {
224         return transition(
225             if (SceneContainerFlag.isEnabled || edgeWithoutSceneContainer == null) {
226                 edge
227             } else {
228                 edgeWithoutSceneContainer
229             }
230         )
231     }
232 
233     /** Given an [edge], return a Flow to collect only relevant [TransitionStep]s. */
234     @SuppressLint("SharedFlowCreation")
235     fun transition(edge: Edge): Flow<TransitionStep> {
236         edge.verifyValidKeyguardStates()
237         val mappedEdge = getMappedEdge(edge)
238 
239         val flow: Flow<TransitionStep> =
240             transitionMap.getOrPut(mappedEdge) {
241                 MutableSharedFlow<TransitionStep>(
242                         extraBufferCapacity = 10,
243                         onBufferOverflow = BufferOverflow.DROP_OLDEST,
244                     )
245                     .traceAs("KTF-${mappedEdge.from}-to-${mappedEdge.to}")
246             }
247 
248         if (!SceneContainerFlag.isEnabled) {
249             return flow
250         }
251         if (edge.isContentWildcardEdge()) {
252             return simulateTransitionStepsForSceneTransitions(edge)
253         }
254         return flow.filterTraced("stl-filter") { step ->
255             val fromContent =
256                 when (edge) {
257                     is Edge.StateToState -> edge.from?.mapToSceneContainerContent()
258                     is Edge.StateToContent -> edge.from?.mapToSceneContainerContent()
259                     is Edge.ContentToState -> edge.from
260                 }
261 
262             val toContent =
263                 when (edge) {
264                     is Edge.StateToState -> edge.to?.mapToSceneContainerContent()
265                     is Edge.StateToContent -> edge.to
266                     is Edge.ContentToState -> edge.to?.mapToSceneContainerContent()
267                 }
268 
269             val isTransitioningBetweenLockscreenStates =
270                 fromContent.isLockscreenOrNull() && toContent.isLockscreenOrNull()
271             val isTransitioningBetweenDesiredScenes =
272                 sceneInteractor.transitionState.value.isTransitioning(fromContent, toContent)
273 
274             // When in STL A -> B settles in A we can't do the same in KTF as KTF requires us to
275             // start B -> A to get back to A. [LockscreenSceneTransitionInteractor] will emit these
276             // steps but because STL is Idle(A) at this point (and never even started a B -> A in
277             // the first place) [isTransitioningBetweenDesiredScenes] will not be satisfied. We need
278             // this condition to not filter out the STARTED and FINISHED step of the "artificially"
279             // reversed B -> A transition.
280             val belongsToInstantReversedTransition =
281                 sceneInteractor.topmostContent.value == toContent &&
282                     sceneTransitionPair.value.previousValue.isTransitioning(toContent, fromContent)
283 
284             // We can't compare the terminal step with the current sceneTransition because
285             // a) STL has no guarantee that it will settle in Idle() when finished/canceled
286             // b) Comparing to Idle(toScene) would make any other FINISHED step settling in
287             //    toScene pass as well
288             val terminalStepBelongsToPreviousTransition =
289                 (step.transitionState == TransitionState.FINISHED ||
290                     step.transitionState == TransitionState.CANCELED) &&
291                     sceneTransitionPair.value.previousValue.isTransitioning(fromContent, toContent)
292 
293             return@filterTraced isTransitioningBetweenLockscreenStates ||
294                 isTransitioningBetweenDesiredScenes ||
295                 terminalStepBelongsToPreviousTransition ||
296                 belongsToInstantReversedTransition
297         }
298     }
299 
300     private fun ContentKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null
301 
302     /**
303      * This function will return a flow that simulates TransitionSteps based on STL movements
304      * filtered by [edge].
305      *
306      * STL transitions outside of Lockscreen Transitions are not tracked in KTI. This is an issue
307      * for wildcard edges, as this means that Scenes.Bouncer -> Scenes.Gone would not appear while
308      * AOD -> Scenes.Bouncer would appear.
309      *
310      * This function will track STL transitions only when a wildcard edge is provided and emit a
311      * RUNNING step for each update to [Transition.progress]. It will also emit a STARTED and
312      * FINISHED step when the transitions starts and finishes.
313      *
314      * All TransitionSteps will have UNDEFINED as to and from state even when one of them is the
315      * Lockscreen Scene. It indicates that both are scenes but it should not be relevant to
316      * consumers of the [transition] API as usually all viewModels are just interested in the
317      * progress value. The correct filtering based on the provided [edge] is always the
318      * responsibility of KTI and therefore only proper [TransitionStep]s are emitted. The filter is
319      * applied within this function.
320      */
321     private fun simulateTransitionStepsForSceneTransitions(edge: Edge) =
322         sceneInteractor.transitionState.flatMapLatestWithFinished {
323             when (it) {
324                 is ObservableTransitionState.Idle -> {
325                     flowOf()
326                 }
327                 is ObservableTransitionState.Transition -> {
328                     val isMatchingTransition =
329                         when (edge) {
330                             is Edge.StateToState ->
331                                 throw IllegalStateException("Should not be reachable.")
332                             is Edge.ContentToState -> it.isTransitioning(from = edge.from)
333                             is Edge.StateToContent -> it.isTransitioning(to = edge.to)
334                         }
335                     if (!isMatchingTransition) {
336                         return@flatMapLatestWithFinished flowOf()
337                     }
338                     flow {
339                         emit(
340                             TransitionStep(
341                                 from = UNDEFINED,
342                                 to = UNDEFINED,
343                                 value = 0f,
344                                 transitionState = TransitionState.STARTED,
345                             )
346                         )
347                         emitAll(
348                             it.progress.map { progress ->
349                                 TransitionStep(
350                                     from = UNDEFINED,
351                                     to = UNDEFINED,
352                                     value = progress,
353                                     transitionState = TransitionState.RUNNING,
354                                 )
355                             }
356                         )
357                     }
358                 }
359             }
360         }
361         .traceAs("KTF-transition-simulator")
362 
363     /**
364      * This function is similar to flatMapLatest but it will additionally emit a FINISHED
365      * TransitionStep whenever the flattened innerFlow emitted a STARTED step and is now being
366      * replaced by a new innerFlow.
367      *
368      * This is to make sure that every STARTED step will receive a corresponding FINISHED step.
369      *
370      * We can't simply write this into a flow {} block because Transition.progress doesn't complete.
371      * We also can't emit the FINISHED step simply when an Idle state is reached because a)
372      * Transitions are not guaranteed to finish in Idle and b) There can be multiple Idle
373      * transitions after another
374      */
375     private fun <T> Flow<T>.flatMapLatestWithFinished(
376         transform: suspend (T) -> Flow<TransitionStep>
377     ): Flow<TransitionStep> =
378         channelFlow {
379                 var job: Job? = null
380                 var startedEmitted = false
381 
382                 coroutineScope {
383                     collect { value ->
384                         traceCoroutine("cancelAndJoin") { job?.cancelAndJoin() }
385 
386                         job =
387                             launch("KTF-flatMapLatestWithFinished") {
388                                 val innerFlow = transform(value)
389                                 try {
390                                     innerFlow.collect { step ->
391                                         if (step.transitionState == TransitionState.STARTED) {
392                                             startedEmitted = true
393                                         }
394                                         traceCoroutine("send($step)") { send(step) }
395                                     }
396                                 } finally {
397                                     if (startedEmitted) {
398                                         val step =
399                                             TransitionStep(
400                                                 from = UNDEFINED,
401                                                 to = UNDEFINED,
402                                                 value = 1f,
403                                                 transitionState = TransitionState.FINISHED,
404                                             )
405                                         traceCoroutine("send($step)") { send(step) }
406                                         startedEmitted = false
407                                     }
408                                 }
409                             }
410                     }
411                 }
412             }
413 
414     /**
415      * Converts old KTF states to UNDEFINED when [SceneContainerFlag] is enabled.
416      *
417      * Does nothing otherwise.
418      *
419      * This method should eventually be removed when new code is only written for scene container.
420      * Even when all edges are ported today, there is still development on going in production that
421      * might utilize old states.
422      */
423     private fun getMappedEdge(edge: Edge): Edge.StateToState {
424         if (!SceneContainerFlag.isEnabled) return edge as Edge.StateToState
425         return when (edge) {
426             is Edge.StateToState ->
427                 Edge.create(
428                     from = edge.from?.mapToSceneContainerState(),
429                     to = edge.to?.mapToSceneContainerState(),
430                 )
431             is Edge.ContentToState -> Edge.create(UNDEFINED, edge.to)
432             is Edge.StateToContent -> Edge.create(edge.from, UNDEFINED)
433         }
434     }
435 
436     fun transitionValue(
437         content: ContentKey? = null,
438         stateWithoutSceneContainer: KeyguardState,
439     ): Flow<Float> {
440         return if (SceneContainerFlag.isEnabled && content != null) {
441             sceneInteractor.transitionProgress(content)
442         } else {
443             transitionValue(stateWithoutSceneContainer)
444         }
445     }
446 
447     /**
448      * The amount of transition into or out of the given [KeyguardState].
449      *
450      * The value will be `0` (or close to `0`, due to float point arithmetic) if not in this step or
451      * `1` when fully in the given state.
452      */
453     fun transitionValue(state: KeyguardState): Flow<Float> {
454         if (SceneContainerFlag.isEnabled && state != state.mapToSceneContainerState()) {
455             Log.e(TAG, "SceneContainer is enabled but a deprecated state $state is used.")
456             return transitionValue(state.mapToSceneContainerContent()!!, state)
457         }
458         return getTransitionValueFlow(state)
459     }
460 
461     /** The last [TransitionStep] with a [TransitionState] of STARTED */
462     val startedKeyguardTransitionStep: StateFlow<TransitionStep> =
463         repository.transitions
464             .filter { step -> step.transitionState == TransitionState.STARTED }
465             .stateInTraced(
466                 "KTF-startedKeyguardTransitionStep",
467                 scope,
468                 SharingStarted.Eagerly,
469                 TransitionStep(),
470             )
471 
472     /**
473      * The [KeyguardState] we're currently in.
474      *
475      * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in
476      * transition, this is the state we're transitioning *from*.
477      *
478      * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always
479      * identical - if a transition FINISHES in a given state, the subsequent state we START a
480      * transition *from* would always be that same previously FINISHED state.
481      *
482      * However, if a transition is CANCELED, the next transition will START from a state we never
483      * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in
484      * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never
485      * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still
486      * be GONE.
487      *
488      * In this example, if there was DOZING-related state that needs to be set up in order to
489      * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were
490      * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would
491      * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state.
492      *
493      * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your
494      * specific use case and how you want to handle cancellations. In general, if you're dealing
495      * with state/UI present across multiple [KeyguardState]s, you probably want
496      * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state,
497      * you likely want [finishedKeyguardState].
498      *
499      * As an example, let's say you want to animate in a message on the lockscreen UI after waking
500      * up, and that TextView is not involved in animations between states. You'd want to collect
501      * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen.
502      * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is
503      * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible
504      * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in
505      * that case. That's likely not what you want.
506      *
507      * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during
508      * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE
509      * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation.
510      * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is
511      * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this
512      * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace
513      * during the LS -> GONE transition.
514      *
515      * As a helpful footnote, here's the values of [finishedKeyguardState] and
516      * [currentKeyguardState] during a sequence with two cancellations:
517      * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE.
518      * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE;
519      *    finishedKeyguardState=GONE.
520      * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN.
521      *    currentKeyguardState=DOZING; finishedKeyguardState=GONE.
522      * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE.
523      *    currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE.
524      * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE;
525      *    finishedKeyguardState=GONE.
526      */
527     val currentKeyguardState: SharedFlow<KeyguardState> =
528         repository.transitions
529             .mapLatest {
530                 if (it.transitionState == TransitionState.FINISHED) {
531                     it.to
532                 } else {
533                     it.from
534                 }
535             }
536             .stateInTraced("KTF-currentKeyguardState", scope, SharingStarted.Eagerly, OFF)
537 
538     val isInTransition =
539         combine(isInTransitionWhere({ true }, { true }), sceneInteractor.transitionState) {
540             isKeyguardTransitioning,
541             sceneTransitionState ->
542             isKeyguardTransitioning ||
543                 (SceneContainerFlag.isEnabled && sceneTransitionState.isTransitioning())
544         }
545 
546     /**
547      * Whether we're in a transition to and from the given [KeyguardState]s, but haven't yet
548      * completed it.
549      *
550      * Provide [edgeWithoutSceneContainer] when the edge is different from what it is without it. If
551      * the edges are equal before and after the flag it is sufficient to provide just [edge].
552      */
553     fun isInTransition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<Boolean> {
554         return if (SceneContainerFlag.isEnabled) {
555                 if (edge.isContentWildcardEdge()) {
556                     sceneInteractor.transitionState.map {
557                         when (edge) {
558                             is Edge.StateToState ->
559                                 throw IllegalStateException("Should not be reachable.")
560                             is Edge.ContentToState -> it.isTransitioning(from = edge.from)
561                             is Edge.StateToContent -> it.isTransitioning(to = edge.to)
562                         }
563                     }
564                 } else {
565                     transition(edge).mapLatest { it.transitionState.isTransitioning() }
566                 }
567             } else {
568                 transition(edgeWithoutSceneContainer ?: edge).mapLatest {
569                     it.transitionState.isTransitioning()
570                 }
571             }
572             .onStart { emit(false) }
573             .traceAs("isInTransition-$edge-$edgeWithoutSceneContainer")
574             .distinctUntilChanged()
575     }
576 
577     /**
578      * Whether we're in a transition between two [KeyguardState]s that match the given predicates,
579      * but haven't yet completed it.
580      *
581      * If you only care about a single state for both from and to, instead use the optimized
582      * [isInTransition].
583      */
584     fun isInTransitionWhere(
585         fromStatePredicate: (KeyguardState) -> Boolean = { true },
586         toStatePredicate: (KeyguardState) -> Boolean = { true },
587     ): Flow<Boolean> {
588         return repository.transitions
589             .filter { it.transitionState != TransitionState.CANCELED }
590             .mapLatest {
591                 it.transitionState != TransitionState.FINISHED &&
592                     fromStatePredicate(it.from) &&
593                     toStatePredicate(it.to)
594             }
595             .distinctUntilChanged()
596     }
597 
598     /** Whether we've FINISHED a transition to a state that matches the given predicate. */
599     fun isFinishedInStateWhere(stateMatcher: (KeyguardState) -> Boolean): Flow<Boolean> {
600         return finishedKeyguardState.map { stateMatcher(it) }.distinctUntilChanged()
601     }
602 
603     fun isFinishedIn(
604         content: ContentKey,
605         stateWithoutSceneContainer: KeyguardState,
606     ): Flow<Boolean> {
607         return if (SceneContainerFlag.isEnabled) {
608                 combine(sceneInteractor.topmostContent, sceneInteractor.transitionState) {
609                     topmostContent,
610                     state ->
611                     topmostContent == content || state.isTransitioning(from = content)
612                 }
613             } else {
614                 isFinishedIn(stateWithoutSceneContainer)
615             }
616             .distinctUntilChanged()
617     }
618 
619     /** Whether we've FINISHED a transition to a state */
620     fun isFinishedIn(state: KeyguardState): Flow<Boolean> {
621         state.checkValidState()
622         return finishedKeyguardState.map { it == state }.distinctUntilChanged()
623     }
624 
625     fun isCurrentlyIn(scene: SceneKey, stateWithoutSceneContainer: KeyguardState): Flow<Boolean> {
626         return if (SceneContainerFlag.isEnabled) {
627                 // In STL there is no difference between finished/currentState
628                 isFinishedIn(scene, stateWithoutSceneContainer)
629             } else {
630                 stateWithoutSceneContainer.checkValidState()
631                 currentKeyguardState.map { it == stateWithoutSceneContainer }
632             }
633             .distinctUntilChanged()
634     }
635 
636     fun getCurrentState(): KeyguardState {
637         return currentKeyguardState.replayCache.last()
638     }
639 
640     fun getStartedState(): KeyguardState {
641         return startedKeyguardTransitionStep.value.to
642     }
643 
644     private val finishedKeyguardState: StateFlow<KeyguardState> =
645         repository.transitions
646             .filter { it.transitionState == TransitionState.FINISHED }
647             .map { it.to }
648             .stateInTraced("KTF-finishedKeyguardState", scope, SharingStarted.Eagerly, OFF)
649 
650     companion object {
651         private val TAG = KeyguardTransitionInteractor::class.simpleName
652     }
653 }
654