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