• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.systemui.scene.domain.startable
18 
19 import android.app.StatusBarManager
20 import com.android.compose.animation.scene.ObservableTransitionState
21 import com.android.compose.animation.scene.OverlayKey
22 import com.android.compose.animation.scene.SceneKey
23 import com.android.internal.logging.UiEventLogger
24 import com.android.keyguard.AuthInteractionProperties
25 import com.android.systemui.CoreStartable
26 import com.android.systemui.Flags
27 import com.android.systemui.animation.ActivityTransitionAnimator
28 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
29 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
30 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
31 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
32 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
33 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
34 import com.android.systemui.classifier.FalsingCollector
35 import com.android.systemui.classifier.FalsingCollectorActual
36 import com.android.systemui.dagger.SysUISingleton
37 import com.android.systemui.dagger.qualifiers.Application
38 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
39 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
40 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
41 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
42 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
43 import com.android.systemui.keyguard.DismissCallbackRegistry
44 import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
45 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
46 import com.android.systemui.keyguard.domain.interactor.TrustInteractor
47 import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor.Companion.keyguardContent
48 import com.android.systemui.log.table.TableLogBuffer
49 import com.android.systemui.model.SceneContainerPlugin
50 import com.android.systemui.model.SysUiState
51 import com.android.systemui.model.updateFlags
52 import com.android.systemui.plugins.FalsingManager
53 import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener
54 import com.android.systemui.power.domain.interactor.PowerInteractor
55 import com.android.systemui.power.shared.model.WakeSleepReason
56 import com.android.systemui.scene.data.model.asIterable
57 import com.android.systemui.scene.data.model.sceneStackOf
58 import com.android.systemui.scene.domain.SceneFrameworkTableLog
59 import com.android.systemui.scene.domain.interactor.DisabledContentInteractor
60 import com.android.systemui.scene.domain.interactor.SceneBackInteractor
61 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
62 import com.android.systemui.scene.domain.interactor.SceneInteractor
63 import com.android.systemui.scene.session.shared.SessionStorage
64 import com.android.systemui.scene.shared.flag.SceneContainerFlag
65 import com.android.systemui.scene.shared.logger.SceneLogger
66 import com.android.systemui.scene.shared.model.Overlays
67 import com.android.systemui.scene.shared.model.SceneFamilies
68 import com.android.systemui.scene.shared.model.Scenes
69 import com.android.systemui.shade.domain.interactor.ShadeInteractor
70 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
71 import com.android.systemui.statusbar.NotificationShadeWindowController
72 import com.android.systemui.statusbar.SysuiStatusBarStateController
73 import com.android.systemui.statusbar.VibratorHelper
74 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
75 import com.android.systemui.statusbar.phone.CentralSurfaces
76 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
77 import com.android.systemui.util.asIndenting
78 import com.android.systemui.util.kotlin.getOrNull
79 import com.android.systemui.util.kotlin.pairwise
80 import com.android.systemui.util.kotlin.sample
81 import com.android.systemui.util.printSection
82 import com.android.systemui.util.println
83 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
84 import com.google.android.msdl.data.model.MSDLToken
85 import com.google.android.msdl.domain.MSDLPlayer
86 import dagger.Lazy
87 import java.io.PrintWriter
88 import java.util.Optional
89 import javax.inject.Inject
90 import kotlinx.coroutines.CoroutineScope
91 import kotlinx.coroutines.channels.awaitClose
92 import kotlinx.coroutines.coroutineScope
93 import kotlinx.coroutines.flow.Flow
94 import kotlinx.coroutines.flow.SharingStarted
95 import kotlinx.coroutines.flow.collectLatest
96 import kotlinx.coroutines.flow.combine
97 import kotlinx.coroutines.flow.distinctUntilChanged
98 import kotlinx.coroutines.flow.distinctUntilChangedBy
99 import kotlinx.coroutines.flow.filter
100 import kotlinx.coroutines.flow.filterIsInstance
101 import kotlinx.coroutines.flow.filterNotNull
102 import kotlinx.coroutines.flow.flatMapLatest
103 import kotlinx.coroutines.flow.flowOf
104 import kotlinx.coroutines.flow.map
105 import kotlinx.coroutines.flow.mapNotNull
106 import kotlinx.coroutines.flow.stateIn
107 import kotlinx.coroutines.launch
108 
109 /**
110  * Hooks up business logic that manipulates the state of the [SceneInteractor] for the system UI
111  * scene container based on state from other systems.
112  */
113 @SysUISingleton
114 class SceneContainerStartable
115 @Inject
116 constructor(
117     @Application private val applicationScope: CoroutineScope,
118     private val sceneInteractor: SceneInteractor,
119     private val deviceEntryInteractor: DeviceEntryInteractor,
120     private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor,
121     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
122     private val bouncerInteractor: BouncerInteractor,
123     private val keyguardInteractor: KeyguardInteractor,
124     private val sysUiState: SysUiState,
125     private val sceneLogger: SceneLogger,
126     @FalsingCollectorActual private val falsingCollector: FalsingCollector,
127     private val falsingManager: FalsingManager,
128     private val powerInteractor: PowerInteractor,
129     private val simBouncerInteractor: Lazy<SimBouncerInteractor>,
130     private val authenticationInteractor: Lazy<AuthenticationInteractor>,
131     private val windowController: NotificationShadeWindowController,
132     private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
133     private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>,
134     private val headsUpInteractor: HeadsUpNotificationInteractor,
135     private val occlusionInteractor: SceneContainerOcclusionInteractor,
136     private val faceUnlockInteractor: DeviceEntryFaceAuthInteractor,
137     private val shadeInteractor: ShadeInteractor,
138     private val uiEventLogger: UiEventLogger,
139     private val sceneBackInteractor: SceneBackInteractor,
140     private val shadeSessionStorage: SessionStorage,
141     private val keyguardEnabledInteractor: KeyguardEnabledInteractor,
142     private val dismissCallbackRegistry: DismissCallbackRegistry,
143     private val statusBarStateController: SysuiStatusBarStateController,
144     private val alternateBouncerInteractor: AlternateBouncerInteractor,
145     private val vibratorHelper: VibratorHelper,
146     private val msdlPlayer: MSDLPlayer,
147     private val disabledContentInteractor: DisabledContentInteractor,
148     private val activityTransitionAnimator: ActivityTransitionAnimator,
149     private val shadeModeInteractor: ShadeModeInteractor,
150     @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer,
151     private val trustInteractor: TrustInteractor,
152 ) : CoreStartable {
153     private val centralSurfaces: CentralSurfaces?
154         get() = centralSurfacesOptLazy.get().getOrNull()
155 
156     private val authInteractionProperties = AuthInteractionProperties()
157 
158     override fun start() {
159         if (SceneContainerFlag.isEnabled) {
160             sceneLogger.logFrameworkEnabled(isEnabled = true)
161             applicationScope.launch { hydrateTableLogBuffer() }
162             hydrateVisibility()
163             automaticallySwitchScenes()
164             hydrateSystemUiState()
165             collectFalsingSignals()
166             respondToFalsingDetections()
167             hydrateInteractionState()
168             handleBouncerOverscroll()
169             handleDeviceEntryHapticsWhileDeviceLocked()
170             hydrateWindowController()
171             hydrateBackStack()
172             resetShadeSessions()
173             handleKeyguardEnabledness()
174             notifyKeyguardDismissCancelledCallbacks()
175             refreshLockscreenEnabled()
176             hydrateActivityTransitionAnimationState()
177             lockWhenDeviceBecomesUntrusted()
178         } else {
179             sceneLogger.logFrameworkEnabled(
180                 isEnabled = false,
181                 reason = SceneContainerFlag.requirementDescription(),
182             )
183         }
184     }
185 
186     override fun dump(pw: PrintWriter, args: Array<out String>) {
187         with(pw.asIndenting()) {
188             printSection("SceneContainerFlag") {
189                 printSection("Framework availability") {
190                     println("isEnabled", SceneContainerFlag.isEnabled)
191                     println(SceneContainerFlag.requirementDescription())
192                 }
193 
194                 if (!SceneContainerFlag.isEnabled) {
195                     return
196                 }
197 
198                 printSection("Framework state") {
199                     println("isVisible", sceneInteractor.isVisible.value)
200                     println("currentScene", sceneInteractor.currentScene.value.debugName)
201                     println(
202                         "currentOverlays",
203                         sceneInteractor.currentOverlays.value.joinToString(", ") { overlay ->
204                             overlay.debugName
205                         },
206                     )
207                     println("backStack", sceneBackInteractor.backStack.value)
208                     println("shadeMode", shadeModeInteractor.shadeMode.value)
209                 }
210 
211                 printSection("Authentication state") {
212                     println("isKeyguardEnabled", keyguardEnabledInteractor.isKeyguardEnabled.value)
213                     println(
214                         "isUnlocked",
215                         deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked,
216                     )
217                     println("isDeviceEntered", deviceEntryInteractor.isDeviceEntered.value)
218                     println(
219                         "isFaceAuthEnabledAndEnrolled",
220                         faceUnlockInteractor.isFaceAuthEnabledAndEnrolled(),
221                     )
222                     println("canSwipeToEnter", deviceEntryInteractor.canSwipeToEnter.value)
223                 }
224 
225                 printSection("Power state") {
226                     println("detailedWakefulness", powerInteractor.detailedWakefulness.value)
227                     println("isDozing", keyguardInteractor.isDozing.value)
228                     println("isAodAvailable", keyguardInteractor.isAodAvailable.value)
229                 }
230             }
231         }
232     }
233 
234     private suspend fun hydrateTableLogBuffer() {
235         coroutineScope {
236             launch { sceneInteractor.hydrateTableLogBuffer(tableLogBuffer) }
237             launch { keyguardEnabledInteractor.hydrateTableLogBuffer(tableLogBuffer) }
238             launch { faceUnlockInteractor.hydrateTableLogBuffer(tableLogBuffer) }
239             launch { powerInteractor.hydrateTableLogBuffer(tableLogBuffer) }
240             launch { keyguardInteractor.hydrateTableLogBuffer(tableLogBuffer) }
241         }
242     }
243 
244     private fun resetShadeSessions() {
245         applicationScope.launch {
246             combine(
247                     sceneBackInteractor.backStack
248                         // We are in a session if either Shade or QuickSettings is on the back stack
249                         .map { backStack ->
250                             backStack.asIterable().any {
251                                 // TODO(b/356596436): Include overlays in the back stack as well.
252                                 it == Scenes.Shade || it == Scenes.QuickSettings
253                             }
254                         }
255                         .distinctUntilChanged(),
256                     // We are also in a session if either Notifications Shade or QuickSettings Shade
257                     // is currently shown (whether idle or animating).
258                     shadeInteractor.isAnyExpanded,
259                 ) { inBackStack, isShadeShown ->
260                     inBackStack || isShadeShown
261                 }
262                 // Once a session has ended, clear the session storage.
263                 .filter { inSession -> !inSession }
264                 .collect { shadeSessionStorage.clear() }
265         }
266     }
267 
268     /** Updates the visibility of the scene container. */
269     private fun hydrateVisibility() {
270         applicationScope.launch {
271             deviceProvisioningInteractor.isDeviceProvisioned
272                 .flatMapLatest { isAllowedToBeVisible ->
273                     if (isAllowedToBeVisible) {
274                         combine(
275                                 sceneInteractor.transitionState.mapNotNull { state ->
276                                     when (state) {
277                                         is ObservableTransitionState.Idle -> {
278                                             if (state.currentScene == Scenes.Dream) {
279                                                 false to "dream is showing"
280                                             } else if (state.currentScene != Scenes.Gone) {
281                                                 true to "scene is not Gone"
282                                             } else if (state.currentOverlays.isNotEmpty()) {
283                                                 true to "overlay is shown"
284                                             } else {
285                                                 false to "scene is Gone and no overlays are shown"
286                                             }
287                                         }
288                                         is ObservableTransitionState.Transition -> {
289                                             if (state.fromContent == Scenes.Gone) {
290                                                 true to "scene transitioning away from Gone"
291                                             } else if (state.fromContent == Scenes.Dream) {
292                                                 true to "scene transitioning away from dream"
293                                             } else {
294                                                 null
295                                             }
296                                         }
297                                     }
298                                 },
299                                 sceneInteractor.transitionState.map { state ->
300                                     state.isTransitioningFromOrTo(Scenes.Communal) ||
301                                         state.isIdle(Scenes.Communal)
302                                 },
303                                 headsUpInteractor.isHeadsUpOrAnimatingAway,
304                                 occlusionInteractor.invisibleDueToOcclusion,
305                                 alternateBouncerInteractor.isVisible,
306                             ) {
307                                 visibilityForTransitionState,
308                                 isCommunalShowing,
309                                 isHeadsUpOrAnimatingAway,
310                                 invisibleDueToOcclusion,
311                                 isAlternateBouncerVisible ->
312                                 when {
313                                     isCommunalShowing ->
314                                         true to "on or transitioning to/from communal"
315                                     isHeadsUpOrAnimatingAway -> true to "showing a HUN"
316                                     isAlternateBouncerVisible -> true to "showing alternate bouncer"
317                                     invisibleDueToOcclusion -> false to "invisible due to occlusion"
318                                     else -> visibilityForTransitionState
319                                 }
320                             }
321                             .distinctUntilChanged()
322                     } else {
323                         flowOf(false to "Device not provisioned or Factory Reset Protection active")
324                     }
325                 }
326                 .collect { (isVisible, loggingReason) ->
327                     sceneInteractor.setVisible(isVisible, loggingReason)
328                 }
329         }
330     }
331 
332     /** Switches between scenes based on ever-changing application state. */
333     private fun automaticallySwitchScenes() {
334         handleBouncerImeVisibility()
335         handleBouncerHiding()
336         handleSimUnlock()
337         handleDeviceUnlockStatus()
338         handlePowerState()
339         handleDreamState()
340         handleShadeTouchability()
341         handleDisableFlags()
342     }
343 
344     private fun handleBouncerImeVisibility() {
345         applicationScope.launch {
346             // TODO (b/308001302): Move this to a bouncer specific interactor.
347             bouncerInteractor.onImeHiddenByUser.collectLatest {
348                 sceneInteractor.hideOverlay(
349                     overlay = Overlays.Bouncer,
350                     loggingReason = "IME hidden.",
351                 )
352             }
353         }
354     }
355 
356     private fun handleBouncerHiding() {
357         applicationScope.launch {
358             repeatWhen(
359                 condition =
360                     authenticationInteractor
361                         .get()
362                         .authenticationMethod
363                         .map { !it.isSecure }
364                         .distinctUntilChanged()
365             ) {
366                 sceneInteractor.hideOverlay(
367                     overlay = Overlays.Bouncer,
368                     loggingReason = "Authentication method changed to a non-secure one.",
369                 )
370             }
371         }
372     }
373 
374     private fun handleSimUnlock() {
375         applicationScope.launch {
376             simBouncerInteractor
377                 .get()
378                 .isAnySimSecure
379                 .sample(deviceUnlockedInteractor.deviceUnlockStatus, ::Pair)
380                 .collect { (isAnySimLocked, unlockStatus) ->
381                     when {
382                         isAnySimLocked -> {
383                             sceneInteractor.showOverlay(
384                                 overlay = Overlays.Bouncer,
385                                 loggingReason = "Need to authenticate locked SIM card.",
386                             )
387                         }
388                         unlockStatus.isUnlocked &&
389                             deviceEntryInteractor.canSwipeToEnter.value == false -> {
390                             val loggingReason =
391                                 "All SIM cards unlocked and device already unlocked and " +
392                                     "lockscreen doesn't require a swipe to dismiss."
393                             switchToScene(
394                                 targetSceneKey = Scenes.Gone,
395                                 loggingReason = loggingReason,
396                             )
397                         }
398                         else -> {
399                             val loggingReason =
400                                 "All SIM cards unlocked and device still locked" +
401                                     " or lockscreen still requires a swipe to dismiss."
402                             switchToScene(
403                                 targetSceneKey = Scenes.Lockscreen,
404                                 loggingReason = loggingReason,
405                             )
406                         }
407                     }
408                 }
409         }
410     }
411 
412     private fun handleDeviceUnlockStatus() {
413         applicationScope.launch {
414             // Track the previous scene, so that we know where to go when the device is unlocked
415             // whilst on the bouncer.
416             val previousScene =
417                 sceneBackInteractor.backScene.stateIn(
418                     this,
419                     SharingStarted.Eagerly,
420                     initialValue = null,
421                 )
422             deviceUnlockedInteractor.deviceUnlockStatus
423                 .mapNotNull { deviceUnlockStatus ->
424                     val (renderedScenes: List<SceneKey>, renderedOverlays) =
425                         when (val transitionState = sceneInteractor.transitionState.value) {
426                             is ObservableTransitionState.Idle ->
427                                 listOf(transitionState.currentScene) to
428                                     transitionState.currentOverlays
429                             is ObservableTransitionState.Transition.ChangeScene ->
430                                 listOf(transitionState.fromScene, transitionState.toScene) to
431                                     transitionState.currentOverlays
432                             is ObservableTransitionState.Transition.OverlayTransition ->
433                                 listOf(transitionState.currentScene) to
434                                     setOfNotNull(
435                                         transitionState.toContent.takeIf { it is OverlayKey },
436                                         transitionState.fromContent.takeIf { it is OverlayKey },
437                                     )
438                         }
439                     val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen)
440                     val isAlternateBouncerVisible = alternateBouncerInteractor.isVisibleState()
441                     val isOnPrimaryBouncer = Overlays.Bouncer in renderedOverlays
442                     if (!deviceUnlockStatus.isUnlocked) {
443                         return@mapNotNull if (
444                             renderedScenes.any { it in keyguardContent } ||
445                                 Overlays.Bouncer in renderedOverlays
446                         ) {
447                             // Already on a keyguard scene or bouncer, no need to change scenes.
448                             null
449                         } else {
450                             // The device locked while on a scene that's not a keyguard scene, go
451                             // to Lockscreen.
452                             Scenes.Lockscreen to "device locked in a non-keyguard scene"
453                         }
454                     }
455 
456                     if (powerInteractor.detailedWakefulness.value.isAsleep()) {
457                         // The logic below is for when the device becomes unlocked. That must be a
458                         // no-op if the device is not awake.
459                         return@mapNotNull null
460                     }
461 
462                     if (
463                         isOnPrimaryBouncer &&
464                             deviceUnlockStatus.deviceUnlockSource == DeviceUnlockSource.TrustAgent
465                     ) {
466                         uiEventLogger.log(BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS)
467                     }
468                     when {
469                         isAlternateBouncerVisible -> {
470                             // When the device becomes unlocked when the alternate bouncer is
471                             // showing, always hide the alternate bouncer and notify dismiss
472                             // succeeded
473                             alternateBouncerInteractor.hide()
474                             dismissCallbackRegistry.notifyDismissSucceeded()
475 
476                             // ... and go to Gone or stay on the current scene
477                             if (
478                                 isOnLockscreen ||
479                                     !statusBarStateController.leaveOpenOnKeyguardHide()
480                             ) {
481                                 Scenes.Gone to
482                                     "device was unlocked with alternate bouncer showing" +
483                                         " and shade didn't need to be left open"
484                             } else {
485                                 replaceLockscreenSceneOnBackStack()
486                                 null
487                             }
488                         }
489                         isOnPrimaryBouncer -> {
490                             // When the device becomes unlocked in primary Bouncer,
491                             // notify dismiss succeeded and remain in current scene or switch to
492                             // Gone.
493                             dismissCallbackRegistry.notifyDismissSucceeded()
494                             // if transition is a scene change, take the destination scene
495                             val targetScene = renderedScenes.last()
496                             if (
497                                 targetScene == Scenes.Lockscreen ||
498                                     !statusBarStateController.leaveOpenOnKeyguardHide()
499                             ) {
500                                 Scenes.Gone to
501                                     "device was unlocked with bouncer showing and shade" +
502                                         " didn't need to be left open"
503                             } else {
504                                 if (previousScene.value != Scenes.Gone) {
505                                     replaceLockscreenSceneOnBackStack()
506                                 }
507                                 targetScene to
508                                     "device was unlocked with primary bouncer showing," +
509                                         " from sceneKey=$targetScene"
510                             }
511                         }
512                         isOnLockscreen ->
513                             // The lockscreen should be dismissed automatically in 2 scenarios:
514                             // 1. When face auth bypass is enabled and authentication happens while
515                             //    the user is on the lockscreen.
516                             // 2. Whenever the user authenticates using an active authentication
517                             //    mechanism like fingerprint auth. Since canSwipeToEnter is true
518                             //    when the user is passively authenticated, the false value here
519                             //    when the unlock state changes indicates this is an active
520                             //    authentication attempt.
521                             when {
522                                 deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen ==
523                                     true ->
524                                     Scenes.Gone to
525                                         "device has been unlocked on lockscreen with bypass " +
526                                             "enabled or using an active authentication " +
527                                             "mechanism: ${deviceUnlockStatus.deviceUnlockSource}"
528                                 else -> null
529                             }
530                         // Not on lockscreen or bouncer, so remain in the current scene but since
531                         // unlocked, replace the Lockscreen scene from the bottom of the navigation
532                         // back stack with the Gone scene.
533                         else -> {
534                             replaceLockscreenSceneOnBackStack()
535                             null
536                         }
537                     }
538                 }
539                 .collect { (targetSceneKey, loggingReason) ->
540                     switchToScene(targetSceneKey = targetSceneKey, loggingReason = loggingReason)
541                 }
542         }
543     }
544 
545     /**
546      * If the [Scenes.Lockscreen] is on the bottom of the navigation backstack, replaces it with
547      * [Scenes.Gone].
548      */
549     private fun replaceLockscreenSceneOnBackStack() {
550         sceneBackInteractor.updateBackStack { stack ->
551             val list = stack.asIterable().toMutableList()
552             if (list.lastOrNull() == Scenes.Lockscreen) {
553                 list[list.size - 1] = Scenes.Gone
554                 sceneStackOf(*list.toTypedArray())
555             } else {
556                 stack
557             }
558         }
559     }
560 
561     private fun handlePowerState() {
562         applicationScope.launch {
563             powerInteractor.detailedWakefulness.collect { wakefulness ->
564                 // Detect a double-tap-power-button gesture that was started while the device was
565                 // still awake.
566                 if (wakefulness.isAsleep()) return@collect
567                 if (!wakefulness.powerButtonLaunchGestureTriggered) return@collect
568                 if (wakefulness.lastSleepReason != WakeSleepReason.POWER_BUTTON) return@collect
569 
570                 // If we're mid-transition from Gone to Lockscreen due to the first power button
571                 // press, then return to Gone.
572                 val transition: ObservableTransitionState.Transition =
573                     sceneInteractor.transitionState.value as? ObservableTransitionState.Transition
574                         ?: return@collect
575                 if (
576                     transition.fromContent == Scenes.Gone &&
577                         transition.toContent == Scenes.Lockscreen
578                 ) {
579                     switchToScene(
580                         targetSceneKey = Scenes.Gone,
581                         loggingReason = "double-tap power gesture",
582                     )
583                 }
584             }
585         }
586         applicationScope.launch {
587             powerInteractor.isAsleep.collect { isAsleep ->
588                 if (isAsleep) {
589                     alternateBouncerInteractor.hide()
590                     dismissCallbackRegistry.notifyDismissCancelled()
591 
592                     switchToScene(
593                         targetSceneKey = Scenes.Lockscreen,
594                         loggingReason = "device is starting to sleep",
595                         sceneState = keyguardInteractor.asleepKeyguardState.value,
596                         freezeAndAnimateToCurrentState = true,
597                     )
598                 } else {
599                     val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
600                     val isUnlocked = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked
601                     if (isUnlocked && canSwipeToEnter == false) {
602                         val isTransitioningToLockscreen =
603                             sceneInteractor.transitioningTo.value == Scenes.Lockscreen
604                         if (!isTransitioningToLockscreen) {
605                             switchToScene(
606                                 targetSceneKey = Scenes.Gone,
607                                 loggingReason =
608                                     "device is waking up while unlocked without the ability to" +
609                                         " swipe up on lockscreen to enter and not on or" +
610                                         " transitioning to, the lockscreen scene.",
611                             )
612                         }
613                     } else if (
614                         authenticationInteractor.get().getAuthenticationMethod() ==
615                             AuthenticationMethodModel.Sim
616                     ) {
617                         sceneInteractor.showOverlay(
618                             overlay = Overlays.Bouncer,
619                             loggingReason = "device is starting to wake up with a locked sim",
620                         )
621                     }
622                 }
623             }
624         }
625     }
626 
627     private fun handleDreamState() {
628         applicationScope.launch {
629             keyguardInteractor.isAbleToDream
630                 .sample(sceneInteractor.transitionState, ::Pair)
631                 .collect { (isAbleToDream, transitionState) ->
632                     if (transitionState.isIdle(Scenes.Communal)) {
633                         // The dream is automatically started underneath the hub, don't transition
634                         // to dream when this is happening as communal is still visible on top.
635                         return@collect
636                     }
637                     if (isAbleToDream) {
638                         switchToScene(
639                             targetSceneKey = Scenes.Dream,
640                             loggingReason = "dream started",
641                         )
642                     } else {
643                         switchToScene(
644                             targetSceneKey = SceneFamilies.Home,
645                             loggingReason = "dream stopped",
646                         )
647                     }
648                 }
649         }
650     }
651 
652     private fun handleShadeTouchability() {
653         applicationScope.launch {
654             repeatWhen(deviceEntryInteractor.isDeviceEntered.map { !it }) {
655                 // Run logic only when the device isn't entered.
656                 repeatWhen(
657                     sceneInteractor.transitionState.map { !it.isTransitioning(to = Scenes.Gone) }
658                 ) {
659                     // Run logic only when not transitioning to gone.
660                     shadeInteractor.isShadeTouchable
661                         .distinctUntilChanged()
662                         .filter { !it }
663                         .collect {
664                             switchToScene(
665                                 targetSceneKey = Scenes.Lockscreen,
666                                 loggingReason =
667                                     "device became non-interactive (SceneContainerStartable)",
668                             )
669                         }
670                 }
671             }
672         }
673     }
674 
675     private fun handleDisableFlags() {
676         applicationScope.launch {
677             launch {
678                 sceneInteractor.currentScene.collectLatest { currentScene ->
679                     disabledContentInteractor.repeatWhenDisabled(currentScene) {
680                         switchToScene(
681                             targetSceneKey = SceneFamilies.Home,
682                             loggingReason =
683                                 "Current scene ${currentScene.debugName} became" + " disabled",
684                         )
685                     }
686                 }
687             }
688 
689             launch {
690                 sceneInteractor.currentOverlays.collectLatest { overlays ->
691                     overlays.forEach { overlay ->
692                         launch {
693                             disabledContentInteractor.repeatWhenDisabled(overlay) {
694                                 sceneInteractor.hideOverlay(
695                                     overlay = overlay,
696                                     loggingReason =
697                                         "Overlay ${overlay.debugName} became" + " disabled",
698                                 )
699                             }
700                         }
701                     }
702                 }
703             }
704         }
705     }
706 
707     private fun handleDeviceEntryHapticsWhileDeviceLocked() {
708         applicationScope.launch {
709             deviceEntryInteractor.isDeviceEntered.collectLatest { isDeviceEntered ->
710                 // Only check for haptics signals before device is entered
711                 if (!isDeviceEntered) {
712                     coroutineScope {
713                         launch {
714                             deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry
715                                 .sample(sceneInteractor.currentScene)
716                                 .collect { currentScene ->
717                                     if (Flags.msdlFeedback()) {
718                                         msdlPlayer.playToken(
719                                             MSDLToken.UNLOCK,
720                                             authInteractionProperties,
721                                         )
722                                     } else {
723                                         vibratorHelper.vibrateAuthSuccess(
724                                             "$TAG, $currentScene device-entry::success"
725                                         )
726                                     }
727                                 }
728                         }
729 
730                         launch {
731                             deviceEntryHapticsInteractor.playErrorHaptic
732                                 .sample(sceneInteractor.currentScene)
733                                 .collect { currentScene ->
734                                     if (Flags.msdlFeedback()) {
735                                         msdlPlayer.playToken(
736                                             MSDLToken.FAILURE,
737                                             authInteractionProperties,
738                                         )
739                                     } else {
740                                         vibratorHelper.vibrateAuthError(
741                                             "$TAG, $currentScene device-entry::error"
742                                         )
743                                     }
744                                 }
745                         }
746                     }
747                 }
748             }
749         }
750     }
751 
752     /** Keeps [SysUiState] up-to-date */
753     private fun hydrateSystemUiState() {
754         applicationScope.launch {
755             combine(
756                     sceneInteractor.transitionState
757                         .mapNotNull { it as? ObservableTransitionState.Idle }
758                         .distinctUntilChanged(),
759                     sceneInteractor.isVisible,
760                     occlusionInteractor.invisibleDueToOcclusion,
761                 ) { idleState, isVisible, invisibleDueToOcclusion ->
762                     SceneContainerPlugin.SceneContainerPluginState(
763                         scene = idleState.currentScene,
764                         overlays = idleState.currentOverlays,
765                         isVisible = isVisible,
766                         invisibleDueToOcclusion = invisibleDueToOcclusion,
767                     )
768                 }
769                 .map { sceneContainerPluginState ->
770                     SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
771                             flag to evaluator(sceneContainerPluginState)
772                         }
773                         .toMap()
774                 }
775                 .distinctUntilChanged()
776                 .collect { flags ->
777                     sysUiState.updateFlags(
778                         *(flags.entries.map { (key, value) -> key to value }).toTypedArray()
779                     )
780                 }
781         }
782     }
783 
784     private fun hydrateWindowController() {
785         applicationScope.launch {
786             sceneInteractor.transitionState
787                 .mapNotNull { transitionState ->
788                     (transitionState as? ObservableTransitionState.Idle)?.currentScene
789                 }
790                 .distinctUntilChanged()
791                 .collect { sceneKey ->
792                     windowController.setNotificationShadeFocusable(sceneKey != Scenes.Gone)
793                 }
794         }
795 
796         applicationScope.launch {
797             deviceEntryInteractor.isDeviceEntered.collect { isDeviceEntered ->
798                 windowController.setKeyguardShowing(!isDeviceEntered)
799             }
800         }
801 
802         applicationScope.launch {
803             occlusionInteractor.invisibleDueToOcclusion.collect { invisibleDueToOcclusion ->
804                 windowController.setKeyguardOccluded(invisibleDueToOcclusion)
805             }
806         }
807     }
808 
809     /** Collects and reports signals into the falsing system. */
810     private fun collectFalsingSignals() {
811         applicationScope.launch {
812             deviceEntryInteractor.isDeviceEntered.collect { isLockscreenDismissed ->
813                 if (isLockscreenDismissed) {
814                     falsingCollector.onSuccessfulUnlock()
815                 }
816             }
817         }
818 
819         applicationScope.launch {
820             keyguardInteractor.isDozing.collect { isDozing ->
821                 falsingCollector.setShowingAod(isDozing)
822             }
823         }
824 
825         applicationScope.launch {
826             powerInteractor.detailedWakefulness
827                 .distinctUntilChangedBy { it.isAwake() }
828                 .collect { wakefulness ->
829                     when {
830                         wakefulness.isAwakeFromTouch() -> falsingCollector.onScreenOnFromTouch()
831                         wakefulness.isAwake() -> falsingCollector.onScreenTurningOn()
832                         wakefulness.isAsleep() -> falsingCollector.onScreenOff()
833                     }
834                 }
835         }
836 
837         applicationScope.launch {
838             sceneInteractor.currentOverlays
839                 .map { Overlays.Bouncer in it }
840                 .distinctUntilChanged()
841                 .collect { switchedToBouncerOverlay ->
842                     if (switchedToBouncerOverlay) {
843                         falsingCollector.onBouncerShown()
844                     } else {
845                         falsingCollector.onBouncerHidden()
846                     }
847                 }
848         }
849     }
850 
851     /** Switches to the lockscreen when falsing is detected. */
852     private fun respondToFalsingDetections() {
853         applicationScope.launch {
854             conflatedCallbackFlow {
855                     val listener = FalsingBeliefListener { trySend(Unit) }
856                     falsingManager.addFalsingBeliefListener(listener)
857                     awaitClose { falsingManager.removeFalsingBeliefListener(listener) }
858                 }
859                 .collect {
860                     val loggingReason = "Falsing detected."
861                     switchToScene(targetSceneKey = Scenes.Lockscreen, loggingReason = loggingReason)
862                 }
863         }
864     }
865 
866     /** Keeps the interaction state of [CentralSurfaces] up-to-date. */
867     private fun hydrateInteractionState() {
868         applicationScope.launch {
869             deviceUnlockedInteractor.deviceUnlockStatus
870                 .map { !it.isUnlocked }
871                 .flatMapLatest { isDeviceLocked ->
872                     if (isDeviceLocked) {
873                         sceneInteractor.transitionState
874                             .mapNotNull { it as? ObservableTransitionState.Idle }
875                             .map { it.currentScene to it.currentOverlays }
876                             .distinctUntilChanged()
877                             .map { (currentScene, currentOverlays) ->
878                                 when {
879                                     // When locked, showing the lockscreen scene should be reported
880                                     // as "interacting" while showing other scenes should report as
881                                     // "not interacting".
882                                     //
883                                     // This is done here in order to match the legacy
884                                     // implementation. The real reason why is lost to lore and myth.
885                                     Overlays.NotificationsShade in currentOverlays -> false
886                                     Overlays.QuickSettingsShade in currentOverlays -> null
887                                     Overlays.Bouncer in currentOverlays -> false
888                                     currentScene == Scenes.Lockscreen -> true
889                                     currentScene == Scenes.Shade -> false
890                                     else -> null
891                                 }
892                             }
893                     } else {
894                         flowOf(null)
895                     }
896                 }
897                 .collect { isInteractingOrNull ->
898                     isInteractingOrNull?.let { isInteracting ->
899                         centralSurfaces?.setInteracting(
900                             StatusBarManager.WINDOW_STATUS_BAR,
901                             isInteracting,
902                         )
903                     }
904                 }
905         }
906     }
907 
908     private fun handleBouncerOverscroll() {
909         applicationScope.launch {
910             sceneInteractor.transitionState
911                 // Only consider transitions.
912                 .filterIsInstance<ObservableTransitionState.Transition>()
913                 // Only consider user-initiated (e.g. drags) that go from bouncer to lockscreen.
914                 .filter { transition ->
915                     transition.fromContent == Overlays.Bouncer &&
916                         transition.toContent == Scenes.Lockscreen &&
917                         transition.isInitiatedByUserInput
918                 }
919                 .flatMapLatest { it.progress }
920                 // Figure out the direction of scrolling.
921                 .map { progress ->
922                     when {
923                         progress > 0 -> 1
924                         progress < 0 -> -1
925                         else -> 0
926                     }
927                 }
928                 .distinctUntilChanged()
929                 // Only consider negative scrolling, AKA overscroll.
930                 .filter { it == -1 }
931                 .collect { faceUnlockInteractor.onSwipeUpOnBouncer() }
932         }
933     }
934 
935     private fun handleKeyguardEnabledness() {
936         // Automatically switches scenes when keyguard is enabled or disabled, as needed.
937         applicationScope.launch {
938             keyguardEnabledInteractor.isKeyguardEnabled
939                 .sample(
940                     combine(
941                         deviceUnlockedInteractor.isInLockdown,
942                         deviceEntryInteractor.isDeviceEntered,
943                         ::Pair,
944                     )
945                 ) { isKeyguardEnabled, (isInLockdown, isDeviceEntered) ->
946                     when {
947                         !isKeyguardEnabled && !isInLockdown && !isDeviceEntered -> {
948                             keyguardEnabledInteractor.setShowKeyguardWhenReenabled(true)
949                             Scenes.Gone to "Keyguard became disabled"
950                         }
951                         isKeyguardEnabled &&
952                             keyguardEnabledInteractor.isShowKeyguardWhenReenabled() -> {
953                             keyguardEnabledInteractor.setShowKeyguardWhenReenabled(false)
954                             Scenes.Lockscreen to "Keyguard became enabled"
955                         }
956                         else -> null
957                     }
958                 }
959                 .filterNotNull()
960                 .collect { (targetScene, loggingReason) ->
961                     switchToScene(targetScene, loggingReason)
962                 }
963         }
964 
965         // Clears the showKeyguardWhenReenabled if the auth method changes to an insecure one.
966         applicationScope.launch {
967             authenticationInteractor
968                 .get()
969                 .authenticationMethod
970                 .map { it.isSecure }
971                 .distinctUntilChanged()
972                 .collect { isAuthenticationMethodSecure ->
973                     if (!isAuthenticationMethodSecure) {
974                         keyguardEnabledInteractor.setShowKeyguardWhenReenabled(false)
975                     }
976                 }
977         }
978     }
979 
980     private fun switchToScene(
981         targetSceneKey: SceneKey,
982         loggingReason: String,
983         sceneState: Any? = null,
984         freezeAndAnimateToCurrentState: Boolean = false,
985     ) {
986         sceneInteractor.changeScene(
987             toScene = targetSceneKey,
988             loggingReason = loggingReason,
989             sceneState = sceneState,
990             forceSettleToTargetScene = freezeAndAnimateToCurrentState,
991         )
992     }
993 
994     private fun hydrateBackStack() {
995         applicationScope.launch {
996             sceneInteractor.currentScene.pairwise().collect { (from, to) ->
997                 sceneBackInteractor.onSceneChange(from = from, to = to)
998             }
999         }
1000     }
1001 
1002     private fun notifyKeyguardDismissCancelledCallbacks() {
1003         applicationScope.launch {
1004             combine(deviceEntryInteractor.isUnlocked, sceneInteractor.currentOverlays.pairwise()) {
1005                     isUnlocked,
1006                     overlayChange ->
1007                     val difference = overlayChange.previousValue - overlayChange.newValue
1008                     !isUnlocked &&
1009                         sceneInteractor.currentScene.value != Scenes.Gone &&
1010                         Overlays.Bouncer in difference
1011                 }
1012                 .collect { notifyKeyguardDismissCancelled ->
1013                     if (notifyKeyguardDismissCancelled) {
1014                         dismissCallbackRegistry.notifyDismissCancelled()
1015                     }
1016                 }
1017         }
1018     }
1019 
1020     /**
1021      * Keeps the value of [DeviceEntryInteractor.isLockscreenEnabled] fresh.
1022      *
1023      * This is needed because that value is sourced from a non-observable data source
1024      * (`LockPatternUtils`, which doesn't expose a listener or callback for this value). Therefore,
1025      * every time a transition to the `Lockscreen` scene is started, the value is re-fetched and
1026      * cached.
1027      */
1028     private fun refreshLockscreenEnabled() {
1029         applicationScope.launch {
1030             sceneInteractor.transitionState
1031                 .map { it.isTransitioning(to = Scenes.Lockscreen) }
1032                 .distinctUntilChanged()
1033                 .filter { it }
1034                 .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() }
1035         }
1036     }
1037 
1038     /**
1039      * Wires the scene framework to activity transition animations that originate from anywhere. A
1040      * subset of these may actually originate from UI inside one of the scenes in the framework.
1041      *
1042      * Telling the scene framework about ongoing activity transition animations is critical so the
1043      * scene framework doesn't make its scene container invisible during a transition.
1044      *
1045      * As it turns out, making the scene container view invisible during a transition animation
1046      * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ
1047      * ending or being canceled.
1048      */
1049     private fun hydrateActivityTransitionAnimationState() {
1050         activityTransitionAnimator.addListener(
1051             object : ActivityTransitionAnimator.Listener {
1052                 override fun onTransitionAnimationStart() {
1053                     sceneInteractor.onTransitionAnimationStart()
1054                 }
1055 
1056                 override fun onTransitionAnimationEnd() {
1057                     sceneInteractor.onTransitionAnimationEnd()
1058                 }
1059             }
1060         )
1061     }
1062 
1063     private fun lockWhenDeviceBecomesUntrusted() {
1064         applicationScope.launch {
1065             trustInteractor.isTrusted.pairwise().collect { (wasTrusted, isTrusted) ->
1066                 if (wasTrusted && !isTrusted && !deviceEntryInteractor.isDeviceEntered.value) {
1067                     deviceEntryInteractor.lockNow(
1068                         "Exited trusted environment while not device not entered"
1069                     )
1070                 }
1071             }
1072         }
1073     }
1074 
1075     private suspend fun repeatWhen(condition: Flow<Boolean>, block: suspend () -> Unit) {
1076         condition.distinctUntilChanged().collectLatest { conditionMet ->
1077             if (conditionMet) {
1078                 block()
1079             }
1080         }
1081     }
1082 
1083     companion object {
1084         private const val TAG = "SceneContainerStartable"
1085     }
1086 }
1087