| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.biometrics.sensors; |
| |
| import android.annotation.IntDef; |
| import android.annotation.MainThread; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.hardware.biometrics.IBiometricService; |
| import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.util.Slog; |
| import android.util.proto.ProtoOutputStream; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.biometrics.BiometricSchedulerProto; |
| import com.android.server.biometrics.BiometricsProto; |
| import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; |
| |
| import java.io.PrintWriter; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.Deque; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * A scheduler for biometric HAL operations. Maintains a queue of {@link BaseClientMonitor} |
| * operations, without caring about its implementation details. Operations may perform zero or more |
| * interactions with the HAL before finishing. |
| * |
| * We currently assume (and require) that each biometric sensor have its own instance of a |
| * {@link BiometricScheduler}. See {@link CoexCoordinator}. |
| */ |
| @MainThread |
| public class BiometricScheduler { |
| |
| private static final String BASE_TAG = "BiometricScheduler"; |
| // Number of recent operations to keep in our logs for dumpsys |
| protected static final int LOG_NUM_RECENT_OPERATIONS = 50; |
| |
| /** |
| * Unknown sensor type. This should never be used, and is a sign that something is wrong during |
| * initialization. |
| */ |
| public static final int SENSOR_TYPE_UNKNOWN = 0; |
| |
| /** |
| * Face authentication. |
| */ |
| public static final int SENSOR_TYPE_FACE = 1; |
| |
| /** |
| * Any UDFPS type. See {@link FingerprintSensorPropertiesInternal#isAnyUdfpsType()}. |
| */ |
| public static final int SENSOR_TYPE_UDFPS = 2; |
| |
| /** |
| * Any other fingerprint sensor. We can add additional definitions in the future when necessary. |
| */ |
| public static final int SENSOR_TYPE_FP_OTHER = 3; |
| |
| @IntDef({SENSOR_TYPE_UNKNOWN, SENSOR_TYPE_FACE, SENSOR_TYPE_UDFPS, SENSOR_TYPE_FP_OTHER}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SensorType {} |
| |
| public static @SensorType int sensorTypeFromFingerprintProperties( |
| @NonNull FingerprintSensorPropertiesInternal props) { |
| if (props.isAnyUdfpsType()) { |
| return SENSOR_TYPE_UDFPS; |
| } |
| |
| return SENSOR_TYPE_FP_OTHER; |
| } |
| |
| public static String sensorTypeToString(@SensorType int sensorType) { |
| switch (sensorType) { |
| case SENSOR_TYPE_UNKNOWN: |
| return "Unknown"; |
| case SENSOR_TYPE_FACE: |
| return "Face"; |
| case SENSOR_TYPE_UDFPS: |
| return "Udfps"; |
| case SENSOR_TYPE_FP_OTHER: |
| return "OtherFp"; |
| default: |
| return "UnknownUnknown"; |
| } |
| } |
| |
| private static final class CrashState { |
| static final int NUM_ENTRIES = 10; |
| final String timestamp; |
| final String currentOperation; |
| final List<String> pendingOperations; |
| |
| CrashState(String timestamp, String currentOperation, List<String> pendingOperations) { |
| this.timestamp = timestamp; |
| this.currentOperation = currentOperation; |
| this.pendingOperations = pendingOperations; |
| } |
| |
| @Override |
| public String toString() { |
| final StringBuilder sb = new StringBuilder(); |
| sb.append(timestamp).append(": "); |
| sb.append("Current Operation: {").append(currentOperation).append("}"); |
| sb.append(", Pending Operations(").append(pendingOperations.size()).append(")"); |
| |
| if (!pendingOperations.isEmpty()) { |
| sb.append(": "); |
| } |
| for (int i = 0; i < pendingOperations.size(); i++) { |
| sb.append(pendingOperations.get(i)); |
| if (i < pendingOperations.size() - 1) { |
| sb.append(", "); |
| } |
| } |
| return sb.toString(); |
| } |
| } |
| |
| @NonNull protected final String mBiometricTag; |
| private final @SensorType int mSensorType; |
| @Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher; |
| @NonNull private final IBiometricService mBiometricService; |
| @NonNull protected final Handler mHandler; |
| @VisibleForTesting @NonNull final Deque<BiometricSchedulerOperation> mPendingOperations; |
| @VisibleForTesting @Nullable BiometricSchedulerOperation mCurrentOperation; |
| @NonNull private final ArrayDeque<CrashState> mCrashStates; |
| |
| private int mTotalOperationsHandled; |
| private final int mRecentOperationsLimit; |
| @NonNull private final List<Integer> mRecentOperations; |
| @NonNull private final CoexCoordinator mCoexCoordinator; |
| |
| // Internal callback, notified when an operation is complete. Notifies the requester |
| // that the operation is complete, before performing internal scheduler work (such as |
| // starting the next client). |
| private final BaseClientMonitor.Callback mInternalCallback = new BaseClientMonitor.Callback() { |
| @Override |
| public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { |
| Slog.d(getTag(), "[Started] " + clientMonitor); |
| |
| if (clientMonitor instanceof AuthenticationClient) { |
| mCoexCoordinator.addAuthenticationClient(mSensorType, |
| (AuthenticationClient<?>) clientMonitor); |
| } |
| } |
| |
| @Override |
| public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { |
| mHandler.post(() -> { |
| if (mCurrentOperation == null) { |
| Slog.e(getTag(), "[Finishing] " + clientMonitor |
| + " but current operation is null, success: " + success |
| + ", possible lifecycle bug in clientMonitor implementation?"); |
| return; |
| } |
| |
| if (!mCurrentOperation.isFor(clientMonitor)) { |
| Slog.e(getTag(), "[Ignoring Finish] " + clientMonitor + " does not match" |
| + " current: " + mCurrentOperation); |
| return; |
| } |
| |
| Slog.d(getTag(), "[Finishing] " + clientMonitor + ", success: " + success); |
| if (clientMonitor instanceof AuthenticationClient) { |
| mCoexCoordinator.removeAuthenticationClient(mSensorType, |
| (AuthenticationClient<?>) clientMonitor); |
| } |
| |
| if (mGestureAvailabilityDispatcher != null) { |
| mGestureAvailabilityDispatcher.markSensorActive( |
| mCurrentOperation.getSensorId(), false /* active */); |
| } |
| |
| if (mRecentOperations.size() >= mRecentOperationsLimit) { |
| mRecentOperations.remove(0); |
| } |
| mRecentOperations.add(mCurrentOperation.getProtoEnum()); |
| mCurrentOperation = null; |
| mTotalOperationsHandled++; |
| startNextOperationIfIdle(); |
| }); |
| } |
| }; |
| |
| @VisibleForTesting |
| BiometricScheduler(@NonNull String tag, |
| @NonNull Handler handler, |
| @SensorType int sensorType, |
| @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, |
| @NonNull IBiometricService biometricService, |
| int recentOperationsLimit, |
| @NonNull CoexCoordinator coexCoordinator) { |
| mBiometricTag = tag; |
| mHandler = handler; |
| mSensorType = sensorType; |
| mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher; |
| mPendingOperations = new ArrayDeque<>(); |
| mBiometricService = biometricService; |
| mCrashStates = new ArrayDeque<>(); |
| mRecentOperationsLimit = recentOperationsLimit; |
| mRecentOperations = new ArrayList<>(); |
| mCoexCoordinator = coexCoordinator; |
| } |
| |
| /** |
| * Creates a new scheduler. |
| * |
| * @param tag for the specific instance of the scheduler. Should be unique. |
| * @param sensorType the sensorType that this scheduler is handling. |
| * @param gestureAvailabilityDispatcher may be null if the sensor does not support gestures |
| * (such as fingerprint swipe). |
| */ |
| public BiometricScheduler(@NonNull String tag, |
| @SensorType int sensorType, |
| @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { |
| this(tag, new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher, |
| IBiometricService.Stub.asInterface( |
| ServiceManager.getService(Context.BIOMETRIC_SERVICE)), |
| LOG_NUM_RECENT_OPERATIONS, CoexCoordinator.getInstance()); |
| } |
| |
| @VisibleForTesting |
| public BaseClientMonitor.Callback getInternalCallback() { |
| return mInternalCallback; |
| } |
| |
| protected String getTag() { |
| return BASE_TAG + "/" + mBiometricTag; |
| } |
| |
| protected void startNextOperationIfIdle() { |
| if (mCurrentOperation != null) { |
| Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation); |
| return; |
| } |
| if (mPendingOperations.isEmpty()) { |
| Slog.d(getTag(), "No operations, returning to idle"); |
| return; |
| } |
| |
| mCurrentOperation = mPendingOperations.poll(); |
| Slog.d(getTag(), "[Polled] " + mCurrentOperation); |
| |
| // If the operation at the front of the queue has been marked for cancellation, send |
| // ERROR_CANCELED. No need to start this client. |
| if (mCurrentOperation.isMarkedCanceling()) { |
| Slog.d(getTag(), "[Now Cancelling] " + mCurrentOperation); |
| mCurrentOperation.cancel(mHandler, mInternalCallback); |
| // Now we wait for the client to send its FinishCallback, which kicks off the next |
| // operation. |
| return; |
| } |
| |
| if (mGestureAvailabilityDispatcher != null && mCurrentOperation.isAcquisitionOperation()) { |
| mGestureAvailabilityDispatcher.markSensorActive( |
| mCurrentOperation.getSensorId(), true /* active */); |
| } |
| |
| // Not all operations start immediately. BiometricPrompt waits for its operation |
| // to arrive at the head of the queue, before pinging it to start. |
| final int cookie = mCurrentOperation.isReadyToStart(); |
| if (cookie == 0) { |
| if (!mCurrentOperation.start(mInternalCallback)) { |
| // Note down current length of queue |
| final int pendingOperationsLength = mPendingOperations.size(); |
| final BiometricSchedulerOperation lastOperation = mPendingOperations.peekLast(); |
| Slog.e(getTag(), "[Unable To Start] " + mCurrentOperation |
| + ". Last pending operation: " + lastOperation); |
| |
| // Then for each operation currently in the pending queue at the time of this |
| // failure, do the same as above. Otherwise, it's possible that something like |
| // setActiveUser fails, but then authenticate (for the wrong user) is invoked. |
| for (int i = 0; i < pendingOperationsLength; i++) { |
| final BiometricSchedulerOperation operation = mPendingOperations.pollFirst(); |
| if (operation != null) { |
| Slog.w(getTag(), "[Aborting Operation] " + operation); |
| operation.abort(); |
| } else { |
| Slog.e(getTag(), "Null operation, index: " + i |
| + ", expected length: " + pendingOperationsLength); |
| } |
| } |
| |
| // It's possible that during cleanup a new set of operations came in. We can try to |
| // run these. A single request from the manager layer to the service layer may |
| // actually be multiple operations (i.e. updateActiveUser + authenticate). |
| mCurrentOperation = null; |
| startNextOperationIfIdle(); |
| } |
| } else { |
| try { |
| mBiometricService.onReadyForAuthentication(cookie); |
| } catch (RemoteException e) { |
| Slog.e(getTag(), "Remote exception when contacting BiometricService", e); |
| } |
| Slog.d(getTag(), "Waiting for cookie before starting: " + mCurrentOperation); |
| } |
| } |
| |
| /** |
| * Starts the {@link #mCurrentOperation} if |
| * 1) its state is {@link BiometricSchedulerOperation#STATE_WAITING_FOR_COOKIE} and |
| * 2) its cookie matches this cookie |
| * |
| * This is currently only used by {@link com.android.server.biometrics.BiometricService}, which |
| * requests sensors to prepare for authentication with a cookie. Once sensor(s) are ready (e.g. |
| * the BiometricService client becomes the current client in the scheduler), the cookie is |
| * returned to BiometricService. Once BiometricService decides that authentication can start, |
| * it invokes this code path. |
| * |
| * @param cookie of the operation to be started |
| */ |
| public void startPreparedClient(int cookie) { |
| if (mCurrentOperation == null) { |
| Slog.e(getTag(), "Current operation is null"); |
| return; |
| } |
| |
| if (mCurrentOperation.startWithCookie(mInternalCallback, cookie)) { |
| Slog.d(getTag(), "[Started] Prepared client: " + mCurrentOperation); |
| } else { |
| Slog.e(getTag(), "[Unable To Start] Prepared client: " + mCurrentOperation); |
| mCurrentOperation = null; |
| startNextOperationIfIdle(); |
| } |
| } |
| |
| /** |
| * Adds a {@link BaseClientMonitor} to the pending queue |
| * |
| * @param clientMonitor operation to be scheduled |
| */ |
| public void scheduleClientMonitor(@NonNull BaseClientMonitor clientMonitor) { |
| scheduleClientMonitor(clientMonitor, null /* clientFinishCallback */); |
| } |
| |
| /** |
| * Adds a {@link BaseClientMonitor} to the pending queue |
| * |
| * @param clientMonitor operation to be scheduled |
| * @param clientCallback optional callback, invoked when the client state changes. |
| */ |
| public void scheduleClientMonitor(@NonNull BaseClientMonitor clientMonitor, |
| @Nullable BaseClientMonitor.Callback clientCallback) { |
| // If the incoming operation should interrupt preceding clients, mark any interruptable |
| // pending clients as canceling. Once they reach the head of the queue, the scheduler will |
| // send ERROR_CANCELED and skip the operation. |
| if (clientMonitor.interruptsPrecedingClients()) { |
| for (BiometricSchedulerOperation operation : mPendingOperations) { |
| if (operation.markCanceling()) { |
| Slog.d(getTag(), "New client, marking pending op as canceling: " + operation); |
| } |
| } |
| } |
| |
| mPendingOperations.add(new BiometricSchedulerOperation(clientMonitor, clientCallback)); |
| Slog.d(getTag(), "[Added] " + clientMonitor |
| + ", new queue size: " + mPendingOperations.size()); |
| |
| // If the new operation should interrupt preceding clients, and if the current operation is |
| // cancellable, start the cancellation process. |
| if (clientMonitor.interruptsPrecedingClients() |
| && mCurrentOperation != null |
| && mCurrentOperation.isInterruptable() |
| && mCurrentOperation.isStarted()) { |
| Slog.d(getTag(), "[Cancelling Interruptable]: " + mCurrentOperation); |
| mCurrentOperation.cancel(mHandler, mInternalCallback); |
| } else { |
| startNextOperationIfIdle(); |
| } |
| } |
| |
| /** |
| * Requests to cancel enrollment. |
| * @param token from the caller, should match the token passed in when requesting enrollment |
| */ |
| public void cancelEnrollment(IBinder token, long requestId) { |
| Slog.d(getTag(), "cancelEnrollment, requestId: " + requestId); |
| |
| if (mCurrentOperation != null |
| && canCancelEnrollOperation(mCurrentOperation, token, requestId)) { |
| Slog.d(getTag(), "Cancelling enrollment op: " + mCurrentOperation); |
| mCurrentOperation.cancel(mHandler, mInternalCallback); |
| } else { |
| for (BiometricSchedulerOperation operation : mPendingOperations) { |
| if (canCancelEnrollOperation(operation, token, requestId)) { |
| Slog.d(getTag(), "Cancelling pending enrollment op: " + operation); |
| operation.markCanceling(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Requests to cancel authentication or detection. |
| * @param token from the caller, should match the token passed in when requesting authentication |
| * @param requestId the id returned when requesting authentication |
| */ |
| public void cancelAuthenticationOrDetection(IBinder token, long requestId) { |
| Slog.d(getTag(), "cancelAuthenticationOrDetection, requestId: " + requestId); |
| |
| if (mCurrentOperation != null |
| && canCancelAuthOperation(mCurrentOperation, token, requestId)) { |
| Slog.d(getTag(), "Cancelling auth/detect op: " + mCurrentOperation); |
| mCurrentOperation.cancel(mHandler, mInternalCallback); |
| } else { |
| for (BiometricSchedulerOperation operation : mPendingOperations) { |
| if (canCancelAuthOperation(operation, token, requestId)) { |
| Slog.d(getTag(), "Cancelling pending auth/detect op: " + operation); |
| operation.markCanceling(); |
| } |
| } |
| } |
| } |
| |
| private static boolean canCancelEnrollOperation(BiometricSchedulerOperation operation, |
| IBinder token, long requestId) { |
| return operation.isEnrollOperation() |
| && operation.isMatchingToken(token) |
| && operation.isMatchingRequestId(requestId); |
| } |
| |
| private static boolean canCancelAuthOperation(BiometricSchedulerOperation operation, |
| IBinder token, long requestId) { |
| // TODO: restrict callers that can cancel without requestId (negative value)? |
| return operation.isAuthenticationOrDetectionOperation() |
| && operation.isMatchingToken(token) |
| && operation.isMatchingRequestId(requestId); |
| } |
| |
| /** |
| * @return the current operation |
| */ |
| public BaseClientMonitor getCurrentClient() { |
| return mCurrentOperation != null ? mCurrentOperation.getClientMonitor() : null; |
| } |
| |
| public int getCurrentPendingCount() { |
| return mPendingOperations.size(); |
| } |
| |
| public void recordCrashState() { |
| if (mCrashStates.size() >= CrashState.NUM_ENTRIES) { |
| mCrashStates.removeFirst(); |
| } |
| final SimpleDateFormat dateFormat = |
| new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US); |
| final String timestamp = dateFormat.format(new Date(System.currentTimeMillis())); |
| final List<String> pendingOperations = new ArrayList<>(); |
| for (BiometricSchedulerOperation operation : mPendingOperations) { |
| pendingOperations.add(operation.toString()); |
| } |
| |
| final CrashState crashState = new CrashState(timestamp, |
| mCurrentOperation != null ? mCurrentOperation.toString() : null, |
| pendingOperations); |
| mCrashStates.add(crashState); |
| Slog.e(getTag(), "Recorded crash state: " + crashState.toString()); |
| } |
| |
| public void dump(PrintWriter pw) { |
| pw.println("Dump of BiometricScheduler " + getTag()); |
| pw.println("Type: " + mSensorType); |
| pw.println("Current operation: " + mCurrentOperation); |
| pw.println("Pending operations: " + mPendingOperations.size()); |
| for (BiometricSchedulerOperation operation : mPendingOperations) { |
| pw.println("Pending operation: " + operation); |
| } |
| for (CrashState crashState : mCrashStates) { |
| pw.println("Crash State " + crashState); |
| } |
| } |
| |
| public byte[] dumpProtoState(boolean clearSchedulerBuffer) { |
| final ProtoOutputStream proto = new ProtoOutputStream(); |
| proto.write(BiometricSchedulerProto.CURRENT_OPERATION, mCurrentOperation != null |
| ? mCurrentOperation.getProtoEnum() : BiometricsProto.CM_NONE); |
| proto.write(BiometricSchedulerProto.TOTAL_OPERATIONS, mTotalOperationsHandled); |
| |
| if (!mRecentOperations.isEmpty()) { |
| for (int i = 0; i < mRecentOperations.size(); i++) { |
| proto.write(BiometricSchedulerProto.RECENT_OPERATIONS, mRecentOperations.get(i)); |
| } |
| } else { |
| // TODO:(b/178828362) Unsure why protobuf has a problem decoding when an empty list |
| // is returned. So, let's just add a no-op for this case. |
| proto.write(BiometricSchedulerProto.RECENT_OPERATIONS, BiometricsProto.CM_NONE); |
| } |
| proto.flush(); |
| |
| if (clearSchedulerBuffer) { |
| mRecentOperations.clear(); |
| } |
| return proto.getBytes(); |
| } |
| |
| /** |
| * Clears the scheduler of anything work-related. This should be used for example when the |
| * HAL dies. |
| */ |
| public void reset() { |
| Slog.d(getTag(), "Resetting scheduler"); |
| mPendingOperations.clear(); |
| mCurrentOperation = null; |
| } |
| } |