1 /*
2  * Copyright (C) 2020 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 android.annotation.IntDef;
20 import android.annotation.MainThread;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.hardware.biometrics.IBiometricService;
25 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
26 import android.os.Handler;
27 import android.os.IBinder;
28 import android.os.Looper;
29 import android.os.RemoteException;
30 import android.os.ServiceManager;
31 import android.util.Slog;
32 import android.util.proto.ProtoOutputStream;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.modules.expresslog.Counter;
36 import com.android.server.biometrics.BiometricSchedulerProto;
37 import com.android.server.biometrics.BiometricsProto;
38 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
39 
40 import java.io.PrintWriter;
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.text.SimpleDateFormat;
44 import java.util.ArrayDeque;
45 import java.util.ArrayList;
46 import java.util.Date;
47 import java.util.Deque;
48 import java.util.List;
49 import java.util.Locale;
50 import java.util.function.Consumer;
51 
52 /**
53  * A scheduler for biometric HAL operations. Maintains a queue of {@link BaseClientMonitor}
54  * operations, without caring about its implementation details. Operations may perform zero or more
55  * interactions with the HAL before finishing.
56  *
57  * We currently assume (and require) that each biometric sensor have its own instance of a
58  * {@link BiometricScheduler}.
59  */
60 @MainThread
61 public class BiometricScheduler {
62 
63     private static final String BASE_TAG = "BiometricScheduler";
64     // Number of recent operations to keep in our logs for dumpsys
65     protected static final int LOG_NUM_RECENT_OPERATIONS = 50;
66 
67     /**
68      * Unknown sensor type. This should never be used, and is a sign that something is wrong during
69      * initialization.
70      */
71     public static final int SENSOR_TYPE_UNKNOWN = 0;
72 
73     /**
74      * Face authentication.
75      */
76     public static final int SENSOR_TYPE_FACE = 1;
77 
78     /**
79      * Any UDFPS type. See {@link FingerprintSensorPropertiesInternal#isAnyUdfpsType()}.
80      */
81     public static final int SENSOR_TYPE_UDFPS = 2;
82 
83     /**
84      * Any other fingerprint sensor. We can add additional definitions in the future when necessary.
85      */
86     public static final int SENSOR_TYPE_FP_OTHER = 3;
87 
88     @IntDef({SENSOR_TYPE_UNKNOWN, SENSOR_TYPE_FACE, SENSOR_TYPE_UDFPS, SENSOR_TYPE_FP_OTHER})
89     @Retention(RetentionPolicy.SOURCE)
90     public @interface SensorType {}
91 
sensorTypeFromFingerprintProperties( @onNull FingerprintSensorPropertiesInternal props)92     public static @SensorType int sensorTypeFromFingerprintProperties(
93             @NonNull FingerprintSensorPropertiesInternal props) {
94         if (props.isAnyUdfpsType()) {
95             return SENSOR_TYPE_UDFPS;
96         }
97 
98         return SENSOR_TYPE_FP_OTHER;
99     }
100 
sensorTypeToString(@ensorType int sensorType)101     public static String sensorTypeToString(@SensorType int sensorType) {
102         switch (sensorType) {
103             case SENSOR_TYPE_UNKNOWN:
104                 return "Unknown";
105             case SENSOR_TYPE_FACE:
106                 return "Face";
107             case SENSOR_TYPE_UDFPS:
108                 return "Udfps";
109             case SENSOR_TYPE_FP_OTHER:
110                 return "OtherFp";
111             default:
112                 return "UnknownUnknown";
113         }
114     }
115 
116     private static final class CrashState {
117         static final int NUM_ENTRIES = 10;
118         final String timestamp;
119         final String currentOperation;
120         final List<String> pendingOperations;
121 
CrashState(String timestamp, String currentOperation, List<String> pendingOperations)122         CrashState(String timestamp, String currentOperation, List<String> pendingOperations) {
123             this.timestamp = timestamp;
124             this.currentOperation = currentOperation;
125             this.pendingOperations = pendingOperations;
126         }
127 
128         @Override
toString()129         public String toString() {
130             final StringBuilder sb = new StringBuilder();
131             sb.append(timestamp).append(": ");
132             sb.append("Current Operation: {").append(currentOperation).append("}");
133             sb.append(", Pending Operations(").append(pendingOperations.size()).append(")");
134 
135             if (!pendingOperations.isEmpty()) {
136                 sb.append(": ");
137             }
138             for (int i = 0; i < pendingOperations.size(); i++) {
139                 sb.append(pendingOperations.get(i));
140                 if (i < pendingOperations.size() - 1) {
141                     sb.append(", ");
142                 }
143             }
144             return sb.toString();
145         }
146     }
147 
148     @NonNull protected final String mBiometricTag;
149     private final @SensorType int mSensorType;
150     @Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
151     @NonNull private final IBiometricService mBiometricService;
152     @NonNull protected final Handler mHandler;
153     @VisibleForTesting @NonNull final Deque<BiometricSchedulerOperation> mPendingOperations;
154     @VisibleForTesting @Nullable BiometricSchedulerOperation mCurrentOperation;
155     @NonNull private final ArrayDeque<CrashState> mCrashStates;
156 
157     private int mTotalOperationsHandled;
158     private final int mRecentOperationsLimit;
159     @NonNull private final List<Integer> mRecentOperations;
160 
161     // Internal callback, notified when an operation is complete. Notifies the requester
162     // that the operation is complete, before performing internal scheduler work (such as
163     // starting the next client).
164     private final ClientMonitorCallback mInternalCallback = new ClientMonitorCallback() {
165         @Override
166         public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
167             Slog.d(getTag(), "[Started] " + clientMonitor);
168         }
169 
170         @Override
171         public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
172             mHandler.post(() -> {
173                 if (mCurrentOperation == null) {
174                     Slog.e(getTag(), "[Finishing] " + clientMonitor
175                             + " but current operation is null, success: " + success
176                             + ", possible lifecycle bug in clientMonitor implementation?");
177                     return;
178                 }
179 
180                 if (!mCurrentOperation.isFor(clientMonitor)) {
181                     Slog.e(getTag(), "[Ignoring Finish] " + clientMonitor + " does not match"
182                             + " current: " + mCurrentOperation);
183                     return;
184                 }
185 
186                 Slog.d(getTag(), "[Finishing] " + clientMonitor + ", success: " + success);
187 
188                 if (mGestureAvailabilityDispatcher != null) {
189                     mGestureAvailabilityDispatcher.markSensorActive(
190                             mCurrentOperation.getSensorId(), false /* active */);
191                 }
192 
193                 if (mRecentOperations.size() >= mRecentOperationsLimit) {
194                     mRecentOperations.remove(0);
195                 }
196                 mRecentOperations.add(mCurrentOperation.getProtoEnum());
197                 mCurrentOperation = null;
198                 mTotalOperationsHandled++;
199                 startNextOperationIfIdle();
200             });
201         }
202     };
203 
204     @VisibleForTesting
BiometricScheduler(@onNull String tag, @NonNull Handler handler, @SensorType int sensorType, @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull IBiometricService biometricService, int recentOperationsLimit)205     BiometricScheduler(@NonNull String tag,
206             @NonNull Handler handler,
207             @SensorType int sensorType,
208             @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
209             @NonNull IBiometricService biometricService,
210             int recentOperationsLimit) {
211         mBiometricTag = tag;
212         mHandler = handler;
213         mSensorType = sensorType;
214         mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher;
215         mPendingOperations = new ArrayDeque<>();
216         mBiometricService = biometricService;
217         mCrashStates = new ArrayDeque<>();
218         mRecentOperationsLimit = recentOperationsLimit;
219         mRecentOperations = new ArrayList<>();
220     }
221 
222     /**
223      * Creates a new scheduler.
224      *
225      * @param tag for the specific instance of the scheduler. Should be unique.
226      * @param sensorType the sensorType that this scheduler is handling.
227      * @param gestureAvailabilityDispatcher may be null if the sensor does not support gestures
228      *                                      (such as fingerprint swipe).
229      */
BiometricScheduler(@onNull String tag, @SensorType int sensorType, @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher)230     public BiometricScheduler(@NonNull String tag,
231             @SensorType int sensorType,
232             @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
233         this(tag, new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher,
234                 IBiometricService.Stub.asInterface(
235                         ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
236                 LOG_NUM_RECENT_OPERATIONS);
237     }
238 
239     @VisibleForTesting
getInternalCallback()240     public ClientMonitorCallback getInternalCallback() {
241         return mInternalCallback;
242     }
243 
getTag()244     protected String getTag() {
245         return BASE_TAG + "/" + mBiometricTag;
246     }
247 
startNextOperationIfIdle()248     protected void startNextOperationIfIdle() {
249         if (mCurrentOperation != null) {
250             Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation);
251             return;
252         }
253         if (mPendingOperations.isEmpty()) {
254             Slog.d(getTag(), "No operations, returning to idle");
255             return;
256         }
257 
258         mCurrentOperation = mPendingOperations.poll();
259         Slog.d(getTag(), "[Polled] " + mCurrentOperation);
260 
261         // If the operation at the front of the queue has been marked for cancellation, send
262         // ERROR_CANCELED. No need to start this client.
263         if (mCurrentOperation.isMarkedCanceling()) {
264             Slog.d(getTag(), "[Now Cancelling] " + mCurrentOperation);
265             mCurrentOperation.cancel(mHandler, mInternalCallback);
266             // Now we wait for the client to send its FinishCallback, which kicks off the next
267             // operation.
268             return;
269         }
270 
271         if (mCurrentOperation.isAcquisitionOperation()) {
272             AcquisitionClient client = (AcquisitionClient) mCurrentOperation.getClientMonitor();
273             if (client.isAlreadyCancelled()) {
274                 mCurrentOperation.cancel(mHandler, mInternalCallback);
275                 return;
276             }
277         }
278 
279         if (mGestureAvailabilityDispatcher != null && mCurrentOperation.isAcquisitionOperation()) {
280             mGestureAvailabilityDispatcher.markSensorActive(
281                     mCurrentOperation.getSensorId(), true /* active */);
282         }
283 
284         // Not all operations start immediately. BiometricPrompt waits for its operation
285         // to arrive at the head of the queue, before pinging it to start.
286         final int cookie = mCurrentOperation.isReadyToStart(mInternalCallback);
287         if (cookie == 0) {
288             if (!mCurrentOperation.start(mInternalCallback)) {
289                 // Note down current length of queue
290                 final int pendingOperationsLength = mPendingOperations.size();
291                 final BiometricSchedulerOperation lastOperation = mPendingOperations.peekLast();
292                 Slog.e(getTag(), "[Unable To Start] " + mCurrentOperation
293                         + ". Last pending operation: " + lastOperation);
294 
295                 // Then for each operation currently in the pending queue at the time of this
296                 // failure, do the same as above. Otherwise, it's possible that something like
297                 // setActiveUser fails, but then authenticate (for the wrong user) is invoked.
298                 for (int i = 0; i < pendingOperationsLength; i++) {
299                     final BiometricSchedulerOperation operation = mPendingOperations.pollFirst();
300                     if (operation != null) {
301                         Slog.w(getTag(), "[Aborting Operation] " + operation);
302                         operation.abort();
303                     } else {
304                         Slog.e(getTag(), "Null operation, index: " + i
305                                 + ", expected length: " + pendingOperationsLength);
306                     }
307                 }
308 
309                 // It's possible that during cleanup a new set of operations came in. We can try to
310                 // run these. A single request from the manager layer to the service layer may
311                 // actually be multiple operations (i.e. updateActiveUser + authenticate).
312                 mCurrentOperation = null;
313                 startNextOperationIfIdle();
314             }
315         } else {
316             try {
317                 mBiometricService.onReadyForAuthentication(
318                         mCurrentOperation.getClientMonitor().getRequestId(), cookie);
319             } catch (RemoteException e) {
320                 Slog.e(getTag(), "Remote exception when contacting BiometricService", e);
321             }
322             Slog.d(getTag(), "Waiting for cookie before starting: " + mCurrentOperation);
323         }
324     }
325 
326     /**
327      * Starts the {@link #mCurrentOperation} if
328      * 1) its state is {@link BiometricSchedulerOperation#STATE_WAITING_FOR_COOKIE} and
329      * 2) its cookie matches this cookie
330      *
331      * This is currently only used by {@link com.android.server.biometrics.BiometricService}, which
332      * requests sensors to prepare for authentication with a cookie. Once sensor(s) are ready (e.g.
333      * the BiometricService client becomes the current client in the scheduler), the cookie is
334      * returned to BiometricService. Once BiometricService decides that authentication can start,
335      * it invokes this code path.
336      *
337      * @param cookie of the operation to be started
338      */
startPreparedClient(int cookie)339     public void startPreparedClient(int cookie) {
340         if (mCurrentOperation == null) {
341             Slog.e(getTag(), "Current operation is null");
342             return;
343         }
344 
345         if (mCurrentOperation.startWithCookie(mInternalCallback, cookie)) {
346             Slog.d(getTag(), "[Started] Prepared client: " + mCurrentOperation);
347         } else {
348             Slog.e(getTag(), "[Unable To Start] Prepared client: " + mCurrentOperation);
349             mCurrentOperation = null;
350             startNextOperationIfIdle();
351         }
352     }
353 
354     /**
355      * Adds a {@link BaseClientMonitor} to the pending queue
356      *
357      * @param clientMonitor operation to be scheduled
358      */
scheduleClientMonitor(@onNull BaseClientMonitor clientMonitor)359     public void scheduleClientMonitor(@NonNull BaseClientMonitor clientMonitor) {
360         scheduleClientMonitor(clientMonitor, null /* clientFinishCallback */);
361     }
362 
363     /**
364      * Adds a {@link BaseClientMonitor} to the pending queue
365      *
366      * @param clientMonitor        operation to be scheduled
367      * @param clientCallback optional callback, invoked when the client state changes.
368      */
scheduleClientMonitor(@onNull BaseClientMonitor clientMonitor, @Nullable ClientMonitorCallback clientCallback)369     public void scheduleClientMonitor(@NonNull BaseClientMonitor clientMonitor,
370             @Nullable ClientMonitorCallback clientCallback) {
371         // If the incoming operation should interrupt preceding clients, mark any interruptable
372         // pending clients as canceling. Once they reach the head of the queue, the scheduler will
373         // send ERROR_CANCELED and skip the operation.
374         if (clientMonitor.interruptsPrecedingClients()) {
375             for (BiometricSchedulerOperation operation : mPendingOperations) {
376                 if (operation.markCanceling()) {
377                     Slog.d(getTag(), "New client, marking pending op as canceling: " + operation);
378                 }
379             }
380         }
381 
382         mPendingOperations.add(new BiometricSchedulerOperation(clientMonitor, clientCallback));
383         Slog.d(getTag(), "[Added] " + clientMonitor
384                 + ", new queue size: " + mPendingOperations.size());
385 
386         // If the new operation should interrupt preceding clients, and if the current operation is
387         // cancellable, start the cancellation process.
388         if (clientMonitor.interruptsPrecedingClients()
389                 && mCurrentOperation != null
390                 && mCurrentOperation.isInterruptable()
391                 && mCurrentOperation.isStarted()) {
392             Slog.d(getTag(), "[Cancelling Interruptable]: " + mCurrentOperation);
393             mCurrentOperation.cancel(mHandler, mInternalCallback);
394         } else {
395             startNextOperationIfIdle();
396         }
397     }
398 
399     /**
400      * Requests to cancel enrollment.
401      * @param token from the caller, should match the token passed in when requesting enrollment
402      */
cancelEnrollment(IBinder token, long requestId)403     public void cancelEnrollment(IBinder token, long requestId) {
404         Slog.d(getTag(), "cancelEnrollment, requestId: " + requestId);
405 
406         if (mCurrentOperation != null
407                 && canCancelEnrollOperation(mCurrentOperation, token, requestId)) {
408             Slog.d(getTag(), "Cancelling enrollment op: " + mCurrentOperation);
409             mCurrentOperation.cancel(mHandler, mInternalCallback);
410         } else {
411             for (BiometricSchedulerOperation operation : mPendingOperations) {
412                 if (canCancelEnrollOperation(operation, token, requestId)) {
413                     Slog.d(getTag(), "Cancelling pending enrollment op: " + operation);
414                     operation.markCanceling();
415                 }
416             }
417         }
418     }
419 
420     /**
421      * Requests to cancel authentication or detection.
422      * @param token from the caller, should match the token passed in when requesting authentication
423      * @param requestId the id returned when requesting authentication
424      */
cancelAuthenticationOrDetection(IBinder token, long requestId)425     public void cancelAuthenticationOrDetection(IBinder token, long requestId) {
426         Slog.d(getTag(), "cancelAuthenticationOrDetection, requestId: " + requestId);
427 
428         if (mCurrentOperation != null
429                 && canCancelAuthOperation(mCurrentOperation, token, requestId)) {
430             Slog.d(getTag(), "Cancelling auth/detect op: " + mCurrentOperation);
431             mCurrentOperation.cancel(mHandler, mInternalCallback);
432         } else {
433             for (BiometricSchedulerOperation operation : mPendingOperations) {
434                 if (canCancelAuthOperation(operation, token, requestId)) {
435                     Slog.d(getTag(), "Cancelling pending auth/detect op: " + operation);
436                     operation.markCanceling();
437                 }
438             }
439         }
440     }
441 
canCancelEnrollOperation(BiometricSchedulerOperation operation, IBinder token, long requestId)442     private static boolean canCancelEnrollOperation(BiometricSchedulerOperation operation,
443             IBinder token, long requestId) {
444         return operation.isEnrollOperation()
445                 && operation.isMatchingToken(token)
446                 && operation.isMatchingRequestId(requestId);
447     }
448 
canCancelAuthOperation(BiometricSchedulerOperation operation, IBinder token, long requestId)449     private static boolean canCancelAuthOperation(BiometricSchedulerOperation operation,
450             IBinder token, long requestId) {
451         // TODO: restrict callers that can cancel without requestId (negative value)?
452         return operation.isAuthenticationOrDetectionOperation()
453                 && operation.isMatchingToken(token)
454                 && operation.isMatchingRequestId(requestId);
455     }
456 
457     /**
458      * Get current operation <code>BaseClientMonitor</code>
459      * @deprecated TODO: b/229994966, encapsulate client monitors
460      * @return the current operation
461      */
462     @Deprecated
463     @Nullable
getCurrentClient()464     public BaseClientMonitor getCurrentClient() {
465         return mCurrentOperation != null ? mCurrentOperation.getClientMonitor() : null;
466     }
467 
468     /**
469      * The current operation if the requestId is set and matches.
470      * @deprecated TODO: b/229994966, encapsulate client monitors
471      */
472     @Deprecated
473     @Nullable
getCurrentClientIfMatches(long requestId, @NonNull Consumer<BaseClientMonitor> clientMonitorConsumer)474     public void getCurrentClientIfMatches(long requestId,
475             @NonNull Consumer<BaseClientMonitor> clientMonitorConsumer) {
476         mHandler.post(() -> {
477             if (mCurrentOperation != null) {
478                 if (mCurrentOperation.isMatchingRequestId(requestId)) {
479                     clientMonitorConsumer.accept(mCurrentOperation.getClientMonitor());
480                     return;
481                 }
482             }
483             clientMonitorConsumer.accept(null);
484         });
485     }
486 
getCurrentPendingCount()487     public int getCurrentPendingCount() {
488         return mPendingOperations.size();
489     }
490 
recordCrashState()491     public void recordCrashState() {
492         if (mCrashStates.size() >= CrashState.NUM_ENTRIES) {
493             mCrashStates.removeFirst();
494         }
495         final SimpleDateFormat dateFormat =
496                 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
497         final String timestamp = dateFormat.format(new Date(System.currentTimeMillis()));
498         final List<String> pendingOperations = new ArrayList<>();
499         for (BiometricSchedulerOperation operation : mPendingOperations) {
500             pendingOperations.add(operation.toString());
501         }
502 
503         final CrashState crashState = new CrashState(timestamp,
504                 mCurrentOperation != null ? mCurrentOperation.toString() : null,
505                 pendingOperations);
506         mCrashStates.add(crashState);
507         Slog.e(getTag(), "Recorded crash state: " + crashState.toString());
508     }
509 
dump(PrintWriter pw)510     public void dump(PrintWriter pw) {
511         pw.println("Dump of BiometricScheduler " + getTag());
512         pw.println("Type: " + mSensorType);
513         pw.println("Current operation: " + mCurrentOperation);
514         pw.println("Pending operations: " + mPendingOperations.size());
515         for (BiometricSchedulerOperation operation : mPendingOperations) {
516             pw.println("Pending operation: " + operation);
517         }
518         for (CrashState crashState : mCrashStates) {
519             pw.println("Crash State " + crashState);
520         }
521     }
522 
dumpProtoState(boolean clearSchedulerBuffer)523     public byte[] dumpProtoState(boolean clearSchedulerBuffer) {
524         final ProtoOutputStream proto = new ProtoOutputStream();
525         proto.write(BiometricSchedulerProto.CURRENT_OPERATION, mCurrentOperation != null
526                 ? mCurrentOperation.getProtoEnum() : BiometricsProto.CM_NONE);
527         proto.write(BiometricSchedulerProto.TOTAL_OPERATIONS, mTotalOperationsHandled);
528 
529         if (!mRecentOperations.isEmpty()) {
530             for (int i = 0; i < mRecentOperations.size(); i++) {
531                 proto.write(BiometricSchedulerProto.RECENT_OPERATIONS, mRecentOperations.get(i));
532             }
533         } else {
534             // TODO:(b/178828362) Unsure why protobuf has a problem decoding when an empty list
535             //  is returned. So, let's just add a no-op for this case.
536             proto.write(BiometricSchedulerProto.RECENT_OPERATIONS, BiometricsProto.CM_NONE);
537         }
538         proto.flush();
539 
540         if (clearSchedulerBuffer) {
541             mRecentOperations.clear();
542         }
543         return proto.getBytes();
544     }
545 
546     /**
547      * Clears the scheduler of anything work-related. This should be used for example when the
548      * HAL dies.
549      */
reset()550     public void reset() {
551         Slog.d(getTag(), "Resetting scheduler");
552         mPendingOperations.clear();
553         mCurrentOperation = null;
554     }
555 
556     /**
557      * Marks all pending operations as canceling and cancels the current
558      * operation.
559      */
clearScheduler()560     private void clearScheduler() {
561         if (mCurrentOperation == null) {
562             return;
563         }
564         for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
565             Slog.d(getTag(), "[Watchdog cancelling pending] "
566                     + pendingOperation.getClientMonitor());
567             pendingOperation.markCancelingForWatchdog();
568         }
569         Slog.d(getTag(), "[Watchdog cancelling current] "
570                 + mCurrentOperation.getClientMonitor());
571         mCurrentOperation.cancel(mHandler, getInternalCallback());
572     }
573 
574     /**
575      * Start the timeout for the watchdog.
576      */
startWatchdog()577     public void startWatchdog() {
578         if (mCurrentOperation == null) {
579             return;
580         }
581         final BiometricSchedulerOperation operation = mCurrentOperation;
582         mHandler.postDelayed(() -> {
583             if (operation == mCurrentOperation && !operation.isFinished()) {
584                 Counter.logIncrement("biometric.value_scheduler_watchdog_triggered_count");
585                 clearScheduler();
586             }
587         }, 10000);
588     }
589 }
590