• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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 package com.android.systemui.scene.domain.interactor
18 
19 import com.android.app.tracing.coroutines.flow.stateInTraced
20 import com.android.compose.animation.scene.ContentKey
21 import com.android.compose.animation.scene.ObservableTransitionState
22 import com.android.compose.animation.scene.OverlayKey
23 import com.android.compose.animation.scene.SceneKey
24 import com.android.compose.animation.scene.TransitionKey
25 import com.android.compose.animation.scene.UserAction
26 import com.android.compose.animation.scene.UserActionResult
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Application
29 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
30 import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
31 import com.android.systemui.log.table.Diffable
32 import com.android.systemui.log.table.TableLogBuffer
33 import com.android.systemui.log.table.TableRowLogger
34 import com.android.systemui.scene.data.repository.SceneContainerRepository
35 import com.android.systemui.scene.domain.resolver.SceneResolver
36 import com.android.systemui.scene.shared.logger.SceneLogger
37 import com.android.systemui.scene.shared.model.Overlays
38 import com.android.systemui.scene.shared.model.SceneFamilies
39 import com.android.systemui.scene.shared.model.Scenes
40 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
41 import com.android.systemui.util.kotlin.pairwise
42 import dagger.Lazy
43 import javax.inject.Inject
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.ExperimentalCoroutinesApi
46 import kotlinx.coroutines.coroutineScope
47 import kotlinx.coroutines.flow.Flow
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.emitAll
52 import kotlinx.coroutines.flow.flatMapLatest
53 import kotlinx.coroutines.flow.flow
54 import kotlinx.coroutines.flow.flowOf
55 import kotlinx.coroutines.flow.map
56 import kotlinx.coroutines.flow.onEach
57 import kotlinx.coroutines.flow.update
58 import kotlinx.coroutines.launch
59 
60 /**
61  * Generic business logic and app state accessors for the scene framework.
62  *
63  * Note that this class should not depend on state or logic of other modules or features. Instead,
64  * other feature modules should depend on and call into this class when their parts of the
65  * application state change.
66  */
67 @SysUISingleton
68 class SceneInteractor
69 @Inject
70 constructor(
71     @Application private val applicationScope: CoroutineScope,
72     private val repository: SceneContainerRepository,
73     private val logger: SceneLogger,
74     private val sceneFamilyResolvers: Lazy<Map<SceneKey, @JvmSuppressWildcards SceneResolver>>,
75     private val deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
76     private val keyguardEnabledInteractor: Lazy<KeyguardEnabledInteractor>,
77     private val disabledContentInteractor: DisabledContentInteractor,
78     private val shadeModeInteractor: ShadeModeInteractor,
79 ) {
80 
81     interface OnSceneAboutToChangeListener {
82 
83         /**
84          * Notifies that the scene is about to change to [toScene].
85          *
86          * The implementation can choose to consume the [sceneState] to prepare the incoming scene.
87          */
88         fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?)
89     }
90 
91     private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>()
92 
93     /**
94      * The keys of all scenes and overlays in the container.
95      *
96      * They will be sorted in z-order such that the last one is the one that should be rendered on
97      * top of all previous ones.
98      */
99     val allContentKeys: List<ContentKey> = repository.allContentKeys
100 
101     /**
102      * The current scene.
103      *
104      * Note that during a transition between scenes, more than one scene might be rendered but only
105      * one is considered the committed/current scene.
106      */
107     val currentScene: StateFlow<SceneKey> = repository.currentScene
108 
109     /**
110      * The current set of overlays to be shown (may be empty).
111      *
112      * Note that during a transition between overlays, a different set of overlays may be rendered -
113      * but only the ones in this set are considered the current overlays.
114      */
115     val currentOverlays: StateFlow<Set<OverlayKey>> = repository.currentOverlays
116 
117     /**
118      * The current state of the transition.
119      *
120      * Consumers should use this state to know:
121      * 1. Whether there is an ongoing transition or if the system is at rest.
122      * 2. When transitioning, which scenes are being transitioned between.
123      * 3. When transitioning, what the progress of the transition is.
124      */
125     val transitionState: StateFlow<ObservableTransitionState> =
126         repository.transitionState
127             .onEach { logger.logSceneTransition(it) }
128             .stateInTraced(
129                 name = "transitionState",
130                 scope = applicationScope,
131                 started = SharingStarted.Eagerly,
132                 initialValue = repository.transitionState.value,
133             )
134 
135     /**
136      * The key of the content that the UI is currently transitioning to or `null` if there is no
137      * active transition at the moment.
138      *
139      * This is a convenience wrapper around [transitionState], meant for flow-challenged consumers
140      * like Java code.
141      */
142     val transitioningTo: StateFlow<ContentKey?> =
143         transitionState
144             .map { state ->
145                 when (state) {
146                     is ObservableTransitionState.Idle -> null
147                     is ObservableTransitionState.Transition -> state.toContent
148                 }
149             }
150             .stateInTraced(
151                 name = "transitioningTo",
152                 scope = applicationScope,
153                 started = SharingStarted.WhileSubscribed(),
154                 initialValue = null,
155             )
156 
157     /**
158      * Whether user input is ongoing for the current transition. For example, if the user is swiping
159      * their finger to transition between scenes, this value will be true while their finger is on
160      * the screen, then false for the rest of the transition.
161      */
162     val isTransitionUserInputOngoing: StateFlow<Boolean> =
163         transitionState
164             .flatMapLatest {
165                 when (it) {
166                     is ObservableTransitionState.Transition -> it.isUserInputOngoing
167                     is ObservableTransitionState.Idle -> flowOf(false)
168                 }
169             }
170             .stateInTraced(
171                 name = "isTransitionUserInputOngoing",
172                 scope = applicationScope,
173                 started = SharingStarted.WhileSubscribed(),
174                 initialValue = false,
175             )
176 
177     /** Whether the scene container is visible. */
178     val isVisible: StateFlow<Boolean> =
179         combine(
180                 repository.isVisible,
181                 repository.isRemoteUserInputOngoing,
182                 repository.activeTransitionAnimationCount,
183             ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount ->
184                 isVisibleInternal(
185                     raw = isVisible,
186                     isRemoteUserInputOngoing = isRemoteUserInteractionOngoing,
187                     activeTransitionAnimationCount = activeTransitionAnimationCount,
188                 )
189             }
190             .stateInTraced(
191                 name = "isVisible",
192                 scope = applicationScope,
193                 started = SharingStarted.WhileSubscribed(),
194                 initialValue = isVisibleInternal(),
195             )
196 
197     /** Whether there's an ongoing remotely-initiated user interaction. */
198     val isRemoteUserInteractionOngoing: StateFlow<Boolean> = repository.isRemoteUserInputOngoing
199 
200     /**
201      * Whether there's an ongoing user interaction started in the scene container Compose hierarchy.
202      */
203     val isSceneContainerUserInputOngoing: StateFlow<Boolean> =
204         repository.isSceneContainerUserInputOngoing
205 
206     /**
207      * The amount of transition into or out of the given [content].
208      *
209      * The value will be `0` if not in this scene or `1` when fully in the given scene.
210      */
211     @OptIn(ExperimentalCoroutinesApi::class)
212     fun transitionProgress(content: ContentKey): Flow<Float> {
213         return transitionState.flatMapLatest { transition ->
214             when (transition) {
215                 is ObservableTransitionState.Idle -> {
216                     flowOf(
217                         if (
218                             transition.currentScene == content ||
219                                 content in transition.currentOverlays
220                         ) {
221                             1f
222                         } else {
223                             0f
224                         }
225                     )
226                 }
227                 is ObservableTransitionState.Transition -> {
228                     when {
229                         transition.toContent == content -> transition.progress
230                         transition.fromContent == content -> transition.progress.map { 1f - it }
231                         else -> flowOf(0f)
232                     }
233                 }
234             }
235         }
236     }
237 
238     fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) {
239         onSceneAboutToChangeListener.add(processor)
240     }
241 
242     /**
243      * Requests a scene change to the given scene.
244      *
245      * The change is animated. Therefore, it will be some time before the UI will switch to the
246      * desired scene. Once enough of the transition has occurred, the [currentScene] will become
247      * [toScene] (unless the transition is canceled by user action or another call to this method).
248      *
249      * If [forceSettleToTargetScene] is `true` and the target scene is the same as the current
250      * scene, any current transition will be canceled and an animation to the target scene will be
251      * started.
252      *
253      * If [Overlays.Bouncer] is showing, we trigger an instant scene change as it will not be user-
254      * visible, and trigger a transition to hide the bouncer.
255      */
256     @JvmOverloads
257     fun changeScene(
258         toScene: SceneKey,
259         loggingReason: String,
260         transitionKey: TransitionKey? = null,
261         sceneState: Any? = null,
262         forceSettleToTargetScene: Boolean = false,
263     ) {
264         val currentSceneKey = currentScene.value
265         val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
266         val bouncerShowing = Overlays.Bouncer in currentOverlays.value
267 
268         if (resolvedScene == currentSceneKey && forceSettleToTargetScene) {
269             logger.logSceneChangeCancellation(scene = resolvedScene, sceneState = sceneState)
270             onSceneAboutToChangeListener.forEach {
271                 it.onSceneAboutToChange(resolvedScene, sceneState)
272             }
273             repository.freezeAndAnimateToCurrentState()
274         }
275 
276         if (
277             !validateSceneChange(
278                 from = currentSceneKey,
279                 to = resolvedScene,
280                 loggingReason = loggingReason,
281             )
282         ) {
283             if (bouncerShowing) {
284                 hideOverlay(
285                     Overlays.Bouncer,
286                     "Scene change cancelled but hiding bouncer for: ($loggingReason)",
287                 )
288             }
289             return
290         }
291 
292         onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(resolvedScene, sceneState) }
293 
294         logger.logSceneChanged(
295             from = currentSceneKey,
296             to = resolvedScene,
297             sceneState = sceneState,
298             reason = loggingReason,
299             isInstant = false,
300         )
301 
302         if (bouncerShowing) {
303             repository.snapToScene(resolvedScene)
304             hideOverlay(Overlays.Bouncer, "Hiding on changeScene for: ($loggingReason)")
305         } else {
306             repository.changeScene(resolvedScene, transitionKey)
307         }
308     }
309 
310     /**
311      * Requests a scene change to the given scene.
312      *
313      * The change is instantaneous and not animated; it will be observable in the next frame and
314      * there will be no transition animation. If [Overlays.Bouncer] is showing, it will instantly be
315      * hidden.
316      */
317     fun snapToScene(toScene: SceneKey, loggingReason: String) {
318         val currentSceneKey = currentScene.value
319         val resolvedScene =
320             sceneFamilyResolvers.get()[toScene]?.let { familyResolver ->
321                 if (familyResolver.includesScene(currentSceneKey)) {
322                     return
323                 } else {
324                     familyResolver.resolvedScene.value
325                 }
326             } ?: toScene
327         if (
328             !validateSceneChange(
329                 from = currentSceneKey,
330                 to = resolvedScene,
331                 loggingReason = loggingReason,
332             )
333         ) {
334             return
335         }
336 
337         logger.logSceneChanged(
338             from = currentSceneKey,
339             to = resolvedScene,
340             sceneState = null,
341             reason = loggingReason,
342             isInstant = true,
343         )
344 
345         repository.snapToScene(resolvedScene)
346         instantlyHideOverlay(Overlays.Bouncer, "Hiding on snapToScene for: ($loggingReason)")
347     }
348 
349     /**
350      * Request to show [overlay] so that it animates in from [currentScene] and ends up being
351      * visible on screen.
352      *
353      * After this returns, this overlay will be included in [currentOverlays]. This does nothing if
354      * [overlay] is already shown.
355      *
356      * @param overlay The overlay to be shown
357      * @param loggingReason The reason why the transition is requested, for logging purposes
358      * @param transitionKey The transition key for this animated transition
359      */
360     @JvmOverloads
361     fun showOverlay(
362         overlay: OverlayKey,
363         loggingReason: String,
364         transitionKey: TransitionKey? = null,
365     ) {
366         if (!validateOverlayChange(to = overlay, loggingReason = loggingReason)) {
367             return
368         }
369 
370         logger.logOverlayChangeRequested(to = overlay, reason = loggingReason)
371 
372         repository.showOverlay(overlay = overlay, transitionKey = transitionKey)
373     }
374 
375     /**
376      * Request to hide [overlay] so that it animates out to [currentScene] and ends up *not* being
377      * visible on screen.
378      *
379      * After this returns, this overlay will not be included in [currentOverlays]. This does nothing
380      * if [overlay] is already hidden.
381      *
382      * @param overlay The overlay to be hidden
383      * @param loggingReason The reason why the transition is requested, for logging purposes
384      * @param transitionKey The transition key for this animated transition
385      */
386     @JvmOverloads
387     fun hideOverlay(
388         overlay: OverlayKey,
389         loggingReason: String,
390         transitionKey: TransitionKey? = null,
391     ) {
392         if (!validateOverlayChange(from = overlay, loggingReason = loggingReason)) {
393             return
394         }
395 
396         logger.logOverlayChangeRequested(from = overlay, reason = loggingReason)
397 
398         repository.hideOverlay(overlay = overlay, transitionKey = transitionKey)
399     }
400 
401     /**
402      * Instantly shows [overlay].
403      *
404      * The change is instantaneous and not animated; it will be observable in the next frame and
405      * there will be no transition animation.
406      */
407     fun instantlyShowOverlay(overlay: OverlayKey, loggingReason: String) {
408         if (!validateOverlayChange(to = overlay, loggingReason = loggingReason)) {
409             return
410         }
411 
412         logger.logOverlayChangeRequested(to = overlay, reason = loggingReason)
413 
414         repository.instantlyShowOverlay(overlay)
415     }
416 
417     /**
418      * Instantly hides [overlay].
419      *
420      * The change is instantaneous and not animated; it will be observable in the next frame and
421      * there will be no transition animation.
422      */
423     fun instantlyHideOverlay(overlay: OverlayKey, loggingReason: String) {
424         if (!validateOverlayChange(from = overlay, loggingReason = loggingReason)) {
425             return
426         }
427 
428         logger.logOverlayChangeRequested(from = overlay, reason = loggingReason)
429 
430         repository.instantlyHideOverlay(overlay)
431     }
432 
433     /**
434      * Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up
435      * being visible.
436      *
437      * This throws if [from] is not currently shown or if [to] is already shown.
438      *
439      * @param from The overlay to be hidden, if any
440      * @param to The overlay to be shown, if any
441      * @param loggingReason The reason why the transition is requested, for logging purposes
442      * @param transitionKey The transition key for this animated transition
443      */
444     @JvmOverloads
445     fun replaceOverlay(
446         from: OverlayKey,
447         to: OverlayKey,
448         loggingReason: String,
449         transitionKey: TransitionKey? = null,
450     ) {
451         if (!validateOverlayChange(from = from, to = to, loggingReason = loggingReason)) {
452             return
453         }
454 
455         logger.logOverlayChangeRequested(from = from, to = to, reason = loggingReason)
456 
457         repository.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
458     }
459 
460     /**
461      * Sets the visibility of the container.
462      *
463      * Please do not call this from outside of the scene framework. If you are trying to force the
464      * visibility to visible or invisible, prefer making changes to the existing caller of this
465      * method or to upstream state used to calculate [isVisible]; for an example of the latter,
466      * please see [onRemoteUserInputStarted] and [onUserInputFinished].
467      */
468     fun setVisible(isVisible: Boolean, loggingReason: String) {
469         val wasVisible = repository.isVisible.value
470         if (wasVisible == isVisible) {
471             return
472         }
473 
474         logger.logVisibilityChange(from = wasVisible, to = isVisible, reason = loggingReason)
475         return repository.setVisible(isVisible)
476     }
477 
478     /**
479      * Notifies that a scene container user interaction has begun.
480      *
481      * This is a user interaction that originates within the Composable hierarchy of the scene
482      * container.
483      */
484     fun onSceneContainerUserInputStarted() {
485         repository.isSceneContainerUserInputOngoing.value = true
486     }
487 
488     /**
489      * Notifies that a remote user interaction has begun.
490      *
491      * This is a user interaction that originates outside of the UI of the scene container and
492      * possibly outside of the System UI process itself.
493      *
494      * As an example, consider the dragging that can happen in the launcher that expands the shade.
495      * This is a user interaction that begins remotely (as it starts in the launcher process) and is
496      * then rerouted by window manager to System UI. While the user interaction definitely continues
497      * within the System UI process and code, it also originates remotely.
498      */
499     fun onRemoteUserInputStarted(loggingReason: String) {
500         logger.logRemoteUserInputStarted(loggingReason)
501         repository.isRemoteUserInputOngoing.value = true
502     }
503 
504     /**
505      * Notifies that the current user interaction (internally or remotely started, see
506      * [onSceneContainerUserInputStarted] and [onRemoteUserInputStarted]) has finished.
507      */
508     fun onUserInputFinished() {
509         logger.logUserInputFinished()
510         repository.isSceneContainerUserInputOngoing.value = false
511         repository.isRemoteUserInputOngoing.value = false
512     }
513 
514     /**
515      * Binds the given flow so the system remembers it.
516      *
517      * Note that you must call is with `null` when the UI is done or risk a memory leak.
518      */
519     fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
520         repository.setTransitionState(transitionState)
521     }
522 
523     /**
524      * Returns the [concrete scene][Scenes] for [sceneKey] if it is a [scene family][SceneFamilies],
525      * otherwise returns a singleton [Flow] containing [sceneKey].
526      */
527     fun resolveSceneFamily(sceneKey: SceneKey): Flow<SceneKey> = flow {
528         emitAll(resolveSceneFamilyOrNull(sceneKey) ?: flowOf(sceneKey))
529     }
530 
531     /**
532      * Returns the [concrete scene][Scenes] for [sceneKey] if it is a [scene family][SceneFamilies],
533      * otherwise returns `null`.
534      */
535     fun resolveSceneFamilyOrNull(sceneKey: SceneKey): StateFlow<SceneKey>? =
536         sceneFamilyResolvers.get()[sceneKey]?.resolvedScene
537 
538     private fun isVisibleInternal(
539         raw: Boolean = repository.isVisible.value,
540         isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value,
541         activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value,
542     ): Boolean {
543         return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0
544     }
545 
546     /**
547      * Validates that the given scene change is allowed.
548      *
549      * Will throw a runtime exception for illegal states (for example, attempting to change to a
550      * scene that's not part of the current scene framework configuration).
551      *
552      * @param from The current scene being transitioned away from
553      * @param to The desired destination scene to transition to
554      * @param loggingReason The reason why the transition is requested, for logging purposes
555      * @return `true` if the scene change is valid; `false` if it shouldn't happen
556      */
557     private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean {
558         check(
559             !shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings)
560         ) {
561             "Can't change scene to ${to.debugName} when dual shade is on!"
562         }
563         check(!shadeModeInteractor.isSplitShade || (to != Scenes.QuickSettings)) {
564             "Can't change scene to ${to.debugName} in split shade mode!"
565         }
566 
567         if (from == to) {
568             logger.logSceneChangeRejection(
569                 from = from,
570                 to = to,
571                 originalChangeReason = loggingReason,
572                 rejectionReason = "${from.debugName} is the same as ${to.debugName}",
573             )
574             return false
575         }
576 
577         if (to !in repository.allContentKeys) {
578             logger.logSceneChangeRejection(
579                 from = from,
580                 to = to,
581                 originalChangeReason = loggingReason,
582                 rejectionReason = "${to.debugName} isn't present in allContentKeys",
583             )
584             return false
585         }
586 
587         if (disabledContentInteractor.isDisabled(to)) {
588             logger.logSceneChangeRejection(
589                 from = from,
590                 to = to,
591                 originalChangeReason = loggingReason,
592                 rejectionReason = "${to.debugName} is currently disabled",
593             )
594             return false
595         }
596 
597         val inMidTransitionFromGone =
598             (transitionState.value as? ObservableTransitionState.Transition)?.fromContent ==
599                 Scenes.Gone
600         val isChangeAllowed =
601             to != Scenes.Gone ||
602                 inMidTransitionFromGone ||
603                 deviceUnlockedInteractor.get().deviceUnlockStatus.value.isUnlocked ||
604                 !keyguardEnabledInteractor.get().isKeyguardEnabled.value
605         check(isChangeAllowed) {
606             "Cannot change to the Gone scene while the device is locked and not currently" +
607                 " transitioning from Gone. Current transition state is ${transitionState.value}." +
608                 " Logging reason for scene change was: $loggingReason"
609         }
610 
611         return true
612     }
613 
614     /**
615      * Validates that the given overlay change is allowed.
616      *
617      * Will throw a runtime exception for illegal states.
618      *
619      * @param from The overlay to be hidden, if any
620      * @param to The overlay to be shown, if any
621      * @param loggingReason The reason why the transition is requested, for logging purposes
622      * @return `true` if the scene change is valid; `false` if it shouldn't happen
623      */
624     private fun validateOverlayChange(
625         from: OverlayKey? = null,
626         to: OverlayKey? = null,
627         loggingReason: String,
628     ): Boolean {
629         check(from != null || to != null) {
630             "No overlay key provided for requested change." +
631                 " Current transition state is ${transitionState.value}." +
632                 " Logging reason for overlay change was: $loggingReason"
633         }
634 
635         check(
636             shadeModeInteractor.isDualShade ||
637                 (to != Overlays.NotificationsShade && to != Overlays.QuickSettingsShade)
638         ) {
639             "Can't show overlay ${to?.debugName} when dual shade is off!"
640         }
641 
642         if (to != null && disabledContentInteractor.isDisabled(to)) {
643             logger.logSceneChangeRejection(
644                 from = from,
645                 to = to,
646                 originalChangeReason = loggingReason,
647                 rejectionReason = "${to.debugName} is currently disabled",
648             )
649             return false
650         }
651 
652         return when {
653             to != null && from != null && to == from -> {
654                 logger.logSceneChangeRejection(
655                     from = from,
656                     to = to,
657                     originalChangeReason = loggingReason,
658                     rejectionReason = "${from.debugName} is the same as ${to.debugName}",
659                 )
660                 false
661             }
662 
663             to != null && to !in repository.allContentKeys -> {
664                 logger.logSceneChangeRejection(
665                     from = from,
666                     to = to,
667                     originalChangeReason = loggingReason,
668                     rejectionReason = "${to.debugName} is not in allContentKeys",
669                 )
670                 false
671             }
672 
673             from != null && from !in currentOverlays.value -> {
674                 logger.logSceneChangeRejection(
675                     from = from,
676                     to = to,
677                     originalChangeReason = loggingReason,
678                     rejectionReason = "${from.debugName} is not a current overlay",
679                 )
680                 false
681             }
682 
683             to != null && to in currentOverlays.value -> {
684                 logger.logSceneChangeRejection(
685                     from = from,
686                     to = to,
687                     originalChangeReason = loggingReason,
688                     rejectionReason = "${to.debugName} is already a current overlay",
689                 )
690                 false
691             }
692 
693             to == Overlays.Bouncer && currentScene.value == Scenes.Gone -> {
694                 logger.logSceneChangeRejection(
695                     from = from,
696                     to = to,
697                     originalChangeReason = loggingReason,
698                     rejectionReason = "Cannot show Bouncer over Gone scene",
699                 )
700                 false
701             }
702 
703             else -> true
704         }
705     }
706 
707     /** Returns a flow indicating if the currently visible scene can be resolved from [family]. */
708     fun isCurrentSceneInFamily(family: SceneKey): Flow<Boolean> =
709         currentScene.map { currentScene -> isSceneInFamily(currentScene, family) }
710 
711     /** Returns `true` if [scene] can be resolved from [family]. */
712     fun isSceneInFamily(scene: SceneKey, family: SceneKey): Boolean =
713         sceneFamilyResolvers.get()[family]?.includesScene(scene) == true
714 
715     /**
716      * Returns a filtered version of [unfiltered], without action-result entries that would navigate
717      * to disabled scenes.
718      */
719     fun filteredUserActions(
720         unfiltered: Flow<Map<UserAction, UserActionResult>>
721     ): Flow<Map<UserAction, UserActionResult>> {
722         return disabledContentInteractor.filteredUserActions(unfiltered)
723     }
724 
725     /**
726      * Notifies that a transition animation has started.
727      *
728      * The scene container will remain visible while any transition animation is running within it.
729      */
730     fun onTransitionAnimationStart() {
731         repository.activeTransitionAnimationCount.update { current ->
732             (current + 1).also {
733                 check(it < 10) {
734                     "Number of active transition animations is too high. Something must be" +
735                         " calling onTransitionAnimationStart too many times!"
736                 }
737             }
738         }
739     }
740 
741     /**
742      * Notifies that a transition animation has ended.
743      *
744      * The scene container will remain visible while any transition animation is running within it.
745      */
746     fun onTransitionAnimationEnd() {
747         decrementActiveTransitionAnimationCount()
748     }
749 
750     /**
751      * Notifies that a transition animation has been canceled.
752      *
753      * The scene container will remain visible while any transition animation is running within it.
754      */
755     fun onTransitionAnimationCancelled() {
756         decrementActiveTransitionAnimationCount()
757     }
758 
759     suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) {
760         coroutineScope {
761             launch {
762                 currentScene
763                     .map { sceneKey -> DiffableSceneKey(key = sceneKey) }
764                     .pairwise()
765                     .collect { (prev, current) ->
766                         tableLogBuffer.logDiffs(prevVal = prev, newVal = current)
767                     }
768             }
769 
770             launch {
771                 currentOverlays
772                     .map { overlayKeys -> DiffableOverlayKeys(keys = overlayKeys) }
773                     .pairwise()
774                     .collect { (prev, current) ->
775                         tableLogBuffer.logDiffs(prevVal = prev, newVal = current)
776                     }
777             }
778         }
779     }
780 
781     private fun decrementActiveTransitionAnimationCount() {
782         repository.activeTransitionAnimationCount.update { current ->
783             (current - 1).also {
784                 check(it >= 0) {
785                     "Number of active transition animations is negative. Something must be" +
786                         " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" +
787                         " many times!"
788                 }
789             }
790         }
791     }
792 
793     private class DiffableSceneKey(private val key: SceneKey) : Diffable<DiffableSceneKey> {
794         override fun logDiffs(prevVal: DiffableSceneKey, row: TableRowLogger) {
795             row.logChange(columnName = "currentScene", value = key.debugName)
796         }
797     }
798 
799     private class DiffableOverlayKeys(private val keys: Set<OverlayKey>) :
800         Diffable<DiffableOverlayKeys> {
801         override fun logDiffs(prevVal: DiffableOverlayKeys, row: TableRowLogger) {
802             row.logChange(
803                 columnName = "currentOverlays",
804                 value = keys.joinToString { key -> key.debugName },
805             )
806         }
807     }
808 
809     /**
810      * Based off of the ordering of [allContentKeys], returns the key of the highest z-order content
811      * out of [content].
812      */
813     private fun determineTopmostContent(content: Set<ContentKey>): ContentKey {
814         // Assuming allContentKeys is sorted by ascending z-order.
815         return checkNotNull(allContentKeys.findLast { it in content }) {
816             "Could not find unknown content $content in allContentKeys $allContentKeys"
817         }
818     }
819 
820     /** Optimization for common case where overlays is empty. */
821     private fun determineTopmostContent(scene: SceneKey, overlays: Set<OverlayKey>): ContentKey {
822         return if (overlays.isEmpty()) {
823             scene
824         } else {
825             determineTopmostContent(overlays)
826         }
827     }
828 
829     /**
830      * The current content that has the highest z-order out of all currently shown scenes and
831      * overlays.
832      *
833      * Note that during a transition between content, a different content may have the highest z-
834      * order. Only the one provided by this flow is considered the current logical topmost content.
835      */
836     @Deprecated("Only to be used for compatibility with KeyguardTransitionFramework")
837     val topmostContent: StateFlow<ContentKey> =
838         combine(currentScene, currentOverlays, ::determineTopmostContent)
839             .stateInTraced(
840                 name = "topmostContent",
841                 scope = applicationScope,
842                 started = SharingStarted.Eagerly,
843                 initialValue = determineTopmostContent(currentScene.value, currentOverlays.value),
844             )
845 }
846