1 /*
<lambda>null2  * Copyright 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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.scene.domain.startable
20 
21 import android.app.StatusBarManager
22 import com.android.compose.animation.scene.ObservableTransitionState
23 import com.android.compose.animation.scene.SceneKey
24 import com.android.internal.logging.UiEventLogger
25 import com.android.systemui.CoreStartable
26 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
27 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
28 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
29 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
30 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
31 import com.android.systemui.classifier.FalsingCollector
32 import com.android.systemui.classifier.FalsingCollectorActual
33 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
34 import com.android.systemui.dagger.SysUISingleton
35 import com.android.systemui.dagger.qualifiers.Application
36 import com.android.systemui.dagger.qualifiers.DisplayId
37 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
38 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
39 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
40 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
41 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
42 import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor
43 import com.android.systemui.model.SceneContainerPlugin
44 import com.android.systemui.model.SysUiState
45 import com.android.systemui.model.updateFlags
46 import com.android.systemui.plugins.FalsingManager
47 import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener
48 import com.android.systemui.power.domain.interactor.PowerInteractor
49 import com.android.systemui.power.shared.model.WakeSleepReason
50 import com.android.systemui.scene.data.model.asIterable
51 import com.android.systemui.scene.domain.interactor.SceneBackInteractor
52 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
53 import com.android.systemui.scene.domain.interactor.SceneInteractor
54 import com.android.systemui.scene.session.shared.SessionStorage
55 import com.android.systemui.scene.shared.flag.SceneContainerFlag
56 import com.android.systemui.scene.shared.logger.SceneLogger
57 import com.android.systemui.scene.shared.model.Scenes
58 import com.android.systemui.shade.domain.interactor.ShadeInteractor
59 import com.android.systemui.statusbar.NotificationShadeWindowController
60 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
61 import com.android.systemui.statusbar.phone.CentralSurfaces
62 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
63 import com.android.systemui.util.asIndenting
64 import com.android.systemui.util.kotlin.getOrNull
65 import com.android.systemui.util.kotlin.pairwise
66 import com.android.systemui.util.kotlin.sample
67 import com.android.systemui.util.printSection
68 import com.android.systemui.util.println
69 import dagger.Lazy
70 import java.io.PrintWriter
71 import java.util.Optional
72 import javax.inject.Inject
73 import kotlinx.coroutines.CoroutineScope
74 import kotlinx.coroutines.ExperimentalCoroutinesApi
75 import kotlinx.coroutines.channels.awaitClose
76 import kotlinx.coroutines.flow.SharingStarted
77 import kotlinx.coroutines.flow.collectLatest
78 import kotlinx.coroutines.flow.combine
79 import kotlinx.coroutines.flow.distinctUntilChanged
80 import kotlinx.coroutines.flow.distinctUntilChangedBy
81 import kotlinx.coroutines.flow.emptyFlow
82 import kotlinx.coroutines.flow.filter
83 import kotlinx.coroutines.flow.filterIsInstance
84 import kotlinx.coroutines.flow.filterNot
85 import kotlinx.coroutines.flow.first
86 import kotlinx.coroutines.flow.flatMapLatest
87 import kotlinx.coroutines.flow.flowOf
88 import kotlinx.coroutines.flow.map
89 import kotlinx.coroutines.flow.mapNotNull
90 import kotlinx.coroutines.flow.stateIn
91 import kotlinx.coroutines.launch
92 
93 /**
94  * Hooks up business logic that manipulates the state of the [SceneInteractor] for the system UI
95  * scene container based on state from other systems.
96  */
97 @SysUISingleton
98 class SceneContainerStartable
99 @Inject
100 constructor(
101     @Application private val applicationScope: CoroutineScope,
102     private val sceneInteractor: SceneInteractor,
103     private val deviceEntryInteractor: DeviceEntryInteractor,
104     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
105     private val bouncerInteractor: BouncerInteractor,
106     private val keyguardInteractor: KeyguardInteractor,
107     private val sysUiState: SysUiState,
108     @DisplayId private val displayId: Int,
109     private val sceneLogger: SceneLogger,
110     @FalsingCollectorActual private val falsingCollector: FalsingCollector,
111     private val falsingManager: FalsingManager,
112     private val powerInteractor: PowerInteractor,
113     private val simBouncerInteractor: Lazy<SimBouncerInteractor>,
114     private val authenticationInteractor: Lazy<AuthenticationInteractor>,
115     private val windowController: NotificationShadeWindowController,
116     private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
117     private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>,
118     private val headsUpInteractor: HeadsUpNotificationInteractor,
119     private val occlusionInteractor: SceneContainerOcclusionInteractor,
120     private val faceUnlockInteractor: DeviceEntryFaceAuthInteractor,
121     private val shadeInteractor: ShadeInteractor,
122     private val uiEventLogger: UiEventLogger,
123     private val sceneBackInteractor: SceneBackInteractor,
124     private val shadeSessionStorage: SessionStorage,
125     private val windowMgrLockscreenVisInteractor: WindowManagerLockscreenVisibilityInteractor,
126 ) : CoreStartable {
127     private val centralSurfaces: CentralSurfaces?
128         get() = centralSurfacesOptLazy.get().getOrNull()
129 
130     override fun start() {
131         if (SceneContainerFlag.isEnabled) {
132             sceneLogger.logFrameworkEnabled(isEnabled = true)
133             hydrateVisibility()
134             automaticallySwitchScenes()
135             hydrateSystemUiState()
136             collectFalsingSignals()
137             respondToFalsingDetections()
138             hydrateInteractionState()
139             handleBouncerOverscroll()
140             hydrateWindowController()
141             hydrateBackStack()
142             resetShadeSessions()
143         } else {
144             sceneLogger.logFrameworkEnabled(
145                 isEnabled = false,
146                 reason = SceneContainerFlag.requirementDescription(),
147             )
148         }
149     }
150 
151     override fun dump(pw: PrintWriter, args: Array<out String>) =
152         pw.asIndenting().run {
153             printSection("SceneContainerFlag") {
154                 println("isEnabled", SceneContainerFlag.isEnabled)
155                 printSection("requirementDescription") {
156                     println(SceneContainerFlag.requirementDescription())
157                 }
158             }
159         }
160 
161     private fun resetShadeSessions() {
162         applicationScope.launch {
163             sceneBackInteractor.backStack
164                 // We are in a session if either Shade or QuickSettings is on the back stack
165                 .map { backStack ->
166                     backStack.asIterable().any { it == Scenes.Shade || it == Scenes.QuickSettings }
167                 }
168                 .distinctUntilChanged()
169                 // Once a session has ended, clear the session storage.
170                 .filter { inSession -> !inSession }
171                 .collect { shadeSessionStorage.clear() }
172         }
173     }
174 
175     /** Updates the visibility of the scene container. */
176     private fun hydrateVisibility() {
177         applicationScope.launch {
178             // TODO(b/296114544): Combine with some global hun state to make it visible!
179             deviceProvisioningInteractor.isDeviceProvisioned
180                 .distinctUntilChanged()
181                 .flatMapLatest { isAllowedToBeVisible ->
182                     if (isAllowedToBeVisible) {
183                         combine(
184                                 sceneInteractor.transitionState.mapNotNull { state ->
185                                     when (state) {
186                                         is ObservableTransitionState.Idle -> {
187                                             if (state.currentScene != Scenes.Gone) {
188                                                 true to "scene is not Gone"
189                                             } else {
190                                                 false to "scene is Gone"
191                                             }
192                                         }
193                                         is ObservableTransitionState.Transition -> {
194                                             if (state.fromScene == Scenes.Gone) {
195                                                 true to "scene transitioning away from Gone"
196                                             } else {
197                                                 null
198                                             }
199                                         }
200                                     }
201                                 },
202                                 headsUpInteractor.isHeadsUpOrAnimatingAway,
203                                 occlusionInteractor.invisibleDueToOcclusion,
204                             ) {
205                                 visibilityForTransitionState,
206                                 isHeadsUpOrAnimatingAway,
207                                 invisibleDueToOcclusion,
208                                 ->
209                                 when {
210                                     isHeadsUpOrAnimatingAway -> true to "showing a HUN"
211                                     invisibleDueToOcclusion -> false to "invisible due to occlusion"
212                                     else -> visibilityForTransitionState
213                                 }
214                             }
215                             .distinctUntilChanged()
216                     } else {
217                         flowOf(false to "Device not provisioned or Factory Reset Protection active")
218                     }
219                 }
220                 .collect { (isVisible, loggingReason) ->
221                     sceneInteractor.setVisible(isVisible, loggingReason)
222                 }
223         }
224     }
225 
226     /** Switches between scenes based on ever-changing application state. */
227     private fun automaticallySwitchScenes() {
228         handleBouncerImeVisibility()
229         handleSimUnlock()
230         handleDeviceUnlockStatus()
231         handlePowerState()
232         handleShadeTouchability()
233         handleSurfaceBehindKeyguardVisibility()
234     }
235 
236     private fun handleSurfaceBehindKeyguardVisibility() {
237         applicationScope.launch {
238             sceneInteractor.currentScene.collectLatest { currentScene ->
239                 if (currentScene == Scenes.Lockscreen) {
240                     // Wait for surface to become visible
241                     windowMgrLockscreenVisInteractor.surfaceBehindVisibility.first { it }
242                     // Make sure the device is actually unlocked before force-changing the scene
243                     deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
244                     // Override the current transition, if any, by forcing the scene to Gone
245                     sceneInteractor.changeScene(
246                         toScene = Scenes.Gone,
247                         loggingReason = "surface behind keyguard is visible",
248                     )
249                 }
250             }
251         }
252     }
253 
254     private fun handleBouncerImeVisibility() {
255         applicationScope.launch {
256             // TODO (b/308001302): Move this to a bouncer specific interactor.
257             bouncerInteractor.onImeHiddenByUser.collectLatest {
258                 if (sceneInteractor.currentScene.value == Scenes.Bouncer) {
259                     sceneInteractor.changeScene(
260                         toScene = Scenes.Lockscreen, // TODO(b/336581871): add sceneState?
261                         loggingReason = "IME hidden",
262                     )
263                 }
264             }
265         }
266     }
267 
268     private fun handleSimUnlock() {
269         applicationScope.launch {
270             simBouncerInteractor
271                 .get()
272                 .isAnySimSecure
273                 .sample(deviceUnlockedInteractor.deviceUnlockStatus, ::Pair)
274                 .collect { (isAnySimLocked, unlockStatus) ->
275                     when {
276                         isAnySimLocked -> {
277                             switchToScene(
278                                 // TODO(b/336581871): add sceneState?
279                                 targetSceneKey = Scenes.Bouncer,
280                                 loggingReason = "Need to authenticate locked SIM card."
281                             )
282                         }
283                         unlockStatus.isUnlocked &&
284                             deviceEntryInteractor.canSwipeToEnter.value == false -> {
285                             switchToScene(
286                                 // TODO(b/336581871): add sceneState?
287                                 targetSceneKey = Scenes.Gone,
288                                 loggingReason =
289                                     "All SIM cards unlocked and device already unlocked and " +
290                                         "lockscreen doesn't require a swipe to dismiss."
291                             )
292                         }
293                         else -> {
294                             switchToScene(
295                                 // TODO(b/336581871): add sceneState?
296                                 targetSceneKey = Scenes.Lockscreen,
297                                 loggingReason =
298                                     "All SIM cards unlocked and device still locked" +
299                                         " or lockscreen still requires a swipe to dismiss."
300                             )
301                         }
302                     }
303                 }
304         }
305     }
306 
307     private fun handleDeviceUnlockStatus() {
308         applicationScope.launch {
309             // Track the previous scene (sans Bouncer), so that we know where to go when the device
310             // is unlocked whilst on the bouncer.
311             val previousScene =
312                 sceneBackInteractor.backScene
313                     .filterNot { it == Scenes.Bouncer }
314                     .stateIn(this, SharingStarted.Eagerly, initialValue = null)
315             deviceUnlockedInteractor.deviceUnlockStatus
316                 .mapNotNull { deviceUnlockStatus ->
317                     val renderedScenes =
318                         when (val transitionState = sceneInteractor.transitionState.value) {
319                             is ObservableTransitionState.Idle -> setOf(transitionState.currentScene)
320                             is ObservableTransitionState.Transition ->
321                                 setOf(
322                                     transitionState.fromScene,
323                                     transitionState.toScene,
324                                 )
325                         }
326                     val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen)
327                     val isOnBouncer = renderedScenes.contains(Scenes.Bouncer)
328                     if (!deviceUnlockStatus.isUnlocked) {
329                         return@mapNotNull if (isOnLockscreen || isOnBouncer) {
330                             // Already on lockscreen or bouncer, no need to change scenes.
331                             null
332                         } else {
333                             // The device locked while on a scene that's not Lockscreen or Bouncer,
334                             // go to Lockscreen.
335                             Scenes.Lockscreen to
336                                 "device locked in non-Lockscreen and non-Bouncer scene"
337                         }
338                     }
339 
340                     if (
341                         isOnBouncer &&
342                             deviceUnlockStatus.deviceUnlockSource == DeviceUnlockSource.TrustAgent
343                     ) {
344                         uiEventLogger.log(BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS)
345                     }
346                     when {
347                         isOnBouncer ->
348                             // When the device becomes unlocked in Bouncer, go to previous scene,
349                             // or Gone.
350                             if (previousScene.value == Scenes.Lockscreen) {
351                                 Scenes.Gone to "device was unlocked in Bouncer scene"
352                             } else {
353                                 val prevScene = previousScene.value
354                                 (prevScene ?: Scenes.Gone) to
355                                     "device was unlocked in Bouncer scene, from sceneKey=$prevScene"
356                             }
357                         isOnLockscreen ->
358                             // The lockscreen should be dismissed automatically in 2 scenarios:
359                             // 1. When face auth bypass is enabled and authentication happens while
360                             //    the user is on the lockscreen.
361                             // 2. Whenever the user authenticates using an active authentication
362                             //    mechanism like fingerprint auth. Since canSwipeToEnter is true
363                             //    when the user is passively authenticated, the false value here
364                             //    when the unlock state changes indicates this is an active
365                             //    authentication attempt.
366                             when {
367                                 deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen ==
368                                     true ->
369                                     Scenes.Gone to
370                                         "device has been unlocked on lockscreen with bypass " +
371                                             "enabled or using an active authentication " +
372                                             "mechanism: ${deviceUnlockStatus.deviceUnlockSource}"
373                                 else -> null
374                             }
375                         // Not on lockscreen or bouncer, so remain in the current scene.
376                         else -> null
377                     }
378                 }
379                 .collect { (targetSceneKey, loggingReason) ->
380                     switchToScene(
381                         targetSceneKey = targetSceneKey,
382                         loggingReason = loggingReason,
383                     )
384                 }
385         }
386     }
387 
388     private fun handlePowerState() {
389         applicationScope.launch {
390             powerInteractor.detailedWakefulness.collect { wakefulness ->
391                 // Detect a double-tap-power-button gesture that was started while the device was
392                 // still awake.
393                 if (wakefulness.isAsleep()) return@collect
394                 if (!wakefulness.powerButtonLaunchGestureTriggered) return@collect
395                 if (wakefulness.lastSleepReason != WakeSleepReason.POWER_BUTTON) return@collect
396 
397                 // If we're mid-transition from Gone to Lockscreen due to the first power button
398                 // press, then return to Gone.
399                 val transition: ObservableTransitionState.Transition =
400                     sceneInteractor.transitionState.value as? ObservableTransitionState.Transition
401                         ?: return@collect
402                 if (
403                     transition.fromScene == Scenes.Gone && transition.toScene == Scenes.Lockscreen
404                 ) {
405                     switchToScene(
406                         targetSceneKey = Scenes.Gone,
407                         loggingReason = "double-tap power gesture",
408                     )
409                 }
410             }
411         }
412         applicationScope.launch {
413             powerInteractor.isAsleep.collect { isAsleep ->
414                 if (isAsleep) {
415                     switchToScene(
416                         // TODO(b/336581871): add sceneState?
417                         targetSceneKey = Scenes.Lockscreen,
418                         loggingReason = "device is starting to sleep",
419                     )
420                 } else {
421                     val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
422                     val isUnlocked = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked
423                     if (isUnlocked && canSwipeToEnter == false) {
424                         val isTransitioningToLockscreen =
425                             sceneInteractor.transitioningTo.value == Scenes.Lockscreen
426                         if (!isTransitioningToLockscreen) {
427                             switchToScene(
428                                 targetSceneKey = Scenes.Gone,
429                                 loggingReason =
430                                     "device is waking up while unlocked without the ability to" +
431                                         " swipe up on lockscreen to enter and not on or" +
432                                         " transitioning to, the lockscreen scene.",
433                             )
434                         }
435                     } else if (
436                         authenticationInteractor.get().getAuthenticationMethod() ==
437                             AuthenticationMethodModel.Sim
438                     ) {
439                         switchToScene(
440                             targetSceneKey = Scenes.Bouncer,
441                             loggingReason = "device is starting to wake up with a locked sim",
442                         )
443                     }
444                 }
445             }
446         }
447     }
448 
449     private fun handleShadeTouchability() {
450         applicationScope.launch {
451             shadeInteractor.isShadeTouchable
452                 .distinctUntilChanged()
453                 .filter { !it }
454                 .collect {
455                     switchToScene(
456                         targetSceneKey = Scenes.Lockscreen,
457                         loggingReason = "device became non-interactive",
458                     )
459                 }
460         }
461     }
462 
463     /** Keeps [SysUiState] up-to-date */
464     private fun hydrateSystemUiState() {
465         applicationScope.launch {
466             combine(
467                     sceneInteractor.transitionState
468                         .mapNotNull { it as? ObservableTransitionState.Idle }
469                         .map { it.currentScene }
470                         .distinctUntilChanged(),
471                     occlusionInteractor.invisibleDueToOcclusion,
472                 ) { sceneKey, invisibleDueToOcclusion ->
473                     SceneContainerPlugin.SceneContainerPluginState(
474                         scene = sceneKey,
475                         invisibleDueToOcclusion = invisibleDueToOcclusion,
476                     )
477                 }
478                 .collect { sceneContainerPluginState ->
479                     sysUiState.updateFlags(
480                         displayId,
481                         *SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
482                                 flag to evaluator.invoke(sceneContainerPluginState)
483                             }
484                             .toTypedArray(),
485                     )
486                 }
487         }
488     }
489 
490     private fun hydrateWindowController() {
491         applicationScope.launch {
492             sceneInteractor.transitionState
493                 .mapNotNull { transitionState ->
494                     (transitionState as? ObservableTransitionState.Idle)?.currentScene
495                 }
496                 .distinctUntilChanged()
497                 .collect { sceneKey ->
498                     windowController.setNotificationShadeFocusable(sceneKey != Scenes.Gone)
499                 }
500         }
501 
502         applicationScope.launch {
503             deviceEntryInteractor.isDeviceEntered.collect { isDeviceEntered ->
504                 windowController.setKeyguardShowing(!isDeviceEntered)
505             }
506         }
507 
508         applicationScope.launch {
509             sceneInteractor.currentScene
510                 .map { it == Scenes.Bouncer }
511                 .distinctUntilChanged()
512                 .collect { isBouncerShowing ->
513                     windowController.setBouncerShowing(isBouncerShowing)
514                 }
515         }
516 
517         applicationScope.launch {
518             occlusionInteractor.invisibleDueToOcclusion.collect { invisibleDueToOcclusion ->
519                 windowController.setKeyguardOccluded(invisibleDueToOcclusion)
520             }
521         }
522     }
523 
524     /** Collects and reports signals into the falsing system. */
525     private fun collectFalsingSignals() {
526         applicationScope.launch {
527             deviceEntryInteractor.isDeviceEntered.collect { isLockscreenDismissed ->
528                 if (isLockscreenDismissed) {
529                     falsingCollector.onSuccessfulUnlock()
530                 }
531             }
532         }
533 
534         applicationScope.launch {
535             keyguardInteractor.isDozing.collect { isDozing ->
536                 falsingCollector.setShowingAod(isDozing)
537             }
538         }
539 
540         applicationScope.launch {
541             keyguardInteractor.isAodAvailable
542                 .flatMapLatest { isAodAvailable ->
543                     if (!isAodAvailable) {
544                         powerInteractor.detailedWakefulness
545                     } else {
546                         emptyFlow()
547                     }
548                 }
549                 .distinctUntilChangedBy { it.isAwake() }
550                 .collect { wakefulness ->
551                     when {
552                         wakefulness.isAwakeFromTouch() -> falsingCollector.onScreenOnFromTouch()
553                         wakefulness.isAwake() -> falsingCollector.onScreenTurningOn()
554                         wakefulness.isAsleep() -> falsingCollector.onScreenOff()
555                     }
556                 }
557         }
558 
559         applicationScope.launch {
560             sceneInteractor.currentScene
561                 .map { it == Scenes.Bouncer }
562                 .distinctUntilChanged()
563                 .collect { switchedToBouncerScene ->
564                     if (switchedToBouncerScene) {
565                         falsingCollector.onBouncerShown()
566                     } else {
567                         falsingCollector.onBouncerHidden()
568                     }
569                 }
570         }
571     }
572 
573     /** Switches to the lockscreen when falsing is detected. */
574     private fun respondToFalsingDetections() {
575         applicationScope.launch {
576             conflatedCallbackFlow {
577                     val listener = FalsingBeliefListener { trySend(Unit) }
578                     falsingManager.addFalsingBeliefListener(listener)
579                     awaitClose { falsingManager.removeFalsingBeliefListener(listener) }
580                 }
581                 .collect { switchToScene(Scenes.Lockscreen, "Falsing detected.") }
582         }
583     }
584 
585     /** Keeps the interaction state of [CentralSurfaces] up-to-date. */
586     private fun hydrateInteractionState() {
587         applicationScope.launch {
588             deviceUnlockedInteractor.deviceUnlockStatus
589                 .map { !it.isUnlocked }
590                 .flatMapLatest { isDeviceLocked ->
591                     if (isDeviceLocked) {
592                         sceneInteractor.transitionState
593                             .mapNotNull { it as? ObservableTransitionState.Idle }
594                             .map { it.currentScene }
595                             .distinctUntilChanged()
596                             .map { sceneKey ->
597                                 when (sceneKey) {
598                                     // When locked, showing the lockscreen scene should be reported
599                                     // as "interacting" while showing other scenes should report as
600                                     // "not interacting".
601                                     //
602                                     // This is done here in order to match the legacy
603                                     // implementation. The real reason why is lost to lore and myth.
604                                     Scenes.Lockscreen -> true
605                                     Scenes.Bouncer -> false
606                                     Scenes.Shade -> false
607                                     Scenes.NotificationsShade -> false
608                                     else -> null
609                                 }
610                             }
611                     } else {
612                         flowOf(null)
613                     }
614                 }
615                 .collect { isInteractingOrNull ->
616                     isInteractingOrNull?.let { isInteracting ->
617                         centralSurfaces?.setInteracting(
618                             StatusBarManager.WINDOW_STATUS_BAR,
619                             isInteracting,
620                         )
621                     }
622                 }
623         }
624     }
625 
626     private fun handleBouncerOverscroll() {
627         applicationScope.launch {
628             sceneInteractor.transitionState
629                 // Only consider transitions.
630                 .filterIsInstance<ObservableTransitionState.Transition>()
631                 // Only consider user-initiated (e.g. drags) that go from bouncer to lockscreen.
632                 .filter { transition ->
633                     transition.fromScene == Scenes.Bouncer &&
634                         transition.toScene == Scenes.Lockscreen &&
635                         transition.isInitiatedByUserInput
636                 }
637                 .flatMapLatest { it.progress }
638                 // Figure out the direction of scrolling.
639                 .map { progress ->
640                     when {
641                         progress > 0 -> 1
642                         progress < 0 -> -1
643                         else -> 0
644                     }
645                 }
646                 .distinctUntilChanged()
647                 // Only consider negative scrolling, AKA overscroll.
648                 .filter { it == -1 }
649                 .collect { faceUnlockInteractor.onSwipeUpOnBouncer() }
650         }
651     }
652 
653     private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) {
654         sceneInteractor.changeScene(
655             toScene = targetSceneKey,
656             loggingReason = loggingReason,
657         )
658     }
659 
660     private fun hydrateBackStack() {
661         applicationScope.launch {
662             sceneInteractor.currentScene.pairwise().collect { (from, to) ->
663                 sceneBackInteractor.onSceneChange(from = from, to = to)
664             }
665         }
666     }
667 }
668