• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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