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.keyguard.domain.interactor
18
19 import android.content.Context
20 import android.hardware.biometrics.BiometricFaceConstants
21 import com.android.keyguard.FaceAuthUiEvent
22 import com.android.keyguard.KeyguardUpdateMonitor
23 import com.android.systemui.CoreStartable
24 import com.android.systemui.R
25 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
26 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Application
29 import com.android.systemui.dagger.qualifiers.Main
30 import com.android.systemui.flags.FeatureFlags
31 import com.android.systemui.flags.Flags
32 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
33 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
34 import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
35 import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
36 import com.android.systemui.keyguard.shared.model.TransitionState
37 import com.android.systemui.log.FaceAuthenticationLogger
38 import com.android.systemui.user.data.repository.UserRepository
39 import com.android.systemui.util.kotlin.pairwise
40 import javax.inject.Inject
41 import kotlinx.coroutines.CoroutineDispatcher
42 import kotlinx.coroutines.CoroutineScope
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.MutableStateFlow
45 import kotlinx.coroutines.flow.filter
46 import kotlinx.coroutines.flow.filterNotNull
47 import kotlinx.coroutines.flow.flowOn
48 import kotlinx.coroutines.flow.launchIn
49 import kotlinx.coroutines.flow.map
50 import kotlinx.coroutines.flow.merge
51 import kotlinx.coroutines.flow.onEach
52 import kotlinx.coroutines.launch
53 import kotlinx.coroutines.withContext
54
55 /**
56 * Encapsulates business logic related face authentication being triggered for device entry from
57 * SystemUI Keyguard.
58 */
59 @SysUISingleton
60 class SystemUIKeyguardFaceAuthInteractor
61 @Inject
62 constructor(
63 private val context: Context,
64 @Application private val applicationScope: CoroutineScope,
65 @Main private val mainDispatcher: CoroutineDispatcher,
66 private val repository: DeviceEntryFaceAuthRepository,
67 private val primaryBouncerInteractor: PrimaryBouncerInteractor,
68 private val alternateBouncerInteractor: AlternateBouncerInteractor,
69 private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
70 private val featureFlags: FeatureFlags,
71 private val faceAuthenticationLogger: FaceAuthenticationLogger,
72 private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
73 private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
74 private val userRepository: UserRepository,
75 ) : CoreStartable, KeyguardFaceAuthInteractor {
76
77 private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
78
79 override fun start() {
80 if (!isEnabled()) {
81 return
82 }
83 // This is required because fingerprint state required for the face auth repository is
84 // backed by KeyguardUpdateMonitor. KeyguardUpdateMonitor constructor accesses the biometric
85 // state which makes lazy injection not an option.
86 keyguardUpdateMonitor.setFaceAuthInteractor(this)
87 observeFaceAuthStateUpdates()
88 faceAuthenticationLogger.interactorStarted()
89 primaryBouncerInteractor.isShowing
90 .whenItFlipsToTrue()
91 .onEach {
92 faceAuthenticationLogger.bouncerVisibilityChanged()
93 runFaceAuth(
94 FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN,
95 fallbackToDetect = true
96 )
97 }
98 .launchIn(applicationScope)
99
100 alternateBouncerInteractor.isVisible
101 .whenItFlipsToTrue()
102 .onEach {
103 faceAuthenticationLogger.alternateBouncerVisibilityChanged()
104 runFaceAuth(
105 FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN,
106 fallbackToDetect = false
107 )
108 }
109 .launchIn(applicationScope)
110
111 merge(
112 keyguardTransitionInteractor.aodToLockscreenTransition,
113 keyguardTransitionInteractor.offToLockscreenTransition,
114 keyguardTransitionInteractor.dozingToLockscreenTransition
115 )
116 .filter { it.transitionState == TransitionState.STARTED }
117 .onEach {
118 faceAuthenticationLogger.lockscreenBecameVisible(it)
119 runFaceAuth(
120 FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED,
121 fallbackToDetect = true
122 )
123 }
124 .launchIn(applicationScope)
125
126 deviceEntryFingerprintAuthRepository.isLockedOut
127 .onEach {
128 if (it) {
129 faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
130 repository.lockoutFaceAuth()
131 }
132 }
133 .launchIn(applicationScope)
134
135 // User switching should stop face auth and then when it is complete we should trigger face
136 // auth so that the switched user can unlock the device with face auth.
137 userRepository.userSwitchingInProgress
138 .pairwise(false)
139 .onEach { (wasSwitching, isSwitching) ->
140 if (!wasSwitching && isSwitching) {
141 repository.pauseFaceAuth()
142 } else if (wasSwitching && !isSwitching) {
143 repository.resumeFaceAuth()
144 runFaceAuth(
145 FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING,
146 fallbackToDetect = true
147 )
148 }
149 }
150 .launchIn(applicationScope)
151 }
152
153 override fun onSwipeUpOnBouncer() {
154 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)
155 }
156
157 override fun onNotificationPanelClicked() {
158 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true)
159 }
160
161 override fun onQsExpansionStared() {
162 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true)
163 }
164
165 override fun onDeviceLifted() {
166 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, true)
167 }
168
169 override fun onAssistantTriggeredOnLockScreen() {
170 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED, true)
171 }
172
173 override fun onUdfpsSensorTouched() {
174 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)
175 }
176
177 override fun onAccessibilityAction() {
178 runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false)
179 }
180
181 override fun registerListener(listener: FaceAuthenticationListener) {
182 listeners.add(listener)
183 }
184
185 override fun unregisterListener(listener: FaceAuthenticationListener) {
186 listeners.remove(listener)
187 }
188
189 override fun isLockedOut(): Boolean = repository.isLockedOut.value
190
191 override fun isRunning(): Boolean = repository.isAuthRunning.value
192
193 override fun canFaceAuthRun(): Boolean = repository.canRunFaceAuth.value
194
195 override fun isEnabled(): Boolean {
196 return featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)
197 }
198
199 override fun onPrimaryBouncerUserInput() {
200 repository.cancel()
201 }
202
203 private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null)
204 /** Provide the status of face authentication */
205 override val authenticationStatus =
206 merge(faceAuthenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
207
208 /** Provide the status of face detection */
209 override val detectionStatus = repository.detectionStatus
210
211 private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
212 if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
213 if (repository.isLockedOut.value) {
214 faceAuthenticationStatusOverride.value =
215 ErrorFaceAuthenticationStatus(
216 BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT,
217 context.resources.getString(R.string.keyguard_face_unlock_unavailable)
218 )
219 } else {
220 faceAuthenticationStatusOverride.value = null
221 applicationScope.launch {
222 withContext(mainDispatcher) {
223 faceAuthenticationLogger.authRequested(uiEvent)
224 repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
225 }
226 }
227 }
228 } else {
229 faceAuthenticationLogger.ignoredFaceAuthTrigger(
230 uiEvent,
231 ignoredReason = "Skipping face auth request because feature flag is false"
232 )
233 }
234 }
235
236 private fun observeFaceAuthStateUpdates() {
237 authenticationStatus
238 .onEach { authStatusUpdate ->
239 listeners.forEach { it.onAuthenticationStatusChanged(authStatusUpdate) }
240 }
241 .flowOn(mainDispatcher)
242 .launchIn(applicationScope)
243 detectionStatus
244 .onEach { detectionStatusUpdate ->
245 listeners.forEach { it.onDetectionStatusChanged(detectionStatusUpdate) }
246 }
247 .flowOn(mainDispatcher)
248 .launchIn(applicationScope)
249 }
250
251 companion object {
252 const val TAG = "KeyguardFaceAuthInteractor"
253 }
254 }
255
256 // Extension method that filters a generic Boolean flow to one that emits
257 // whenever there is flip from false -> true
whenItFlipsToTruenull258 private fun Flow<Boolean>.whenItFlipsToTrue(): Flow<Boolean> {
259 return this.pairwise()
260 .filter { pair -> !pair.previousValue && pair.newValue }
261 .map { it.newValue }
262 }
263