1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.keyguard.domain.interactor 19 20 import com.android.keyguard.logging.KeyguardLogger 21 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor 22 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags 23 import com.android.systemui.dagger.SysUISingleton 24 import com.android.systemui.dagger.qualifiers.Application 25 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor 26 import com.android.systemui.keyguard.data.repository.KeyguardRepository 27 import com.android.systemui.keyguard.shared.model.DismissAction 28 import com.android.systemui.keyguard.shared.model.KeyguardDone 29 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE 30 import com.android.systemui.lifecycle.ExclusiveActivatable 31 import com.android.systemui.log.core.LogLevel 32 import com.android.systemui.scene.domain.interactor.SceneInteractor 33 import com.android.systemui.scene.shared.flag.SceneContainerFlag 34 import com.android.systemui.scene.shared.model.Scenes 35 import com.android.systemui.shade.domain.interactor.ShadeInteractor 36 import dagger.Lazy 37 import javax.inject.Inject 38 import kotlinx.coroutines.CoroutineScope 39 import kotlinx.coroutines.awaitCancellation 40 import kotlinx.coroutines.coroutineScope 41 import kotlinx.coroutines.flow.Flow 42 import kotlinx.coroutines.flow.SharingStarted 43 import kotlinx.coroutines.flow.StateFlow 44 import kotlinx.coroutines.flow.combine 45 import kotlinx.coroutines.flow.distinctUntilChanged 46 import kotlinx.coroutines.flow.filter 47 import kotlinx.coroutines.flow.flow 48 import kotlinx.coroutines.flow.map 49 import kotlinx.coroutines.flow.merge 50 import kotlinx.coroutines.flow.stateIn 51 import kotlinx.coroutines.launch 52 53 /** Encapsulates business-logic for actions to run when the keyguard is dismissed. */ 54 @SysUISingleton 55 class KeyguardDismissActionInteractor 56 @Inject 57 constructor( 58 private val repository: KeyguardRepository, 59 transitionInteractor: KeyguardTransitionInteractor, 60 val dismissInteractor: KeyguardDismissInteractor, 61 @Application private val applicationScope: CoroutineScope, 62 deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>, 63 shadeInteractor: Lazy<ShadeInteractor>, 64 keyguardInteractor: Lazy<KeyguardInteractor>, 65 sceneInteractor: Lazy<SceneInteractor>, 66 private val keyguardLogger: KeyguardLogger, 67 private val primaryBouncerInteractor: PrimaryBouncerInteractor, 68 ) : ExclusiveActivatable() { 69 private val dismissAction: Flow<DismissAction> = repository.dismissAction 70 71 // TODO (b/268240415): use message in alt + primary bouncer message 72 // message to show to the user about the dismiss action, else empty string 73 val message = dismissAction.map { it.message } 74 75 /** 76 * True if the dismiss action will run an animation on the lockscreen and requires any views 77 * that would obscure this animation (ie: the primary bouncer) to immediately hide, so the 78 * animation would be visible. 79 */ 80 val willAnimateDismissActionOnLockscreen: StateFlow<Boolean> = 81 dismissAction 82 .map { it.willAnimateOnLockscreen } 83 .stateIn( 84 scope = applicationScope, 85 started = SharingStarted.WhileSubscribed(), 86 initialValue = false, 87 ) 88 89 private val finishedTransitionToGone: Flow<Unit> = 90 if (SceneContainerFlag.isEnabled) { 91 // Using sceneInteractor instead of transitionInteractor because of a race 92 // condition that forms between transitionInteractor (transitionState) and 93 // isOnShadeWhileUnlocked where the latter emits false before the former emits 94 // true, causing the merge to not emit until it's too late. 95 sceneInteractor 96 .get() 97 .currentScene 98 .map { it == Scenes.Gone } 99 .distinctUntilChanged() 100 .filter { it } 101 .map {} 102 } else { 103 transitionInteractor 104 .isFinishedIn(content = Scenes.Gone, stateWithoutSceneContainer = GONE) 105 .filter { it } 106 .map {} 107 } 108 109 /** 110 * True if the any variation of the notification shade or quick settings is showing AND the 111 * device is unlocked. Else, false. 112 */ 113 private val isOnShadeWhileUnlocked: Flow<Boolean> = 114 if (SceneContainerFlag.isEnabled) { 115 combine( 116 shadeInteractor.get().isAnyExpanded, 117 deviceUnlockedInteractor.get().deviceUnlockStatus, 118 ) { isAnyExpanded, unlockStatus -> 119 isAnyExpanded && unlockStatus.isUnlocked 120 } 121 .distinctUntilChanged() 122 } else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) { 123 combine( 124 shadeInteractor.get().isAnyExpanded, 125 keyguardInteractor.get().isKeyguardDismissible, 126 ) { isAnyExpanded, keyguardDismissible -> 127 isAnyExpanded && keyguardDismissible 128 } 129 .distinctUntilChanged() 130 } else { 131 flow { 132 error( 133 "This should not be used when both SceneContainerFlag " + 134 "and ComposeBouncerFlag are disabled" 135 ) 136 } 137 } 138 139 fun runDismissAnimationOnKeyguard(): Boolean { 140 return willAnimateDismissActionOnLockscreen.value 141 } 142 143 fun runAfterKeyguardGone(runnable: Runnable) { 144 if (ComposeBouncerFlags.isUnexpectedlyInLegacyMode()) return 145 setDismissAction( 146 DismissAction.RunAfterKeyguardGone( 147 dismissAction = { runnable.run() }, 148 onCancelAction = {}, 149 message = "", 150 willAnimateOnLockscreen = false, 151 ) 152 ) 153 } 154 155 fun setDismissAction(dismissAction: DismissAction) { 156 if (ComposeBouncerFlags.isUnexpectedlyInLegacyMode()) return 157 repository.dismissAction.value.onCancelAction.run() 158 repository.setDismissAction(dismissAction) 159 } 160 161 /** Launch any relevant coroutines that are required by this interactor. */ 162 override suspend fun onActivated(): Nothing { 163 coroutineScope { 164 launch { 165 merge(finishedTransitionToGone, isOnShadeWhileUnlocked.filter { it }.map {}) 166 .collect { 167 log("finishedTransitionToGone") 168 runDismissAction() 169 } 170 } 171 172 launch { 173 dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction.collect { 174 log("eventsThatRequireKeyguardDismissal") 175 runDismissAction() 176 } 177 } 178 179 launch { repository.dismissAction.collect { log("updatedDismissAction=$it") } } 180 awaitCancellation() 181 } 182 } 183 184 fun clearDismissAction() { 185 repository.setDismissAction(DismissAction.None) 186 } 187 188 /** Run the dismiss action and starts the dismiss keyguard transition. */ 189 private suspend fun runDismissAction() { 190 val dismissAction = repository.dismissAction.value 191 var keyguardDoneTiming: KeyguardDone = KeyguardDone.IMMEDIATE 192 if (dismissAction != DismissAction.None) { 193 keyguardDoneTiming = dismissAction.onDismissAction.invoke() 194 dismissInteractor.setKeyguardDone(keyguardDoneTiming) 195 clearDismissAction() 196 } 197 if (!SceneContainerFlag.isEnabled) { 198 // This is required to reset some state flows in the repository which ideally should be 199 // sharedFlows but are not due to performance concerns. 200 primaryBouncerInteractor.notifyKeyguardAuthenticatedHandled() 201 } 202 } 203 204 private fun log(message: String) { 205 keyguardLogger.log(TAG, LogLevel.DEBUG, message) 206 } 207 208 companion object { 209 private const val TAG = "KeyguardDismissAction" 210 } 211 } 212