• 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 package com.android.systemui.deviceentry.domain.interactor
18 
19 import com.android.internal.policy.IKeyguardDismissCallback
20 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
21 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
22 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
26 import com.android.systemui.keyguard.DismissCallbackRegistry
27 import com.android.systemui.log.table.TableLogBuffer
28 import com.android.systemui.log.table.logDiffsForTable
29 import com.android.systemui.scene.data.model.asIterable
30 import com.android.systemui.scene.domain.SceneFrameworkTableLog
31 import com.android.systemui.scene.domain.interactor.SceneBackInteractor
32 import com.android.systemui.scene.domain.interactor.SceneInteractor
33 import com.android.systemui.scene.shared.model.Overlays
34 import com.android.systemui.scene.shared.model.Scenes
35 import com.android.systemui.util.kotlin.pairwise
36 import com.android.systemui.utils.coroutines.flow.mapLatestConflated
37 import javax.inject.Inject
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.SharingStarted
41 import kotlinx.coroutines.flow.StateFlow
42 import kotlinx.coroutines.flow.combine
43 import kotlinx.coroutines.flow.distinctUntilChanged
44 import kotlinx.coroutines.flow.filter
45 import kotlinx.coroutines.flow.first
46 import kotlinx.coroutines.flow.map
47 import kotlinx.coroutines.flow.onStart
48 import kotlinx.coroutines.flow.stateIn
49 import kotlinx.coroutines.launch
50 
51 /**
52  * Hosts application business logic related to device entry.
53  *
54  * Device entry occurs when the user successfully dismisses (or bypasses) the lockscreen, regardless
55  * of the authentication method used.
56  */
57 @SysUISingleton
58 class DeviceEntryInteractor
59 @Inject
60 constructor(
61     @Application private val applicationScope: CoroutineScope,
62     private val repository: DeviceEntryRepository,
63     private val authenticationInteractor: AuthenticationInteractor,
64     private val sceneInteractor: SceneInteractor,
65     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
66     private val alternateBouncerInteractor: AlternateBouncerInteractor,
67     private val dismissCallbackRegistry: DismissCallbackRegistry,
68     sceneBackInteractor: SceneBackInteractor,
69     @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer,
70 ) {
71     /**
72      * Whether the device is unlocked.
73      *
74      * A device that is not yet unlocked requires unlocking by completing an authentication
75      * challenge according to the current authentication method, unless in cases when the current
76      * authentication method is not "secure" (for example, None and Swipe); in such cases, the value
77      * of this flow will always be `true`, even if the lockscreen is showing and still needs to be
78      * dismissed by the user to proceed.
79      */
80     val isUnlocked: StateFlow<Boolean> =
81         deviceUnlockedInteractor.deviceUnlockStatus
82             .map { it.isUnlocked }
83             .stateIn(
84                 scope = applicationScope,
85                 started = SharingStarted.WhileSubscribed(),
86                 initialValue = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked,
87             )
88 
89     /**
90      * Emits `true` when the current scene switches to [Scenes.Gone] for the first time after having
91      * been on [Scenes.Lockscreen].
92      *
93      * Different from [isDeviceEntered] such that the current scene must actually go through
94      * [Scenes.Gone] to produce a `true`. [isDeviceEntered] also takes into account the navigation
95      * back stack and will produce a `true` value even when the current scene is still not
96      * [Scenes.Gone] but the bottommost entry of the navigation back stack switched from
97      * [Scenes.Lockscreen] to [Scenes.Gone] while the user is staring at another scene.
98      */
99     val isDeviceEnteredDirectly: StateFlow<Boolean> =
100         sceneInteractor.currentScene
101             .filter { currentScene ->
102                 currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen
103             }
104             .mapLatestConflated { scene ->
105                 if (scene == Scenes.Gone) {
106                     // Make sure device unlock status is definitely unlocked before we
107                     // consider the device "entered".
108                     deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
109                     true
110                 } else {
111                     false
112                 }
113             }
114             .stateIn(
115                 scope = applicationScope,
116                 started = SharingStarted.Eagerly,
117                 initialValue = false,
118             )
119 
120     /**
121      * Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method).
122      * This can be `false` when the device is unlocked, e.g. when the user still needs to swipe away
123      * the non-secure lockscreen, even though they've already authenticated.
124      *
125      * Note: This does not imply that the lockscreen is visible or not.
126      *
127      * Different from [isDeviceEnteredDirectly] such that the current scene doesn't actually have to
128      * go through [Scenes.Gone] to produce a `true`. [isDeviceEnteredDirectly] doesn't take the
129      * navigation back stack into account and will only produce a `true` value even when the current
130      * scene is actually [Scenes.Gone].
131      */
132     val isDeviceEntered: StateFlow<Boolean> =
133         combine(
134                 // This flow emits true when the currentScene switches to Gone for the first time
135                 // after having been on Lockscreen.
136                 isDeviceEnteredDirectly,
137                 // This flow emits true only if the bottom of the navigation back stack has been
138                 // switched from Lockscreen to Gone. In other words, only if the device was unlocked
139                 // while visiting at least one scene "above" the Lockscreen scene.
140                 sceneBackInteractor.backStack
141                     // The bottom of the back stack, which is Lockscreen, Gone, or null if empty.
142                     .map { it.asIterable().lastOrNull() }
143                     // Filter out cases where the stack changes but the bottom remains unchanged.
144                     .distinctUntilChanged()
145                     // Detect changes of the bottom of the stack, start with null, so the first
146                     // update emits a value and the logic doesn't need to wait for a second value
147                     // before emitting something.
148                     .pairwise(initialValue = null)
149                     // Replacing a bottom of the stack that was Lockscreen with Gone constitutes a
150                     // "device entered" event.
151                     .map { (from, to) -> from == Scenes.Lockscreen && to == Scenes.Gone },
152             ) { enteredDirectly, enteredOnBackStack ->
153                 enteredOnBackStack || enteredDirectly
154             }
155             .logDiffsForTable(
156                 tableLogBuffer = tableLogBuffer,
157                 columnName = "isDeviceEntered",
158                 initialValue = false,
159             )
160             .stateIn(
161                 scope = applicationScope,
162                 started = SharingStarted.Eagerly,
163                 initialValue = false,
164             )
165 
166     val isLockscreenEnabled: Flow<Boolean> by lazy {
167         repository.isLockscreenEnabled.onStart { refreshLockscreenEnabled() }
168     }
169 
170     /**
171      * Whether it's currently possible to swipe up to enter the device without requiring
172      * authentication or when the device is already authenticated using a passive authentication
173      * mechanism like face or trust manager. This returns `false` whenever the lockscreen has been
174      * dismissed.
175      *
176      * A value of `null` is meaningless and is used as placeholder while the actual value is still
177      * being loaded in the background.
178      *
179      * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other
180      * UI.
181      */
182     val canSwipeToEnter: StateFlow<Boolean?> by lazy {
183         combine(
184                 authenticationInteractor.authenticationMethod.map {
185                     it == AuthenticationMethodModel.None
186                 },
187                 isLockscreenEnabled,
188                 deviceUnlockedInteractor.deviceUnlockStatus,
189                 isDeviceEntered,
190             ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered ->
191                 val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled
192                 (isSwipeAuthMethod ||
193                     (deviceUnlockStatus.isUnlocked &&
194                         deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) &&
195                     !isDeviceEntered
196             }
197             .logDiffsForTable(
198                 tableLogBuffer = tableLogBuffer,
199                 columnName = "canSwipeToEnter",
200                 initialValue = false,
201             )
202             .stateIn(
203                 scope = applicationScope,
204                 started = SharingStarted.Eagerly,
205                 // Starts as null to prevent downstream collectors from falsely assuming that the
206                 // user can or cannot swipe to enter the device while the real value is being loaded
207                 // from upstream data sources.
208                 initialValue = null,
209             )
210     }
211 
212     /**
213      * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
214      * dismissed once the authentication challenge is completed. For example, completing a biometric
215      * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
216      * lockscreen.
217      */
218     val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
219 
220     /**
221      * Attempt to enter the device and dismiss the lockscreen. If authentication is required to
222      * unlock the device it will transition to bouncer.
223      *
224      * @param callback An optional callback to invoke when the attempt succeeds, fails, or is
225      *   canceled
226      */
227     @JvmOverloads
228     fun attemptDeviceEntry(callback: IKeyguardDismissCallback? = null) {
229         callback?.let { dismissCallbackRegistry.addCallback(it) }
230 
231         // TODO (b/307768356),
232         //       1. Check if the device is already authenticated by trust agent/passive biometrics
233         //       2. Show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show
234         //       3. For face auth only setups trigger face auth, delay transitioning to bouncer for
235         //          a small amount of time.
236         //       4. Transition to bouncer scene
237         applicationScope.launch {
238             if (isAuthenticationRequired()) {
239                 if (alternateBouncerInteractor.canShowAlternateBouncer.value) {
240                     alternateBouncerInteractor.forceShow()
241                 } else {
242                     sceneInteractor.showOverlay(
243                         overlay = Overlays.Bouncer,
244                         loggingReason = "request to unlock device while authentication required",
245                     )
246                 }
247             } else {
248                 sceneInteractor.changeScene(
249                     toScene = Scenes.Gone,
250                     loggingReason = "request to unlock device while authentication isn't required",
251                 )
252             }
253         }
254     }
255 
256     /**
257      * Returns `true` if the device currently requires authentication before entry is granted;
258      * `false` if the device can be entered without authenticating first.
259      */
260     suspend fun isAuthenticationRequired(): Boolean {
261         return !deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked &&
262             authenticationInteractor.getAuthenticationMethod().isSecure
263     }
264 
265     /**
266      * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has
267      * chosen any secure authentication method and even if they set the lockscreen to be dismissed
268      * when the user swipes on it.
269      */
270     suspend fun isLockscreenEnabled(): Boolean {
271         return repository.isLockscreenEnabled()
272     }
273 
274     /**
275      * Forces a refresh of the value of [isLockscreenEnabled] such that the flow emits the latest
276      * value.
277      *
278      * Without calling this method, the flow will have a stale value unless the collector is removed
279      * and re-added.
280      */
281     suspend fun refreshLockscreenEnabled() {
282         isLockscreenEnabled()
283     }
284 
285     /** Locks the device instantly. */
286     fun lockNow(debuggingReason: String) {
287         deviceUnlockedInteractor.lockNow(debuggingReason)
288     }
289 }
290