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 final CoexCoordinator sInstance = new CoexCoordinator(); 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 /** The singleton instance. */ 151 @NonNull getInstance()152 public static CoexCoordinator getInstance() { 153 return sInstance; 154 } 155 156 @VisibleForTesting setAdvancedLogicEnabled(boolean enabled)157 public void setAdvancedLogicEnabled(boolean enabled) { 158 mAdvancedLogicEnabled = enabled; 159 } 160 setFaceHapticDisabledWhenNonBypass(boolean disabled)161 public void setFaceHapticDisabledWhenNonBypass(boolean disabled) { 162 mFaceHapticDisabledWhenNonBypass = disabled; 163 } 164 165 @VisibleForTesting reset()166 void reset() { 167 mClientMap.clear(); 168 } 169 170 // SensorType to AuthenticationClient map 171 private final Map<Integer, AuthenticationClient<?>> mClientMap = new HashMap<>(); 172 @VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths = new LinkedList<>(); 173 private boolean mAdvancedLogicEnabled; 174 private boolean mFaceHapticDisabledWhenNonBypass; 175 private final Handler mHandler = new Handler(Looper.getMainLooper()); 176 CoexCoordinator()177 private CoexCoordinator() {} 178 addAuthenticationClient(@iometricScheduler.SensorType int sensorType, @NonNull AuthenticationClient<?> client)179 public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType, 180 @NonNull AuthenticationClient<?> client) { 181 if (DEBUG) { 182 Slog.d(TAG, "addAuthenticationClient(" + sensorTypeToString(sensorType) + ")" 183 + ", client: " + client); 184 } 185 186 if (mClientMap.containsKey(sensorType)) { 187 Slog.w(TAG, "Overwriting existing client: " + mClientMap.get(sensorType) 188 + " with new client: " + client); 189 } 190 191 mClientMap.put(sensorType, client); 192 } 193 removeAuthenticationClient(@iometricScheduler.SensorType int sensorType, @NonNull AuthenticationClient<?> client)194 public void removeAuthenticationClient(@BiometricScheduler.SensorType int sensorType, 195 @NonNull AuthenticationClient<?> client) { 196 if (DEBUG) { 197 Slog.d(TAG, "removeAuthenticationClient(" + sensorTypeToString(sensorType) + ")" 198 + ", client: " + client); 199 } 200 201 if (!mClientMap.containsKey(sensorType)) { 202 Slog.e(TAG, "sensorType: " + sensorType + " does not exist in map. Client: " + client); 203 return; 204 } 205 mClientMap.remove(sensorType); 206 } 207 208 /** 209 * Notify the coordinator that authentication succeeded (accepted) 210 */ onAuthenticationSucceeded(long currentTimeMillis, @NonNull AuthenticationClient<?> client, @NonNull Callback callback)211 public void onAuthenticationSucceeded(long currentTimeMillis, 212 @NonNull AuthenticationClient<?> client, 213 @NonNull Callback callback) { 214 final boolean isUsingSingleModality = isSingleAuthOnly(client); 215 216 if (client.isBiometricPrompt()) { 217 if (!isUsingSingleModality && hasMultipleSuccessfulAuthentications()) { 218 // only send feedback on the first one 219 } else { 220 callback.sendHapticFeedback(); 221 } 222 // For BP, BiometricService will add the authToken to Keystore. 223 callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); 224 callback.handleLifecycleAfterAuth(); 225 } else if (isUnknownClient(client)) { 226 // Client doesn't exist in our map for some reason. Give the user feedback so the 227 // device doesn't feel like it's stuck. All other cases below can assume that the 228 // client exists in our map. 229 callback.sendHapticFeedback(); 230 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 231 callback.handleLifecycleAfterAuth(); 232 } else if (mAdvancedLogicEnabled && client.isKeyguard()) { 233 if (isUsingSingleModality) { 234 // Single sensor authentication 235 callback.sendHapticFeedback(); 236 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 237 callback.handleLifecycleAfterAuth(); 238 } else { 239 // Multi sensor authentication 240 AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); 241 AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 242 if (isCurrentFaceAuth(client)) { 243 if (isUdfpsActivelyAuthing(udfps)) { 244 // Face auth success while UDFPS is actively authing. No callback, no haptic 245 // Feedback will be provided after UDFPS result: 246 // 1) UDFPS succeeds - simply remove this from the queue 247 // 2) UDFPS rejected - use this face auth success to notify clients 248 mSuccessfulAuths.add(new SuccessfulAuth(mHandler, mSuccessfulAuths, 249 currentTimeMillis, SENSOR_TYPE_FACE, client, callback)); 250 } else { 251 if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) { 252 Slog.w(TAG, "Skipping face success haptic"); 253 } else { 254 callback.sendHapticFeedback(); 255 } 256 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 257 callback.handleLifecycleAfterAuth(); 258 } 259 } else if (isCurrentUdfps(client)) { 260 if (isFaceScanning()) { 261 // UDFPS succeeds while face is still scanning 262 // Cancel face auth and/or prevent it from invoking haptics/callbacks after 263 face.cancel(); 264 } 265 266 removeAndFinishAllFaceFromQueue(); 267 268 callback.sendHapticFeedback(); 269 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 270 callback.handleLifecycleAfterAuth(); 271 } else { 272 // Capacitive fingerprint sensor (or other) 273 callback.sendHapticFeedback(); 274 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 275 callback.handleLifecycleAfterAuth(); 276 } 277 } 278 } else { 279 // Non-keyguard authentication. For example, Fingerprint Settings use of 280 // FingerprintManager for highlighting fingers 281 callback.sendHapticFeedback(); 282 callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 283 callback.handleLifecycleAfterAuth(); 284 } 285 } 286 287 /** 288 * Notify the coordinator that a rejection has occurred. 289 */ onAuthenticationRejected(long currentTimeMillis, @NonNull AuthenticationClient<?> client, @LockoutTracker.LockoutMode int lockoutMode, @NonNull Callback callback)290 public void onAuthenticationRejected(long currentTimeMillis, 291 @NonNull AuthenticationClient<?> client, 292 @LockoutTracker.LockoutMode int lockoutMode, 293 @NonNull Callback callback) { 294 final boolean isUsingSingleModality = isSingleAuthOnly(client); 295 296 if (mAdvancedLogicEnabled && client.isKeyguard()) { 297 if (isUsingSingleModality) { 298 callback.sendHapticFeedback(); 299 callback.handleLifecycleAfterAuth(); 300 } else { 301 // Multi sensor authentication 302 AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); 303 AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 304 if (isCurrentFaceAuth(client)) { 305 if (isUdfpsActivelyAuthing(udfps)) { 306 // UDFPS should still be running in this case, do not vibrate. However, we 307 // should notify the callback and finish the client, so that Keyguard and 308 // BiometricScheduler do not get stuck. 309 Slog.d(TAG, "Face rejected in multi-sensor auth, udfps: " + udfps); 310 callback.handleLifecycleAfterAuth(); 311 } else if (isUdfpsAuthAttempted(udfps)) { 312 // If UDFPS is STATE_STARTED_PAUSED (e.g. finger rejected but can still 313 // auth after pointer goes down, it means UDFPS encountered a rejection. In 314 // this case, we need to play the final reject haptic since face auth is 315 // also done now. 316 callback.sendHapticFeedback(); 317 callback.handleLifecycleAfterAuth(); 318 } else { 319 // UDFPS auth has never been attempted. 320 if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) { 321 Slog.w(TAG, "Skipping face reject haptic"); 322 } else { 323 callback.sendHapticFeedback(); 324 } 325 callback.handleLifecycleAfterAuth(); 326 } 327 } else if (isCurrentUdfps(client)) { 328 // Face should either be running, or have already finished 329 SuccessfulAuth auth = popSuccessfulFaceAuthIfExists(currentTimeMillis); 330 if (auth != null) { 331 Slog.d(TAG, "Using recent auth: " + auth); 332 callback.handleLifecycleAfterAuth(); 333 334 auth.mCallback.sendHapticFeedback(); 335 auth.mCallback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); 336 auth.mCallback.handleLifecycleAfterAuth(); 337 } else { 338 Slog.d(TAG, "UDFPS rejected in multi-sensor auth"); 339 callback.sendHapticFeedback(); 340 callback.handleLifecycleAfterAuth(); 341 } 342 } else { 343 Slog.d(TAG, "Unknown client rejected: " + client); 344 callback.sendHapticFeedback(); 345 callback.handleLifecycleAfterAuth(); 346 } 347 } 348 } else if (client.isBiometricPrompt() && !isUsingSingleModality) { 349 if (!isCurrentFaceAuth(client)) { 350 callback.sendHapticFeedback(); 351 } 352 callback.handleLifecycleAfterAuth(); 353 } else { 354 callback.sendHapticFeedback(); 355 callback.handleLifecycleAfterAuth(); 356 } 357 358 // Always notify keyguard, otherwise the cached "running" state in KeyguardUpdateMonitor 359 // will get stuck. 360 if (lockoutMode == LockoutTracker.LOCKOUT_NONE) { 361 // Don't send onAuthenticationFailed if we're in lockout, it causes a 362 // janky UI on Keyguard/BiometricPrompt since "authentication failed" 363 // will show briefly and be replaced by "device locked out" message. 364 callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); 365 } 366 } 367 368 /** 369 * Notify the coordinator that an error has occurred. 370 */ onAuthenticationError(@onNull AuthenticationClient<?> client, @BiometricConstants.Errors int error, @NonNull ErrorCallback callback)371 public void onAuthenticationError(@NonNull AuthenticationClient<?> client, 372 @BiometricConstants.Errors int error, @NonNull ErrorCallback callback) { 373 final boolean isUsingSingleModality = isSingleAuthOnly(client); 374 375 // Figure out non-coex state 376 final boolean shouldUsuallyVibrate; 377 if (isCurrentFaceAuth(client)) { 378 final boolean notDetectedOnKeyguard = client.isKeyguard() && !client.wasUserDetected(); 379 final boolean authAttempted = client.wasAuthAttempted(); 380 381 switch (error) { 382 case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT: 383 case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT: 384 case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT: 385 shouldUsuallyVibrate = authAttempted && !notDetectedOnKeyguard; 386 break; 387 default: 388 shouldUsuallyVibrate = false; 389 break; 390 } 391 } else { 392 shouldUsuallyVibrate = false; 393 } 394 395 // Figure out coex state 396 final boolean hapticSuppressedByCoex; 397 if (mAdvancedLogicEnabled && client.isKeyguard()) { 398 if (isUsingSingleModality) { 399 hapticSuppressedByCoex = false; 400 } else { 401 hapticSuppressedByCoex = isCurrentFaceAuth(client) 402 && !client.isKeyguardBypassEnabled(); 403 } 404 } else if (client.isBiometricPrompt() && !isUsingSingleModality) { 405 hapticSuppressedByCoex = isCurrentFaceAuth(client); 406 } else { 407 hapticSuppressedByCoex = false; 408 } 409 410 // Combine and send feedback if appropriate 411 if (shouldUsuallyVibrate && !hapticSuppressedByCoex) { 412 callback.sendHapticFeedback(); 413 } else { 414 Slog.v(TAG, "no haptic shouldUsuallyVibrate: " + shouldUsuallyVibrate 415 + ", hapticSuppressedByCoex: " + hapticSuppressedByCoex); 416 } 417 } 418 419 @Nullable popSuccessfulFaceAuthIfExists(long currentTimeMillis)420 private SuccessfulAuth popSuccessfulFaceAuthIfExists(long currentTimeMillis) { 421 for (SuccessfulAuth auth : mSuccessfulAuths) { 422 if (currentTimeMillis - auth.mAuthTimestamp >= SUCCESSFUL_AUTH_VALID_DURATION_MS) { 423 // TODO(b/193089985): This removes the auth but does not notify the client with 424 // an appropriate lifecycle event (such as ERROR_CANCELED), and violates the 425 // API contract. However, this might be OK for now since the validity duration 426 // is way longer than the time it takes to auth with fingerprint. 427 Slog.e(TAG, "Removing stale auth: " + auth); 428 mSuccessfulAuths.remove(auth); 429 } else if (auth.mSensorType == SENSOR_TYPE_FACE) { 430 mSuccessfulAuths.remove(auth); 431 return auth; 432 } 433 } 434 return null; 435 } 436 removeAndFinishAllFaceFromQueue()437 private void removeAndFinishAllFaceFromQueue() { 438 // Note that these auth are all successful, but have never notified the client (e.g. 439 // keyguard). To comply with the authentication lifecycle, we must notify the client that 440 // auth is "done". The safest thing to do is to send ERROR_CANCELED. 441 for (SuccessfulAuth auth : mSuccessfulAuths) { 442 if (auth.mSensorType == SENSOR_TYPE_FACE) { 443 Slog.d(TAG, "Removing from queue, canceling, and finishing: " + auth); 444 auth.mCallback.sendAuthenticationCanceled(); 445 auth.mCallback.handleLifecycleAfterAuth(); 446 mSuccessfulAuths.remove(auth); 447 } 448 } 449 } 450 isCurrentFaceAuth(@onNull AuthenticationClient<?> client)451 private boolean isCurrentFaceAuth(@NonNull AuthenticationClient<?> client) { 452 return client == mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 453 } 454 isCurrentUdfps(@onNull AuthenticationClient<?> client)455 private boolean isCurrentUdfps(@NonNull AuthenticationClient<?> client) { 456 return client == mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); 457 } 458 isFaceScanning()459 private boolean isFaceScanning() { 460 AuthenticationClient<?> client = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); 461 return client != null && client.getState() == AuthenticationClient.STATE_STARTED; 462 } 463 isUdfpsActivelyAuthing(@ullable AuthenticationClient<?> client)464 private static boolean isUdfpsActivelyAuthing(@Nullable AuthenticationClient<?> client) { 465 if (client instanceof Udfps) { 466 return client.getState() == AuthenticationClient.STATE_STARTED; 467 } 468 return false; 469 } 470 isUdfpsAuthAttempted(@ullable AuthenticationClient<?> client)471 private static boolean isUdfpsAuthAttempted(@Nullable AuthenticationClient<?> client) { 472 if (client instanceof Udfps) { 473 return client.getState() == AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED; 474 } 475 return false; 476 } 477 isUnknownClient(@onNull AuthenticationClient<?> client)478 private boolean isUnknownClient(@NonNull AuthenticationClient<?> client) { 479 for (AuthenticationClient<?> c : mClientMap.values()) { 480 if (c == client) { 481 return false; 482 } 483 } 484 return true; 485 } 486 isSingleAuthOnly(@onNull AuthenticationClient<?> client)487 private boolean isSingleAuthOnly(@NonNull AuthenticationClient<?> client) { 488 if (mClientMap.values().size() != 1) { 489 return false; 490 } 491 492 for (AuthenticationClient<?> c : mClientMap.values()) { 493 if (c != client) { 494 return false; 495 } 496 } 497 return true; 498 } 499 hasMultipleSuccessfulAuthentications()500 private boolean hasMultipleSuccessfulAuthentications() { 501 int count = 0; 502 for (AuthenticationClient<?> c : mClientMap.values()) { 503 if (c.wasAuthSuccessful()) { 504 count++; 505 } 506 if (count > 1) { 507 return true; 508 } 509 } 510 return false; 511 } 512 513 @Override toString()514 public String toString() { 515 StringBuilder sb = new StringBuilder(); 516 sb.append("Enabled: ").append(mAdvancedLogicEnabled); 517 sb.append(", Face Haptic Disabled: ").append(mFaceHapticDisabledWhenNonBypass); 518 sb.append(", Queue size: " ).append(mSuccessfulAuths.size()); 519 for (SuccessfulAuth auth : mSuccessfulAuths) { 520 sb.append(", Auth: ").append(auth.toString()); 521 } 522 523 return sb.toString(); 524 } 525 } 526