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