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