• 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 android.app.trust.TrustManager
20 import android.content.Context
21 import android.hardware.biometrics.BiometricFaceConstants
22 import android.hardware.biometrics.BiometricSourceType
23 import android.service.dreams.Flags.dreamsV2
24 import com.android.keyguard.KeyguardUpdateMonitor
25 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
26 import com.android.systemui.biometrics.shared.model.LockoutMode
27 import com.android.systemui.biometrics.shared.model.SensorStrength
28 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
29 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.dagger.qualifiers.Main
33 import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
34 import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig
35 import com.android.systemui.deviceentry.shared.FaceAuthUiEvent
36 import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
37 import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
38 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
39 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
40 import com.android.systemui.keyguard.shared.model.DevicePosture
41 import com.android.systemui.keyguard.shared.model.Edge
42 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
43 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
44 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
45 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
46 import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
47 import com.android.systemui.keyguard.shared.model.TransitionState
48 import com.android.systemui.log.FaceAuthenticationLogger
49 import com.android.systemui.log.table.TableLogBuffer
50 import com.android.systemui.log.table.logDiffsForTable
51 import com.android.systemui.power.domain.interactor.PowerInteractor
52 import com.android.systemui.res.R
53 import com.android.systemui.scene.domain.interactor.SceneInteractor
54 import com.android.systemui.scene.shared.flag.SceneContainerFlag
55 import com.android.systemui.scene.shared.model.Overlays
56 import com.android.systemui.scene.shared.model.Scenes
57 import com.android.systemui.user.data.model.SelectionStatus
58 import com.android.systemui.user.data.repository.UserRepository
59 import com.android.systemui.util.kotlin.pairwise
60 import com.android.systemui.util.kotlin.sample
61 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
62 import dagger.Lazy
63 import javax.inject.Inject
64 import kotlinx.coroutines.CoroutineDispatcher
65 import kotlinx.coroutines.CoroutineScope
66 import kotlinx.coroutines.channels.awaitClose
67 import kotlinx.coroutines.flow.Flow
68 import kotlinx.coroutines.flow.MutableStateFlow
69 import kotlinx.coroutines.flow.StateFlow
70 import kotlinx.coroutines.flow.collect
71 import kotlinx.coroutines.flow.distinctUntilChanged
72 import kotlinx.coroutines.flow.filter
73 import kotlinx.coroutines.flow.filterNotNull
74 import kotlinx.coroutines.flow.flowOn
75 import kotlinx.coroutines.flow.launchIn
76 import kotlinx.coroutines.flow.map
77 import kotlinx.coroutines.flow.merge
78 import kotlinx.coroutines.flow.onEach
79 import kotlinx.coroutines.yield
80 
81 /**
82  * Encapsulates business logic related face authentication being triggered for device entry from
83  * SystemUI Keyguard.
84  */
85 @SysUISingleton
86 class SystemUIDeviceEntryFaceAuthInteractor
87 @Inject
88 constructor(
89     private val context: Context,
90     @Application private val applicationScope: CoroutineScope,
91     @Main private val mainDispatcher: CoroutineDispatcher,
92     private val repository: DeviceEntryFaceAuthRepository,
93     private val primaryBouncerInteractor: Lazy<PrimaryBouncerInteractor>,
94     private val alternateBouncerInteractor: AlternateBouncerInteractor,
95     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
96     private val faceAuthenticationLogger: FaceAuthenticationLogger,
97     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
98     private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
99     private val userRepository: UserRepository,
100     private val facePropertyRepository: FacePropertyRepository,
101     private val faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig,
102     private val powerInteractor: PowerInteractor,
103     private val biometricSettingsRepository: BiometricSettingsRepository,
104     private val trustManager: TrustManager,
105     private val sceneInteractor: Lazy<SceneInteractor>,
106     deviceEntryFaceAuthStatusInteractor: DeviceEntryFaceAuthStatusInteractor,
107 ) : DeviceEntryFaceAuthInteractor {
108 
109     private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
110 
111     override fun start() {
112         // Todo(b/310594096): there is a dependency cycle introduced by the repository depending on
113         //  KeyguardBypassController, which in turn depends on KeyguardUpdateMonitor through
114         //  its other dependencies. Once bypassEnabled state is available through a repository, we
115         //  can break that cycle and inject this interactor directly into KeyguardUpdateMonitor
116         keyguardUpdateMonitor.setFaceAuthInteractor(this)
117         observeFaceAuthStateUpdates()
118         faceAuthenticationLogger.interactorStarted()
119         isBouncerVisible
120             .whenItFlipsToTrue()
121             .onEach {
122                 faceAuthenticationLogger.bouncerVisibilityChanged()
123                 runFaceAuth(
124                     FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN,
125                     fallbackToDetect = false,
126                 )
127             }
128             .launchIn(applicationScope)
129 
130         alternateBouncerInteractor.isVisible
131             .whenItFlipsToTrue()
132             .onEach {
133                 faceAuthenticationLogger.alternateBouncerVisibilityChanged()
134                 runFaceAuth(
135                     FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN,
136                     fallbackToDetect = false,
137                 )
138             }
139             .launchIn(applicationScope)
140 
141         val transitionFlows = buildList {
142             add(keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)))
143             add(keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN)))
144             add(keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)))
145 
146             if (dreamsV2()) {
147                 add(keyguardTransitionInteractor.transition(Edge.create(DREAMING, LOCKSCREEN)))
148             }
149         }
150 
151         transitionFlows
152             .merge()
153             .filter { it.transitionState == TransitionState.STARTED }
154             .sample(powerInteractor.detailedWakefulness)
155             .filter { wakefulnessModel ->
156                 val validWakeupReason =
157                     faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
158                         wakefulnessModel.lastWakeReason
159                     )
160                 if (!validWakeupReason) {
161                     faceAuthenticationLogger.ignoredWakeupReason(wakefulnessModel.lastWakeReason)
162                 }
163                 validWakeupReason
164             }
165             .onEach {
166                 faceAuthenticationLogger.lockscreenBecameVisible(it)
167                 FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED.extraInfo =
168                     it.lastWakeReason.powerManagerWakeReason
169                 runFaceAuth(
170                     FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED,
171                     fallbackToDetect = true,
172                 )
173             }
174             .launchIn(applicationScope)
175 
176         deviceEntryFingerprintAuthInteractor.isLockedOut
177             .sample(biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, ::Pair)
178             .filter { (_, faceEnabledAndEnrolled) ->
179                 // We don't care about this if face auth is not enabled.
180                 faceEnabledAndEnrolled
181             }
182             .map { (fpLockedOut, _) -> fpLockedOut }
183             .sample(userRepository.selectedUser, ::Pair)
184             .onEach { (fpLockedOut, currentUser) ->
185                 if (fpLockedOut) {
186                     faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
187                     if (isFaceAuthEnabledAndEnrolled()) {
188                         repository.setLockedOut(true)
189                     }
190                 } else {
191                     // Fingerprint is not locked out anymore, revert face lockout state back to
192                     // previous value.
193                     resetLockedOutState(currentUser.userInfo.id)
194                 }
195             }
196             .launchIn(applicationScope)
197 
198         // User switching should stop face auth and then when it is complete we should trigger face
199         // auth so that the switched user can unlock the device with face auth.
200         userRepository.selectedUser
201             .pairwise()
202             .filter { (previous, curr) ->
203                 val wasSwitching = previous.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS
204                 val isSwitching = curr.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS
205                 // User switching was in progress and is complete now.
206                 wasSwitching && !isSwitching
207             }
208             .map { (_, curr) -> curr.userInfo.id }
209             .sample(isBouncerVisible, ::Pair)
210             .onEach { (userId, isBouncerCurrentlyVisible) ->
211                 if (!isFaceAuthEnabledAndEnrolled()) {
212                     return@onEach
213                 }
214                 resetLockedOutState(userId)
215                 yield()
216                 runFaceAuth(
217                     FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING,
218                     // Fallback to detection if bouncer is not showing so that we can detect a
219                     // face and then show the bouncer to the user if face auth can't run
220                     fallbackToDetect = !isBouncerCurrentlyVisible,
221                 )
222             }
223             .launchIn(applicationScope)
224 
225         facePropertyRepository.cameraInfo
226             .onEach {
227                 if (it != null && isRunning()) {
228                     repository.cancel()
229                     runFaceAuth(
230                         FaceAuthUiEvent.FACE_AUTH_CAMERA_AVAILABLE_CHANGED,
231                         fallbackToDetect = true,
232                     )
233                 }
234             }
235             .launchIn(applicationScope)
236 
237         if (SceneContainerFlag.isEnabled) {
238             sceneInteractor
239                 .get()
240                 .transitionState
241                 .filter { it.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) }
242                 .distinctUntilChanged()
243                 .onEach { onShadeExpansionStarted() }
244                 .launchIn(applicationScope)
245         }
246     }
247 
248     private val isBouncerVisible: Flow<Boolean> by lazy {
249         if (SceneContainerFlag.isEnabled) {
250             sceneInteractor.get().transitionState.map { it.isIdle(Overlays.Bouncer) }
251         } else {
252             primaryBouncerInteractor.get().isShowing
253         }
254     }
255 
256     private suspend fun resetLockedOutState(currentUserId: Int) {
257         val lockoutMode = facePropertyRepository.getLockoutMode(currentUserId)
258         repository.setLockedOut(
259             lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED
260         )
261     }
262 
263     override fun onSwipeUpOnBouncer() {
264         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)
265     }
266 
267     override fun onNotificationPanelClicked() {
268         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true)
269     }
270 
271     override fun onShadeExpansionStarted() {
272         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, false)
273     }
274 
275     override fun onDeviceLifted() {
276         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, true)
277     }
278 
279     override fun onAssistantTriggeredOnLockScreen() {
280         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED, true)
281     }
282 
283     override fun onUdfpsSensorTouched() {
284         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)
285     }
286 
287     override fun onAccessibilityAction() {
288         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false)
289     }
290 
291     override fun onWalletLaunched() {
292         if (facePropertyRepository.sensorInfo.value?.strength == SensorStrength.STRONG) {
293             runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED, true)
294         }
295     }
296 
297     override fun onDeviceUnfolded() {
298         if (facePropertyRepository.supportedPostures.contains(DevicePosture.OPENED)) {
299             runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED, true)
300         }
301     }
302 
303     override fun registerListener(listener: FaceAuthenticationListener) {
304         listeners.add(listener)
305     }
306 
307     override fun unregisterListener(listener: FaceAuthenticationListener) {
308         listeners.remove(listener)
309     }
310 
311     override fun isRunning(): Boolean = repository.isAuthRunning.value
312 
313     override fun canFaceAuthRun(): Boolean = repository.canRunFaceAuth.value
314 
315     override fun isFaceAuthStrong(): Boolean =
316         facePropertyRepository.sensorInfo.value?.strength == SensorStrength.STRONG
317 
318     override fun onPrimaryBouncerUserInput() {
319         repository.cancel()
320     }
321 
322     private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null)
323 
324     /** Provide the status of face authentication */
325     override val authenticationStatus =
326         merge(
327             faceAuthenticationStatusOverride.filterNotNull(),
328             deviceEntryFaceAuthStatusInteractor.authenticationStatus.filterNotNull(),
329         )
330 
331     /** Provide the status of face detection */
332     override val detectionStatus = repository.detectionStatus
333     override val isLockedOut: StateFlow<Boolean> = repository.isLockedOut
334     override val isAuthenticated: StateFlow<Boolean> = repository.isAuthenticated
335     override val isBypassEnabled: Flow<Boolean> = repository.isBypassEnabled
336 
337     private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
338         if (repository.isLockedOut.value) {
339             faceAuthenticationStatusOverride.value =
340                 ErrorFaceAuthenticationStatus(
341                     BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT,
342                     context.resources.getString(R.string.keyguard_face_unlock_unavailable),
343                 )
344         } else {
345             faceAuthenticationStatusOverride.value = null
346             faceAuthenticationLogger.authRequested(uiEvent)
347             repository.requestAuthenticate(uiEvent, fallbackToDetection = fallbackToDetect)
348         }
349     }
350 
351     override fun isFaceAuthEnabledAndEnrolled(): Boolean =
352         biometricSettingsRepository.isFaceAuthEnrolledAndEnabled.value
353 
354     private fun observeFaceAuthStateUpdates() {
355         authenticationStatus
356             .onEach { authStatusUpdate ->
357                 listeners.forEach { it.onAuthenticationStatusChanged(authStatusUpdate) }
358             }
359             .flowOn(mainDispatcher)
360             .launchIn(applicationScope)
361         detectionStatus
362             .onEach { detectionStatusUpdate ->
363                 listeners.forEach { it.onDetectionStatusChanged(detectionStatusUpdate) }
364             }
365             .flowOn(mainDispatcher)
366             .launchIn(applicationScope)
367         repository.isLockedOut
368             .onEach { lockedOut -> listeners.forEach { it.onLockoutStateChanged(lockedOut) } }
369             .flowOn(mainDispatcher)
370             .launchIn(applicationScope)
371         repository.isAuthRunning
372             .onEach { running -> listeners.forEach { it.onRunningStateChanged(running) } }
373             .flowOn(mainDispatcher)
374             .launchIn(applicationScope)
375         repository.isAuthenticated
376             .sample(userRepository.selectedUserInfo, ::Pair)
377             .onEach { (isAuthenticated, userInfo) ->
378                 if (!isAuthenticated) {
379                     faceAuthenticationLogger.clearFaceRecognized()
380                     trustManager.clearAllBiometricRecognized(BiometricSourceType.FACE, userInfo.id)
381                 }
382             }
383             .onEach { (isAuthenticated, _) ->
384                 listeners.forEach { it.onAuthenticatedChanged(isAuthenticated) }
385             }
386             .flowOn(mainDispatcher)
387             .launchIn(applicationScope)
388 
389         biometricSettingsRepository.isFaceAuthEnrolledAndEnabled
390             .onEach { enrolledAndEnabled ->
391                 listeners.forEach { it.onAuthEnrollmentStateChanged(enrolledAndEnabled) }
392             }
393             .flowOn(mainDispatcher)
394             .launchIn(applicationScope)
395     }
396 
397     override suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) {
398         conflatedCallbackFlow {
399                 val listener =
400                     object : FaceAuthenticationListener {
401                         override fun onAuthEnrollmentStateChanged(enrolled: Boolean) {
402                             trySend(isFaceAuthEnabledAndEnrolled())
403                         }
404                     }
405 
406                 registerListener(listener)
407 
408                 awaitClose { unregisterListener(listener) }
409             }
410             .logDiffsForTable(
411                 tableLogBuffer = tableLogBuffer,
412                 columnName = "isFaceAuthEnabledAndEnrolled",
413                 initialValue = isFaceAuthEnabledAndEnrolled(),
414             )
415             .collect()
416     }
417 
418     companion object {
419         const val TAG = "DeviceEntryFaceAuthInteractor"
420     }
421 }
422 
423 // Extension method that filters a generic Boolean flow to one that emits
424 // whenever there is flip from false -> true
whenItFlipsToTruenull425 private fun Flow<Boolean>.whenItFlipsToTrue(): Flow<Boolean> {
426     return this.pairwise()
427         .filter { pair -> !pair.previousValue && pair.newValue }
428         .map { it.newValue }
429 }
430