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