• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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