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