1 /*
2  *   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
258 private fun Flow<Boolean>.whenItFlipsToTrue(): Flow<Boolean> {
259     return this.pairwise()
260         .filter { pair -> !pair.previousValue && pair.newValue }
261         .map { it.newValue }
262 }
263