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