1 /* 2 * Copyright (C) 2021 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.server.biometrics.sensors; 18 19 import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_FACE; 20 import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_UDFPS; 21 import static com.android.server.biometrics.sensors.BiometricScheduler.sensorTypeToString; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.hardware.biometrics.BiometricConstants; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.util.Slog; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.server.biometrics.sensors.BiometricScheduler.SensorType; 32 import com.android.server.biometrics.sensors.fingerprint.Udfps; 33 34 import java.util.HashMap; 35 import java.util.LinkedList; 36 import java.util.Map; 37 38 /** 39 * Singleton that contains the core logic for determining if haptics and authentication callbacks 40 * should be sent to receivers. Note that this class is used even when coex is not required (e.g. 41 * single sensor devices, or multi-sensor devices where only a single sensor is authenticating). 42 * This allows us to have all business logic in one testable place. 43 */ 44 public class CoexCoordinator { 45 46 private static final String TAG = "BiometricCoexCoordinator"; 47 public static final String SETTING_ENABLE_NAME = 48 "com.android.server.biometrics.sensors.CoexCoordinator.enable"; 49 public static final String FACE_HAPTIC_DISABLE = 50 "com.android.server.biometrics.sensors.CoexCoordinator.disable_face_haptics"; 51 private static final boolean DEBUG = true; 52 53 // Successful authentications should be used within this amount of time. 54 static final long SUCCESSFUL_AUTH_VALID_DURATION_MS = 5000; 55 56 /** 57 * Callback interface notifying the owner of "results" from the CoexCoordinator's business 58 * logic for accept and reject. 59 */ 60 interface Callback { 61 /** 62 * Requests the owner to send the result (success/reject) and any associated info to the 63 * receiver (e.g. keyguard, BiometricService, etc). 64 */ sendAuthenticationResult(boolean addAuthTokenIfStrong)65 void sendAuthenticationResult(boolean addAuthTokenIfStrong); 66 67 /** 68 * Requests the owner to initiate a vibration for this event. 69 */ sendHapticFeedback()70 void sendHapticFeedback(); 71 72 /** 73 * Requests the owner to handle the AuthenticationClient's lifecycle (e.g. finish and remove 74 * from scheduler if auth was successful). 75 */ handleLifecycleAfterAuth()76 void handleLifecycleAfterAuth(); 77 78 /** 79 * Requests the owner to notify the caller that authentication was canceled. 80 */ sendAuthenticationCanceled()81 void sendAuthenticationCanceled(); 82 } 83 84 /** 85 * Callback interface notifying the owner of "results" from the CoexCoordinator's business 86 * logic for errors. 87 */ 88 interface ErrorCallback { 89 /** 90 * Requests the owner to initiate a vibration for this event. 91 */ sendHapticFeedback()92 void sendHapticFeedback(); 93 } 94 95 private static CoexCoordinator sInstance; 96 97 @VisibleForTesting 98 public static class SuccessfulAuth { 99 final long mAuthTimestamp; 100 final @SensorType int mSensorType; 101 final AuthenticationClient<?> mAuthenticationClient; 102 final Callback mCallback; 103 final CleanupRunnable mCleanupRunnable; 104 105 public static class CleanupRunnable implements Runnable { 106 @NonNull final LinkedList<SuccessfulAuth> mSuccessfulAuths; 107 @NonNull final SuccessfulAuth mAuth; 108 @NonNull final Callback mCallback; 109 CleanupRunnable(@onNull LinkedList<SuccessfulAuth> successfulAuths, @NonNull SuccessfulAuth auth, @NonNull Callback callback)110 public CleanupRunnable(@NonNull LinkedList<SuccessfulAuth> successfulAuths, 111 @NonNull SuccessfulAuth auth, @NonNull Callback callback) { 112 mSuccessfulAuths = successfulAuths; 113 mAuth = auth; 114 mCallback = callback; 115 } 116 117 @Override run()118 public void run() { 119 final boolean removed = mSuccessfulAuths.remove(mAuth); 120 Slog.w(TAG, "Removing stale successfulAuth: " + mAuth.toString() 121 + ", success: " + removed); 122 mCallback.handleLifecycleAfterAuth(); 123 } 124 } 125 SuccessfulAuth(@onNull Handler handler, @NonNull LinkedList<SuccessfulAuth> successfulAuths, long currentTimeMillis, @SensorType int sensorType, @NonNull AuthenticationClient<?> authenticationClient, @NonNull Callback callback)126 public SuccessfulAuth(@NonNull Handler handler, 127 @NonNull LinkedList<SuccessfulAuth> successfulAuths, 128 long currentTimeMillis, 129 @SensorType int sensorType, 130 @NonNull AuthenticationClient<?> authenticationClient, 131 @NonNull Callback callback) { 132 mAuthTimestamp = currentTimeMillis; 133 mSensorType = sensorType; 134 mAuthenticationClient = authenticationClient; 135 mCallback = callback; 136 137 mCleanupRunnable = new CleanupRunnable(successfulAuths, this, callback); 138 139 handler.postDelayed(mCleanupRunnable, SUCCESSFUL_AUTH_VALID_DURATION_MS); 140 } 141 142 @Override toString()143 public String toString() { 144 return "SensorType: " + sensorTypeToString(mSensorType) 145 + ", mAuthTimestamp: " + mAuthTimestamp 146 + ", authenticationClient: " + mAuthenticationClient; 147 } 148 } 149 150 /** 151 * @return a singleton instance. 152 */ 153 @NonNull getInstance()154 public static CoexCoordinator getInstance() { 155 if (sInstance == null) { 156 sInstance = new CoexCoordinator(); 157 } 158 return sInstance; 159 } 160 161 @VisibleForTesting setAdvancedLogicEnabled(boolean enabled)162 public void setAdvancedLogicEnabled(boolean enabled) { 163 mAdvancedLogicEnabled = enabled; 164 } 165 setFaceHapticDisabledWhenNonBypass(boolean disabled)166 public void setFaceHapticDisabledWhenNonBypass(boolean disabled) { 167 mFaceHapticDisabledWhenNonBypass = disabled; 168 } 169 170 @VisibleForTesting reset()171 void reset() { 172 mClientMap.clear(); 173 } 174 175 // SensorType to AuthenticationClient map 176 private final Map<Integer, AuthenticationClient<?>> mClientMap; 177 @VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths; 178 private boolean mAdvancedLogicEnabled; 179 private boolean mFaceHapticDisabledWhenNonBypass; 180 private final Handler mHandler; 181 CoexCoordinator()182 private CoexCoordinator() { 183 // Singleton 184 mClientMap = new HashMap<>(); 185 mSuccessfulAuths = new LinkedList<>(); 186 mHandler = new Handler(Looper.getMainLooper()); 187 } 188 addAuthenticationClient(@iometricScheduler.SensorType int sensorType, @NonNull AuthenticationClient<?> client)189 public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType, 190 @NonNull AuthenticationClient<?> client) { 191 if (DEBUG) { 192 Slog.d(TAG, "addAuthenticationClient(" + sensorTypeToString(sensorType) + ")" 193 + ", client: " + client); 194 } 195 196 if (mClientMap.containsKey(sensorType)) { 197 Slog.w(TAG, "Overwriting existing client: " + mClientMap.get(sensorType) 198 + " with new client: " + client); 199 } 200 201 mClientMap.put(sensorType, client); 202 } 203 removeAuthenticationClient(@iometricScheduler.SensorType int sensorType, @NonNull AuthenticationClient<?> client)204 public void removeAuthenticationClient(@BiometricScheduler.SensorType int sensorType, 205 @NonNull AuthenticationClient<?> client) { 206 if (DEBUG) { 207 Slog.d(TAG, "removeAuthenticationClient(" + sensorTypeToString(sensorType) + ")" 208 + ", client: " + client); 209 } 210 211 if (!mClientMap.containsKey(sensorType)) { 212 Slog.e(TAG, "sensorType: " + sensorType + " does not exist in map. Client: " + client); 213 return; 214 } 215 mClientMap.remove(sensorType); 216 } 217 218 /** 219 * Notify the coordinator that authentication succeeded (accepted) 220 */ onAuthenticationSucceeded(long currentTimeMillis, @NonNull AuthenticationClient<?> client, @NonNull Callback callback)221 public void onAuthenticationSucceeded(long currentTimeMillis, 222 @NonNull AuthenticationClient<?> client, 223 @NonNull Callback callback) { 224 if (client.isBiometricPrompt()) { 225 callback.sendHapticFeedback(); 226 // For BP, BiometricService will add the authToken to Keystore. 227 callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); 228 callback.handleLifecycleAfterAuth(); 229 } else if (isUnknownClient(client)) { 230 // Client doesn't exist in our map for some reason. Give the user feedback so the 231 // device doesn't feel like it's stuck. All other cases below can assume that the 232 // client exists in our map. 233 callback.sendHapticFeedback(); 234 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 235 callback.handleLifecycleAfterAuth(); 236 } else if (mAdvancedLogicEnabled && client.isKeyguard()) { 237 if (isSingleAuthOnly(client)) { 238 // Single sensor authentication 239 callback.sendHapticFeedback(); 240 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 241 callback.handleLifecycleAfterAuth(); 242 } else { 243 // Multi sensor authentication 244 AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); 245 AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 246 if (isCurrentFaceAuth(client)) { 247 if (isUdfpsActivelyAuthing(udfps)) { 248 // Face auth success while UDFPS is actively authing. No callback, no haptic 249 // Feedback will be provided after UDFPS result: 250 // 1) UDFPS succeeds - simply remove this from the queue 251 // 2) UDFPS rejected - use this face auth success to notify clients 252 mSuccessfulAuths.add(new SuccessfulAuth(mHandler, mSuccessfulAuths, 253 currentTimeMillis, SENSOR_TYPE_FACE, client, callback)); 254 } else { 255 if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) { 256 Slog.w(TAG, "Skipping face success haptic"); 257 } else { 258 callback.sendHapticFeedback(); 259 } 260 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 261 callback.handleLifecycleAfterAuth(); 262 } 263 } else if (isCurrentUdfps(client)) { 264 if (isFaceScanning()) { 265 // UDFPS succeeds while face is still scanning 266 // Cancel face auth and/or prevent it from invoking haptics/callbacks after 267 face.cancel(); 268 } 269 270 removeAndFinishAllFaceFromQueue(); 271 272 callback.sendHapticFeedback(); 273 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 274 callback.handleLifecycleAfterAuth(); 275 } else { 276 // Capacitive fingerprint sensor (or other) 277 callback.sendHapticFeedback(); 278 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 279 callback.handleLifecycleAfterAuth(); 280 } 281 } 282 } else { 283 // Non-keyguard authentication. For example, Fingerprint Settings use of 284 // FingerprintManager for highlighting fingers 285 callback.sendHapticFeedback(); 286 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 287 callback.handleLifecycleAfterAuth(); 288 } 289 } 290 291 /** 292 * Notify the coordinator that a rejection has occurred. 293 */ onAuthenticationRejected(long currentTimeMillis, @NonNull AuthenticationClient<?> client, @LockoutTracker.LockoutMode int lockoutMode, @NonNull Callback callback)294 public void onAuthenticationRejected(long currentTimeMillis, 295 @NonNull AuthenticationClient<?> client, 296 @LockoutTracker.LockoutMode int lockoutMode, 297 @NonNull Callback callback) { 298 final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard(); 299 300 if (keyguardAdvancedLogic) { 301 if (isSingleAuthOnly(client)) { 302 callback.sendHapticFeedback(); 303 callback.handleLifecycleAfterAuth(); 304 } else { 305 // Multi sensor authentication 306 AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); 307 AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 308 if (isCurrentFaceAuth(client)) { 309 if (isUdfpsActivelyAuthing(udfps)) { 310 // UDFPS should still be running in this case, do not vibrate. However, we 311 // should notify the callback and finish the client, so that Keyguard and 312 // BiometricScheduler do not get stuck. 313 Slog.d(TAG, "Face rejected in multi-sensor auth, udfps: " + udfps); 314 callback.handleLifecycleAfterAuth(); 315 } else if (isUdfpsAuthAttempted(udfps)) { 316 // If UDFPS is STATE_STARTED_PAUSED (e.g. finger rejected but can still 317 // auth after pointer goes down, it means UDFPS encountered a rejection. In 318 // this case, we need to play the final reject haptic since face auth is 319 // also done now. 320 callback.sendHapticFeedback(); 321 callback.handleLifecycleAfterAuth(); 322 } 323 else { 324 // UDFPS auth has never been attempted. 325 if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) { 326 Slog.w(TAG, "Skipping face reject haptic"); 327 } else { 328 callback.sendHapticFeedback(); 329 } 330 callback.handleLifecycleAfterAuth(); 331 } 332 } else if (isCurrentUdfps(client)) { 333 // Face should either be running, or have already finished 334 SuccessfulAuth auth = popSuccessfulFaceAuthIfExists(currentTimeMillis); 335 if (auth != null) { 336 Slog.d(TAG, "Using recent auth: " + auth); 337 callback.handleLifecycleAfterAuth(); 338 339 auth.mCallback.sendHapticFeedback(); 340 auth.mCallback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 341 auth.mCallback.handleLifecycleAfterAuth(); 342 } else if (isFaceScanning()) { 343 // UDFPS rejected but face is still scanning 344 Slog.d(TAG, "UDFPS rejected in multi-sensor auth, face: " + face); 345 callback.handleLifecycleAfterAuth(); 346 347 // TODO(b/193089985): Enforce/ensure that face auth finishes (whether 348 // accept/reject) within X amount of time. Otherwise users will be stuck 349 // waiting with their finger down for a long time. 350 } else { 351 // Face not scanning, and was not found in the queue. Most likely, face 352 // auth was too long ago. 353 Slog.d(TAG, "UDFPS rejected in multi-sensor auth, face not scanning"); 354 callback.sendHapticFeedback(); 355 callback.handleLifecycleAfterAuth(); 356 } 357 } else { 358 Slog.d(TAG, "Unknown client rejected: " + client); 359 callback.sendHapticFeedback(); 360 callback.handleLifecycleAfterAuth(); 361 } 362 } 363 } else { 364 callback.sendHapticFeedback(); 365 callback.handleLifecycleAfterAuth(); 366 } 367 368 // Always notify keyguard, otherwise the cached "running" state in KeyguardUpdateMonitor 369 // will get stuck. 370 if (lockoutMode == LockoutTracker.LOCKOUT_NONE) { 371 // Don't send onAuthenticationFailed if we're in lockout, it causes a 372 // janky UI on Keyguard/BiometricPrompt since "authentication failed" 373 // will show briefly and be replaced by "device locked out" message. 374 callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); 375 } 376 } 377 378 /** 379 * Notify the coordinator that an error has occurred. 380 */ onAuthenticationError(@onNull AuthenticationClient<?> client, @BiometricConstants.Errors int error, @NonNull ErrorCallback callback)381 public void onAuthenticationError(@NonNull AuthenticationClient<?> client, 382 @BiometricConstants.Errors int error, @NonNull ErrorCallback callback) { 383 // Figure out non-coex state 384 final boolean shouldUsuallyVibrate; 385 if (isCurrentFaceAuth(client)) { 386 final boolean notDetectedOnKeyguard = client.isKeyguard() && !client.wasUserDetected(); 387 final boolean authAttempted = client.wasAuthAttempted(); 388 389 switch (error) { 390 case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT: 391 case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT: 392 case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT: 393 shouldUsuallyVibrate = authAttempted && !notDetectedOnKeyguard; 394 break; 395 default: 396 shouldUsuallyVibrate = false; 397 break; 398 } 399 } else { 400 shouldUsuallyVibrate = false; 401 } 402 403 // Figure out coex state 404 final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard(); 405 final boolean hapticSuppressedByCoex; 406 407 if (keyguardAdvancedLogic) { 408 if (isSingleAuthOnly(client)) { 409 hapticSuppressedByCoex = false; 410 } else { 411 hapticSuppressedByCoex = isCurrentFaceAuth(client) 412 && !client.isKeyguardBypassEnabled(); 413 } 414 } else { 415 hapticSuppressedByCoex = false; 416 } 417 418 // Combine and send feedback if appropriate 419 Slog.d(TAG, "shouldUsuallyVibrate: " + shouldUsuallyVibrate 420 + ", hapticSuppressedByCoex: " + hapticSuppressedByCoex); 421 if (shouldUsuallyVibrate && !hapticSuppressedByCoex) { 422 callback.sendHapticFeedback(); 423 } 424 } 425 426 @Nullable popSuccessfulFaceAuthIfExists(long currentTimeMillis)427 private SuccessfulAuth popSuccessfulFaceAuthIfExists(long currentTimeMillis) { 428 for (SuccessfulAuth auth : mSuccessfulAuths) { 429 if (currentTimeMillis - auth.mAuthTimestamp >= SUCCESSFUL_AUTH_VALID_DURATION_MS) { 430 // TODO(b/193089985): This removes the auth but does not notify the client with 431 // an appropriate lifecycle event (such as ERROR_CANCELED), and violates the 432 // API contract. However, this might be OK for now since the validity duration 433 // is way longer than the time it takes to auth with fingerprint. 434 Slog.e(TAG, "Removing stale auth: " + auth); 435 mSuccessfulAuths.remove(auth); 436 } else if (auth.mSensorType == SENSOR_TYPE_FACE) { 437 mSuccessfulAuths.remove(auth); 438 return auth; 439 } 440 } 441 return null; 442 } 443 removeAndFinishAllFaceFromQueue()444 private void removeAndFinishAllFaceFromQueue() { 445 // Note that these auth are all successful, but have never notified the client (e.g. 446 // keyguard). To comply with the authentication lifecycle, we must notify the client that 447 // auth is "done". The safest thing to do is to send ERROR_CANCELED. 448 for (SuccessfulAuth auth : mSuccessfulAuths) { 449 if (auth.mSensorType == SENSOR_TYPE_FACE) { 450 Slog.d(TAG, "Removing from queue, canceling, and finishing: " + auth); 451 auth.mCallback.sendAuthenticationCanceled(); 452 auth.mCallback.handleLifecycleAfterAuth(); 453 mSuccessfulAuths.remove(auth); 454 } 455 } 456 } 457 isCurrentFaceAuth(@onNull AuthenticationClient<?> client)458 private boolean isCurrentFaceAuth(@NonNull AuthenticationClient<?> client) { 459 return client == mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 460 } 461 isCurrentUdfps(@onNull AuthenticationClient<?> client)462 private boolean isCurrentUdfps(@NonNull AuthenticationClient<?> client) { 463 return client == mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); 464 } 465 isFaceScanning()466 private boolean isFaceScanning() { 467 AuthenticationClient<?> client = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 468 return client != null && client.getState() == AuthenticationClient.STATE_STARTED; 469 } 470 isUdfpsActivelyAuthing(@ullable AuthenticationClient<?> client)471 private static boolean isUdfpsActivelyAuthing(@Nullable AuthenticationClient<?> client) { 472 if (client instanceof Udfps) { 473 return client.getState() == AuthenticationClient.STATE_STARTED; 474 } 475 return false; 476 } 477 isUdfpsAuthAttempted(@ullable AuthenticationClient<?> client)478 private static boolean isUdfpsAuthAttempted(@Nullable AuthenticationClient<?> client) { 479 if (client instanceof Udfps) { 480 return client.getState() == AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED; 481 } 482 return false; 483 } 484 isUnknownClient(@onNull AuthenticationClient<?> client)485 private boolean isUnknownClient(@NonNull AuthenticationClient<?> client) { 486 for (AuthenticationClient<?> c : mClientMap.values()) { 487 if (c == client) { 488 return false; 489 } 490 } 491 return true; 492 } 493 isSingleAuthOnly(@onNull AuthenticationClient<?> client)494 private boolean isSingleAuthOnly(@NonNull AuthenticationClient<?> client) { 495 if (mClientMap.values().size() != 1) { 496 return false; 497 } 498 499 for (AuthenticationClient<?> c : mClientMap.values()) { 500 if (c != client) { 501 return false; 502 } 503 } 504 return true; 505 } 506 507 @Override toString()508 public String toString() { 509 StringBuilder sb = new StringBuilder(); 510 sb.append("Enabled: ").append(mAdvancedLogicEnabled); 511 sb.append(", Face Haptic Disabled: ").append(mFaceHapticDisabledWhenNonBypass); 512 sb.append(", Queue size: " ).append(mSuccessfulAuths.size()); 513 for (SuccessfulAuth auth : mSuccessfulAuths) { 514 sb.append(", Auth: ").append(auth.toString()); 515 } 516 517 return sb.toString(); 518 } 519 } 520