blob: e8b50d90b5865e6795a7facd54d8cb8e7ce74ba1 [file] [log] [blame]
/*
* Copyright (C) 2021 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.NonNull;
import android.annotation.Nullable;
import android.hardware.biometrics.BiometricConstants;
import android.os.Handler;
import android.os.IBinder;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Contains all the necessary information for a HAL operation.
*/
public class BiometricSchedulerOperation {
protected static final String TAG = "BiometricSchedulerOperation";
/**
* The operation is added to the list of pending operations and waiting for its turn.
*/
protected static final int STATE_WAITING_IN_QUEUE = 0;
/**
* The operation is added to the list of pending operations, but a subsequent operation
* has been added. This state only applies to {@link Interruptable} operations. When this
* operation reaches the head of the queue, it will send ERROR_CANCELED and finish.
*/
protected static final int STATE_WAITING_IN_QUEUE_CANCELING = 1;
/**
* The operation has reached the front of the queue and has started.
*/
protected static final int STATE_STARTED = 2;
/**
* The operation was started, but is now canceling. Operations should wait for the HAL to
* acknowledge that the operation was canceled, at which point it finishes.
*/
protected static final int STATE_STARTED_CANCELING = 3;
/**
* The operation has reached the head of the queue but is waiting for BiometricService
* to acknowledge and start the operation.
*/
protected static final int STATE_WAITING_FOR_COOKIE = 4;
/**
* The {@link BaseClientMonitor.Callback} has been invoked and the client is finished.
*/
protected static final int STATE_FINISHED = 5;
@IntDef({STATE_WAITING_IN_QUEUE,
STATE_WAITING_IN_QUEUE_CANCELING,
STATE_STARTED,
STATE_STARTED_CANCELING,
STATE_WAITING_FOR_COOKIE,
STATE_FINISHED})
@Retention(RetentionPolicy.SOURCE)
protected @interface OperationState {}
private static final int CANCEL_WATCHDOG_DELAY_MS = 3000;
@NonNull
private final BaseClientMonitor mClientMonitor;
@Nullable
private final BaseClientMonitor.Callback mClientCallback;
@OperationState
private int mState;
@VisibleForTesting
@NonNull
final Runnable mCancelWatchdog;
BiometricSchedulerOperation(
@NonNull BaseClientMonitor clientMonitor,
@Nullable BaseClientMonitor.Callback callback
) {
this(clientMonitor, callback, STATE_WAITING_IN_QUEUE);
}
protected BiometricSchedulerOperation(
@NonNull BaseClientMonitor clientMonitor,
@Nullable BaseClientMonitor.Callback callback,
@OperationState int state
) {
mClientMonitor = clientMonitor;
mClientCallback = callback;
mState = state;
mCancelWatchdog = () -> {
if (!isFinished()) {
Slog.e(TAG, "[Watchdog Triggered]: " + this);
getWrappedCallback().onClientFinished(mClientMonitor, false /* success */);
}
};
}
/**
* Zero if this operation is ready to start or has already started. A non-zero cookie
* is returned if the operation has not started and is waiting on
* {@link android.hardware.biometrics.IBiometricService#onReadyForAuthentication(int)}.
*
* @return cookie or 0 if ready/started
*/
public int isReadyToStart() {
if (mState == STATE_WAITING_FOR_COOKIE || mState == STATE_WAITING_IN_QUEUE) {
final int cookie = mClientMonitor.getCookie();
if (cookie != 0) {
mState = STATE_WAITING_FOR_COOKIE;
}
return cookie;
}
return 0;
}
/**
* Start this operation without waiting for a cookie
* (i.e. {@link #isReadyToStart() returns zero}
*
* @param callback lifecycle callback
* @return if this operation started
*/
public boolean start(@NonNull BaseClientMonitor.Callback callback) {
checkInState("start",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING);
if (mClientMonitor.getCookie() != 0) {
throw new IllegalStateException("operation requires cookie");
}
return doStart(callback);
}
/**
* Start this operation after receiving the given cookie.
*
* @param callback lifecycle callback
* @param cookie cookie indicting the operation should begin
* @return if this operation started
*/
public boolean startWithCookie(@NonNull BaseClientMonitor.Callback callback, int cookie) {
checkInState("start",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING);
if (mClientMonitor.getCookie() != cookie) {
Slog.e(TAG, "Mismatched cookie for operation: " + this + ", received: " + cookie);
return false;
}
return doStart(callback);
}
private boolean doStart(@NonNull BaseClientMonitor.Callback callback) {
final BaseClientMonitor.Callback cb = getWrappedCallback(callback);
if (mState == STATE_WAITING_IN_QUEUE_CANCELING) {
Slog.d(TAG, "Operation marked for cancellation, cancelling now: " + this);
cb.onClientFinished(mClientMonitor, true /* success */);
if (mClientMonitor instanceof ErrorConsumer) {
final ErrorConsumer errorConsumer = (ErrorConsumer) mClientMonitor;
errorConsumer.onError(BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */);
} else {
Slog.w(TAG, "monitor cancelled but does not implement ErrorConsumer");
}
return false;
}
if (isUnstartableHalOperation()) {
Slog.v(TAG, "unable to start: " + this);
((HalClientMonitor<?>) mClientMonitor).unableToStart();
cb.onClientFinished(mClientMonitor, false /* success */);
return false;
}
mState = STATE_STARTED;
mClientMonitor.start(cb);
Slog.v(TAG, "started: " + this);
return true;
}
/**
* Abort a pending operation.
*
* This is similar to cancel but the operation must not have been started. It will
* immediately abort the operation and notify the client that it has finished unsuccessfully.
*/
public void abort() {
checkInState("cannot abort a non-pending operation",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING);
if (isHalOperation()) {
((HalClientMonitor<?>) mClientMonitor).unableToStart();
}
getWrappedCallback().onClientFinished(mClientMonitor, false /* success */);
Slog.v(TAG, "Aborted: " + this);
}
/** Flags this operation as canceled, if possible, but does not cancel it until started. */
public boolean markCanceling() {
if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) {
mState = STATE_WAITING_IN_QUEUE_CANCELING;
return true;
}
return false;
}
/**
* Cancel the operation now.
*
* @param handler handler to use for the cancellation watchdog
* @param callback lifecycle callback (only used if this operation hasn't started, otherwise
* the callback used from {@link #start(BaseClientMonitor.Callback)} is used)
*/
public void cancel(@NonNull Handler handler, @NonNull BaseClientMonitor.Callback callback) {
checkNotInState("cancel", STATE_FINISHED);
final int currentState = mState;
if (!isInterruptable()) {
Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this);
return;
}
if (currentState == STATE_STARTED_CANCELING) {
Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this);
return;
}
mState = STATE_STARTED_CANCELING;
if (currentState == STATE_WAITING_IN_QUEUE
|| currentState == STATE_WAITING_IN_QUEUE_CANCELING
|| currentState == STATE_WAITING_FOR_COOKIE) {
Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor);
((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback));
} else {
Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor);
((Interruptable) mClientMonitor).cancel();
}
// forcibly finish this client if the HAL does not acknowledge within the timeout
handler.postDelayed(mCancelWatchdog, CANCEL_WATCHDOG_DELAY_MS);
}
@NonNull
private BaseClientMonitor.Callback getWrappedCallback() {
return getWrappedCallback(null);
}
@NonNull
private BaseClientMonitor.Callback getWrappedCallback(
@Nullable BaseClientMonitor.Callback callback) {
final BaseClientMonitor.Callback destroyCallback = new BaseClientMonitor.Callback() {
@Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
Slog.d(TAG, "[Finished / destroy]: " + clientMonitor);
mClientMonitor.destroy();
mState = STATE_FINISHED;
}
};
return new BaseClientMonitor.CompositeCallback(destroyCallback, callback, mClientCallback);
}
/** {@link BaseClientMonitor#getSensorId()}. */
public int getSensorId() {
return mClientMonitor.getSensorId();
}
/** {@link BaseClientMonitor#getProtoEnum()}. */
public int getProtoEnum() {
return mClientMonitor.getProtoEnum();
}
/** {@link BaseClientMonitor#getTargetUserId()}. */
public int getTargetUserId() {
return mClientMonitor.getTargetUserId();
}
/** If the given clientMonitor is the same as the one in the constructor. */
public boolean isFor(@NonNull BaseClientMonitor clientMonitor) {
return mClientMonitor == clientMonitor;
}
/** If this operation is {@link Interruptable}. */
public boolean isInterruptable() {
return mClientMonitor instanceof Interruptable;
}
private boolean isHalOperation() {
return mClientMonitor instanceof HalClientMonitor<?>;
}
private boolean isUnstartableHalOperation() {
if (isHalOperation()) {
final HalClientMonitor<?> client = (HalClientMonitor<?>) mClientMonitor;
if (client.getFreshDaemon() == null) {
return true;
}
}
return false;
}
/** If this operation is an enrollment. */
public boolean isEnrollOperation() {
return mClientMonitor instanceof EnrollClient;
}
/** If this operation is authentication. */
public boolean isAuthenticateOperation() {
return mClientMonitor instanceof AuthenticationClient;
}
/** If this operation is authentication or detection. */
public boolean isAuthenticationOrDetectionOperation() {
final boolean isAuthentication = mClientMonitor instanceof AuthenticationConsumer;
final boolean isDetection = mClientMonitor instanceof DetectionConsumer;
return isAuthentication || isDetection;
}
/** If this operation performs acquisition {@link AcquisitionClient}. */
public boolean isAcquisitionOperation() {
return mClientMonitor instanceof AcquisitionClient;
}
/**
* If this operation matches the original requestId.
*
* By default, monitors are not associated with a request id to retain the original
* behavior (i.e. if no requestId is explicitly set then assume it matches)
*
* @param requestId a unique id {@link BaseClientMonitor#setRequestId(long)}.
*/
public boolean isMatchingRequestId(long requestId) {
return !mClientMonitor.hasRequestId()
|| mClientMonitor.getRequestId() == requestId;
}
/** If the token matches */
public boolean isMatchingToken(@Nullable IBinder token) {
return mClientMonitor.getToken() == token;
}
/** If this operation has started. */
public boolean isStarted() {
return mState == STATE_STARTED;
}
/** If this operation is cancelling but has not yet completed. */
public boolean isCanceling() {
return mState == STATE_STARTED_CANCELING;
}
/** If this operation has finished and completed its lifecycle. */
public boolean isFinished() {
return mState == STATE_FINISHED;
}
/** If {@link #markCanceling()} was called but the operation hasn't been canceled. */
public boolean isMarkedCanceling() {
return mState == STATE_WAITING_IN_QUEUE_CANCELING;
}
/**
* The monitor passed to the constructor.
* @deprecated avoid using and move to encapsulate within the operation
*/
@Deprecated
public BaseClientMonitor getClientMonitor() {
return mClientMonitor;
}
private void checkNotInState(String message, @OperationState int... states) {
for (int state : states) {
if (mState == state) {
throw new IllegalStateException(message + ": illegal state= " + state);
}
}
}
private void checkInState(String message, @OperationState int... states) {
for (int state : states) {
if (mState == state) {
return;
}
}
throw new IllegalStateException(message + ": illegal state= " + mState);
}
@Override
public String toString() {
return mClientMonitor + ", State: " + mState;
}
}