1 /*
<lambda>null2  * Copyright (C) 2024 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 androidx.annotation.VisibleForTesting
22 import com.android.compose.animation.scene.ObservableTransitionState
23 import com.android.compose.animation.scene.SceneKey
24 import com.android.systemui.CoreStartable
25 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dagger.qualifiers.Application
28 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
29 import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor
30 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
31 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
32 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
33 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
34 import com.android.systemui.scene.domain.interactor.SceneInteractor
35 import com.android.systemui.scene.shared.flag.SceneContainerFlag
36 import com.android.systemui.scene.shared.model.Scenes
37 import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor
38 import com.android.systemui.statusbar.phone.DozeServiceHost
39 import com.android.systemui.statusbar.phone.ScrimController
40 import com.android.systemui.statusbar.phone.ScrimState
41 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
42 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
43 import javax.inject.Inject
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.ExperimentalCoroutinesApi
46 import kotlinx.coroutines.channels.awaitClose
47 import kotlinx.coroutines.flow.Flow
48 import kotlinx.coroutines.flow.combine
49 import kotlinx.coroutines.flow.filterNotNull
50 import kotlinx.coroutines.flow.map
51 import kotlinx.coroutines.flow.onEach
52 import kotlinx.coroutines.launch
53 
54 @SysUISingleton
55 class ScrimStartable
56 @Inject
57 constructor(
58     @Application private val applicationScope: CoroutineScope,
59     private val scrimController: ScrimController,
60     sceneInteractor: SceneInteractor,
61     deviceEntryInteractor: DeviceEntryInteractor,
62     keyguardInteractor: KeyguardInteractor,
63     occlusionInteractor: SceneContainerOcclusionInteractor,
64     biometricUnlockInteractor: BiometricUnlockInteractor,
65     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
66     private val alternateBouncerInteractor: AlternateBouncerInteractor,
67     brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor,
68     private val dozeServiceHost: DozeServiceHost,
69 ) : CoreStartable {
70 
71     @VisibleForTesting
72     val scrimState: Flow<ScrimState?> =
73         combine(
74                 deviceEntryInteractor.isDeviceEntered,
75                 occlusionInteractor.invisibleDueToOcclusion,
76                 sceneInteractor.currentScene,
77                 sceneInteractor.transitionState,
78                 keyguardInteractor.isDozing,
79                 keyguardInteractor.isDreaming,
80                 biometricUnlockInteractor.unlockState,
81                 brightnessMirrorShowingInteractor.isShowing,
82                 keyguardInteractor.isPulsing,
83                 conflatedCallbackFlow {
84                     val listener =
85                         DozeServiceHost.HasPendingScreenOffCallbackChangeListener {
86                             hasPendingScreenOffCallback ->
87                             trySend(hasPendingScreenOffCallback)
88                         }
89                     dozeServiceHost.setHasPendingScreenOffCallbackChangeListener(listener)
90                     awaitClose {
91                         dozeServiceHost.setHasPendingScreenOffCallbackChangeListener(null)
92                     }
93                 },
94             ) { flowValues ->
95                 val isDeviceEntered = flowValues[0] as Boolean
96                 val isOccluded = flowValues[1] as Boolean
97                 val currentScene = flowValues[2] as SceneKey
98                 val transitionState = flowValues[3] as ObservableTransitionState
99                 val isDozing = flowValues[4] as Boolean
100                 val isDreaming = flowValues[5] as Boolean
101                 val biometricUnlockState = flowValues[6] as BiometricUnlockModel
102                 val isBrightnessMirrorVisible = flowValues[7] as Boolean
103                 val isPulsing = flowValues[8] as Boolean
104                 val hasPendingScreenOffCallback = flowValues[9] as Boolean
105 
106                 // This is true when the lockscreen scene is either the current scene or somewhere
107                 // in the
108                 // navigation back stack of scenes.
109                 val isOnKeyguard = !isDeviceEntered
110                 val isCurrentSceneBouncer = currentScene == Scenes.Bouncer
111                 // This is true when moving away from one of the keyguard scenes to the gone scene.
112                 // It
113                 // happens only when unlocking or when dismissing a dismissible lockscreen.
114                 val isTransitioningAwayFromKeyguard =
115                     transitionState is ObservableTransitionState.Transition &&
116                         transitionState.fromScene.isKeyguard() &&
117                         transitionState.toScene == Scenes.Gone
118 
119                 // This is true when any of the shade scenes is the current scene.
120                 val isCurrentSceneShade = currentScene.isShade()
121                 // This is true when moving into one of the shade scenes when a non-shade scene.
122                 val isTransitioningToShade =
123                     transitionState is ObservableTransitionState.Transition &&
124                         !transitionState.fromScene.isShade() &&
125                         transitionState.toScene.isShade()
126 
127                 // This is true after completing a transition to communal.
128                 val isIdleOnCommunal = transitionState.isIdle(Scenes.Communal)
129 
130                 // This is true during the process of an unlock of the device.
131                 // TODO(b/330587738): add support for remote unlock animations. If such an
132                 //   animation is underway, unlocking should be true.
133                 val unlocking =
134                     isOnKeyguard &&
135                         (biometricUnlockState.mode == BiometricUnlockMode.WAKE_AND_UNLOCK ||
136                             isTransitioningAwayFromKeyguard)
137 
138                 if (alternateBouncerInteractor.isVisibleState()) {
139                     // This will cancel the keyguardFadingAway animation if it is running. We need
140                     // to do
141                     // this as otherwise it can remain pending and leave keyguard in a weird state.
142                     onKeyguardFadedAway(isTransitioningAwayFromKeyguard)
143                     if (!isTransitioningToShade) {
144                         // Safeguard which prevents the scrim from being stuck in the wrong
145                         // state
146                         Model(scrimState = ScrimState.KEYGUARD, unlocking = unlocking)
147                     } else {
148                         // Assume scrim state for shade is already correct and do nothing
149                         null
150                     }
151                 } else if (isCurrentSceneBouncer && !unlocking) {
152                     // Bouncer needs the front scrim when it's on top of an activity, tapping on a
153                     // notification, editing QS or being dismissed by
154                     // FLAG_DISMISS_KEYGUARD_ACTIVITY.
155                     Model(
156                         scrimState =
157                             if (statusBarKeyguardViewManager.primaryBouncerNeedsScrimming()) {
158                                 ScrimState.BOUNCER_SCRIMMED
159                             } else {
160                                 ScrimState.BOUNCER
161                             },
162                         unlocking = false,
163                     )
164                 } else if (isBrightnessMirrorVisible) {
165                     Model(scrimState = ScrimState.BRIGHTNESS_MIRROR, unlocking = unlocking)
166                 } else if (isCurrentSceneShade && !isDeviceEntered) {
167                     Model(scrimState = ScrimState.SHADE_LOCKED, unlocking = unlocking)
168                 } else if (isPulsing) {
169                     Model(scrimState = ScrimState.PULSING, unlocking = unlocking)
170                 } else if (hasPendingScreenOffCallback) {
171                     Model(scrimState = ScrimState.OFF, unlocking = unlocking)
172                 } else if (isDozing && !unlocking) {
173                     // This will cancel the keyguardFadingAway animation if it is running. We need
174                     // to do
175                     // this as otherwise it can remain pending and leave keyguard in a weird state.
176                     onKeyguardFadedAway(isTransitioningAwayFromKeyguard)
177                     Model(scrimState = ScrimState.AOD, unlocking = false)
178                 } else if (isIdleOnCommunal) {
179                     if (isOnKeyguard && isDreaming && !unlocking) {
180                         Model(scrimState = ScrimState.GLANCEABLE_HUB_OVER_DREAM, unlocking = false)
181                     } else {
182                         Model(scrimState = ScrimState.GLANCEABLE_HUB, unlocking = unlocking)
183                     }
184                 } else if (isOnKeyguard && !unlocking && !isOccluded) {
185                     Model(scrimState = ScrimState.KEYGUARD, unlocking = false)
186                 } else if (isOnKeyguard && !unlocking && isDreaming) {
187                     Model(scrimState = ScrimState.DREAMING, unlocking = false)
188                 } else {
189                     Model(scrimState = ScrimState.UNLOCKED, unlocking = unlocking)
190                 }
191             }
192             .onEach { model ->
193                 if (model != null) {
194                     scrimController.setExpansionAffectsAlpha(!model.unlocking)
195                 }
196             }
197             .map { model -> model?.scrimState }
198 
199     override fun start() {
200         if (!SceneContainerFlag.isEnabled) {
201             return
202         }
203 
204         hydrateScrimState()
205     }
206 
207     private fun hydrateScrimState() {
208         applicationScope.launch {
209             scrimState.filterNotNull().collect { scrimState ->
210                 scrimController.transitionTo(scrimState)
211             }
212         }
213     }
214 
215     private fun onKeyguardFadedAway(isKeyguardGoingAway: Boolean) {
216         if (isKeyguardGoingAway) {
217             statusBarKeyguardViewManager.onKeyguardFadedAway()
218         }
219     }
220 
221     private fun SceneKey.isKeyguard(): Boolean {
222         return this == Scenes.Lockscreen || this == Scenes.Bouncer
223     }
224 
225     private fun SceneKey.isShade(): Boolean {
226         return this == Scenes.Shade ||
227             this == Scenes.QuickSettings ||
228             this == Scenes.NotificationsShade ||
229             this == Scenes.QuickSettingsShade
230     }
231 
232     private data class Model(
233         val scrimState: ScrimState,
234         val unlocking: Boolean,
235     )
236 }
237