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