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