Merge "Removed Coexcoordinator."

This commit is contained in:
Joshua Mccloskey
2022-08-30 03:22:33 +00:00
committed by Android (Google) Code Review
12 changed files with 76 additions and 1308 deletions

View File

@@ -72,7 +72,6 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.DumpUtils;
import com.android.server.SystemService;
import com.android.server.biometrics.sensors.CoexCoordinator;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -951,16 +950,6 @@ public class BiometricService extends SystemService {
return new ArrayList<>();
}
public boolean isAdvancedCoexLogicEnabled(Context context) {
return Settings.Secure.getInt(context.getContentResolver(),
CoexCoordinator.SETTING_ENABLE_NAME, 1) != 0;
}
public boolean isCoexFaceNonBypassHapticsDisabled(Context context) {
return Settings.Secure.getInt(context.getContentResolver(),
CoexCoordinator.FACE_HAPTIC_DISABLE, 0) != 0;
}
public Supplier<Long> getRequestGenerator() {
final AtomicLong generator = new AtomicLong(0);
return () -> generator.incrementAndGet();
@@ -992,14 +981,6 @@ public class BiometricService extends SystemService {
mEnabledOnKeyguardCallbacks);
mRequestCounter = mInjector.getRequestGenerator();
// TODO(b/193089985) This logic lives here (outside of CoexCoordinator) so that it doesn't
// need to depend on context. We can remove this code once the advanced logic is enabled
// by default.
CoexCoordinator coexCoordinator = CoexCoordinator.getInstance();
coexCoordinator.setAdvancedLogicEnabled(injector.isAdvancedCoexLogicEnabled(context));
coexCoordinator.setFaceHapticDisabledWhenNonBypass(
injector.isCoexFaceNonBypassHapticsDisabled(context));
try {
injector.getActivityManagerService().registerUserSwitchObserver(
new UserSwitchObserver() {
@@ -1333,7 +1314,5 @@ public class BiometricService extends SystemService {
pw.println();
pw.println("CurrentSession: " + mAuthSession);
pw.println();
pw.println("CoexCoordinator: " + CoexCoordinator.getInstance().toString());
pw.println();
}
}

View File

@@ -29,7 +29,6 @@ import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricOverlayConstants;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.security.KeyStore;
import android.util.EventLog;
import android.util.Slog;
@@ -46,9 +45,7 @@ import java.util.function.Supplier;
* A class to keep track of the authentication state for a given client.
*/
public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
implements AuthenticationConsumer {
private static final String TAG = "Biometrics/AuthenticationClient";
implements AuthenticationConsumer {
// New, has not started yet
public static final int STATE_NEW = 0;
@@ -67,28 +64,27 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
STATE_STARTED_PAUSED_ATTEMPTED,
STATE_STOPPED})
@interface State {}
private static final String TAG = "Biometrics/AuthenticationClient";
protected final long mOperationId;
private final boolean mIsStrongBiometric;
private final boolean mRequireConfirmation;
private final ActivityTaskManager mActivityTaskManager;
private final BiometricManager mBiometricManager;
@Nullable private final TaskStackListener mTaskStackListener;
@Nullable
private final TaskStackListener mTaskStackListener;
private final LockoutTracker mLockoutTracker;
private final boolean mIsRestricted;
private final boolean mAllowBackgroundAuthentication;
private final boolean mIsKeyguardBypassEnabled;
protected final long mOperationId;
// TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update
// the state. We should think of a way to improve this in the future.
@State
protected int mState = STATE_NEW;
private long mStartTimeMs;
private boolean mAuthAttempted;
private boolean mAuthSuccess = false;
// TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update
// the state. We should think of a way to improve this in the future.
protected @State int mState = STATE_NEW;
public AuthenticationClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
@NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener,
int targetUserId, long operationId, boolean restricted, @NonNull String owner,
@@ -111,8 +107,9 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
mIsKeyguardBypassEnabled = isKeyguardBypassEnabled;
}
public @LockoutTracker.LockoutMode int handleFailedAttempt(int userId) {
final @LockoutTracker.LockoutMode int lockoutMode =
@LockoutTracker.LockoutMode
public int handleFailedAttempt(int userId) {
@LockoutTracker.LockoutMode final int lockoutMode =
mLockoutTracker.getLockoutModeForUser(userId);
final PerformanceTracker performanceTracker =
PerformanceTracker.getInstanceForSensorId(getSensorId());
@@ -173,14 +170,16 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
final ClientMonitorCallbackConverter listener = getListener();
if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")"
+ ", ID:" + identifier.getBiometricId()
+ ", Owner: " + getOwnerString()
+ ", isBP: " + isBiometricPrompt()
+ ", listener: " + listener
+ ", requireConfirmation: " + mRequireConfirmation
+ ", user: " + getTargetUserId()
+ ", clientMonitor: " + toString());
if (DEBUG) {
Slog.v(TAG, "onAuthenticated(" + authenticated + ")"
+ ", ID:" + identifier.getBiometricId()
+ ", Owner: " + getOwnerString()
+ ", isBP: " + isBiometricPrompt()
+ ", listener: " + listener
+ ", requireConfirmation: " + mRequireConfirmation
+ ", user: " + getTargetUserId()
+ ", clientMonitor: " + this);
}
final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());
if (isCryptoOperation()) {
@@ -239,142 +238,57 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
getSensorId(), getTargetUserId(), byteToken);
}
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
coordinator.onAuthenticationSucceeded(SystemClock.uptimeMillis(), this,
new CoexCoordinator.Callback() {
@Override
public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
if (addAuthTokenIfStrong && mIsStrongBiometric) {
final int result = KeyStore.getInstance().addAuthToken(byteToken);
Slog.d(TAG, "addAuthToken: " + result);
// For BP, BiometricService will add the authToken to Keystore.
if (!isBiometricPrompt() && mIsStrongBiometric) {
final int result = KeyStore.getInstance().addAuthToken(byteToken);
if (result != KeyStore.NO_ERROR) {
Slog.d(TAG, "Error adding auth token : " + result);
} else {
Slog.d(TAG, "addAuthToken: " + result);
}
} else {
Slog.d(TAG, "Skipping addAuthToken");
}
try {
if (listener != null) {
if (!mIsRestricted) {
listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
getTargetUserId(), mIsStrongBiometric);
} else {
Slog.d(TAG, "Skipping addAuthToken");
}
if (listener != null) {
try {
// Explicitly have if/else here to make it super obvious in case the
// code is touched in the future.
if (!mIsRestricted) {
listener.onAuthenticationSucceeded(getSensorId(),
identifier,
byteToken,
getTargetUserId(),
mIsStrongBiometric);
} else {
listener.onAuthenticationSucceeded(getSensorId(),
null /* identifier */,
byteToken,
getTargetUserId(),
mIsStrongBiometric);
}
} catch (RemoteException e) {
Slog.e(TAG, "Unable to notify listener", e);
}
} else {
Slog.w(TAG, "Client not listening");
listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
byteToken,
getTargetUserId(), mIsStrongBiometric);
}
} else {
Slog.e(TAG, "Received successful auth, but client was not listening");
}
@Override
public void sendHapticFeedback() {
if (listener != null && mShouldVibrate) {
vibrateSuccess();
}
}
@Override
public void handleLifecycleAfterAuth() {
AuthenticationClient.this.handleLifecycleAfterAuth(true /* authenticated */);
}
@Override
public void sendAuthenticationCanceled() {
sendCancelOnly(listener);
}
});
} else { // not authenticated
} catch (RemoteException e) {
Slog.e(TAG, "Unable to notify listener", e);
mCallback.onClientFinished(this, false);
return;
}
} else {
if (isBackgroundAuth) {
Slog.e(TAG, "cancelling due to background auth");
cancel();
} else {
// Allow system-defined limit of number of attempts before giving up
final @LockoutTracker.LockoutMode int lockoutMode =
@LockoutTracker.LockoutMode final int lockoutMode =
handleFailedAttempt(getTargetUserId());
if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
markAlreadyDone();
}
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
coordinator.onAuthenticationRejected(SystemClock.uptimeMillis(), this, lockoutMode,
new CoexCoordinator.Callback() {
@Override
public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
if (listener != null) {
try {
listener.onAuthenticationFailed(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Unable to notify listener", e);
}
}
}
@Override
public void sendHapticFeedback() {
if (listener != null && mShouldVibrate) {
vibrateError();
}
}
@Override
public void handleLifecycleAfterAuth() {
AuthenticationClient.this.handleLifecycleAfterAuth(false /* authenticated */);
}
@Override
public void sendAuthenticationCanceled() {
sendCancelOnly(listener);
}
});
try {
listener.onAuthenticationFailed(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Unable to notify listener", e);
mCallback.onClientFinished(this, false);
return;
}
}
}
}
/**
* Only call this method on interfaces where lockout does not come from onError, I.E. the
* old HIDL implementation.
*/
protected void onLockoutTimed(long durationMillis) {
final ClientMonitorCallbackConverter listener = getListener();
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
coordinator.onAuthenticationError(this, BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
new CoexCoordinator.ErrorCallback() {
@Override
public void sendHapticFeedback() {
if (listener != null && mShouldVibrate) {
vibrateError();
}
}
});
}
/**
* Only call this method on interfaces where lockout does not come from onError, I.E. the
* old HIDL implementation.
*/
protected void onLockoutPermanent() {
final ClientMonitorCallbackConverter listener = getListener();
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
coordinator.onAuthenticationError(this,
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
new CoexCoordinator.ErrorCallback() {
@Override
public void sendHapticFeedback() {
if (listener != null && mShouldVibrate) {
vibrateError();
}
}
});
AuthenticationClient.this.handleLifecycleAfterAuth(authenticated);
}
private void sendCancelOnly(@Nullable ClientMonitorCallbackConverter listener) {
@@ -396,7 +310,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
public void onAcquired(int acquiredInfo, int vendorCode) {
super.onAcquired(acquiredInfo, vendorCode);
final @LockoutTracker.LockoutMode int lockoutMode =
@LockoutTracker.LockoutMode final int lockoutMode =
mLockoutTracker.getLockoutModeForUser(getTargetUserId());
if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
@@ -408,8 +322,6 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
public void onError(@BiometricConstants.Errors int errorCode, int vendorCode) {
super.onError(errorCode, vendorCode);
mState = STATE_STOPPED;
CoexCoordinator.getInstance().onAuthenticationError(this, errorCode, this::vibrateError);
}
/**
@@ -419,7 +331,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
public void start(@NonNull ClientMonitorCallback callback) {
super.start(callback);
final @LockoutTracker.LockoutMode int lockoutMode =
@LockoutTracker.LockoutMode final int lockoutMode =
mLockoutTracker.getLockoutModeForUser(getTargetUserId());
if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
Slog.v(TAG, "In lockout mode(" + lockoutMode + ") ; disallowing authentication");
@@ -450,22 +362,20 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
}
/**
* Handles lifecycle, e.g. {@link BiometricScheduler},
* {@link com.android.server.biometrics.sensors.BaseClientMonitor.Callback} after authentication
* results are known. Note that this happens asynchronously from (but shortly after)
* {@link #onAuthenticated(BiometricAuthenticator.Identifier, boolean, ArrayList)} and allows
* {@link CoexCoordinator} a chance to invoke/delay this event.
* @param authenticated
* Handles lifecycle, e.g. {@link BiometricScheduler} after authentication. This is necessary
* as different clients handle the lifecycle of authentication success/reject differently. I.E.
* Fingerprint does not finish authentication when it is rejected.
*/
protected abstract void handleLifecycleAfterAuth(boolean authenticated);
/**
* @return true if a user was detected (i.e. face was found, fingerprint sensor was touched.
* etc)
* etc)
*/
public abstract boolean wasUserDetected();
public @State int getState() {
@State
public int getState() {
return mState;
}

View File

@@ -54,7 +54,7 @@ import java.util.function.Consumer;
* 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}.
* {@link BiometricScheduler}.
*/
@MainThread
public class BiometricScheduler {
@@ -156,7 +156,6 @@ public class BiometricScheduler {
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
@@ -165,11 +164,6 @@ public class BiometricScheduler {
@Override
public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
Slog.d(getTag(), "[Started] " + clientMonitor);
if (clientMonitor instanceof AuthenticationClient) {
mCoexCoordinator.addAuthenticationClient(mSensorType,
(AuthenticationClient<?>) clientMonitor);
}
}
@Override
@@ -189,10 +183,6 @@ public class BiometricScheduler {
}
Slog.d(getTag(), "[Finishing] " + clientMonitor + ", success: " + success);
if (clientMonitor instanceof AuthenticationClient) {
mCoexCoordinator.removeAuthenticationClient(mSensorType,
(AuthenticationClient<?>) clientMonitor);
}
if (mGestureAvailabilityDispatcher != null) {
mGestureAvailabilityDispatcher.markSensorActive(
@@ -216,8 +206,7 @@ public class BiometricScheduler {
@SensorType int sensorType,
@Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
@NonNull IBiometricService biometricService,
int recentOperationsLimit,
@NonNull CoexCoordinator coexCoordinator) {
int recentOperationsLimit) {
mBiometricTag = tag;
mHandler = handler;
mSensorType = sensorType;
@@ -227,7 +216,6 @@ public class BiometricScheduler {
mCrashStates = new ArrayDeque<>();
mRecentOperationsLimit = recentOperationsLimit;
mRecentOperations = new ArrayList<>();
mCoexCoordinator = coexCoordinator;
}
/**
@@ -244,7 +232,7 @@ public class BiometricScheduler {
this(tag, new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher,
IBiometricService.Stub.asInterface(
ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
LOG_NUM_RECENT_OPERATIONS, CoexCoordinator.getInstance());
LOG_NUM_RECENT_OPERATIONS);
}
@VisibleForTesting

View File

@@ -1,525 +0,0 @@
/*
* 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 static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_FACE;
import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_UDFPS;
import static com.android.server.biometrics.sensors.BiometricScheduler.sensorTypeToString;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.biometrics.BiometricConstants;
import android.os.Handler;
import android.os.Looper;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.sensors.BiometricScheduler.SensorType;
import com.android.server.biometrics.sensors.fingerprint.Udfps;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* Singleton that contains the core logic for determining if haptics and authentication callbacks
* should be sent to receivers. Note that this class is used even when coex is not required (e.g.
* single sensor devices, or multi-sensor devices where only a single sensor is authenticating).
* This allows us to have all business logic in one testable place.
*/
public class CoexCoordinator {
private static final String TAG = "BiometricCoexCoordinator";
public static final String SETTING_ENABLE_NAME =
"com.android.server.biometrics.sensors.CoexCoordinator.enable";
public static final String FACE_HAPTIC_DISABLE =
"com.android.server.biometrics.sensors.CoexCoordinator.disable_face_haptics";
private static final boolean DEBUG = true;
// Successful authentications should be used within this amount of time.
static final long SUCCESSFUL_AUTH_VALID_DURATION_MS = 5000;
/**
* Callback interface notifying the owner of "results" from the CoexCoordinator's business
* logic for accept and reject.
*/
interface Callback {
/**
* Requests the owner to send the result (success/reject) and any associated info to the
* receiver (e.g. keyguard, BiometricService, etc).
*/
void sendAuthenticationResult(boolean addAuthTokenIfStrong);
/**
* Requests the owner to initiate a vibration for this event.
*/
void sendHapticFeedback();
/**
* Requests the owner to handle the AuthenticationClient's lifecycle (e.g. finish and remove
* from scheduler if auth was successful).
*/
void handleLifecycleAfterAuth();
/**
* Requests the owner to notify the caller that authentication was canceled.
*/
void sendAuthenticationCanceled();
}
/**
* Callback interface notifying the owner of "results" from the CoexCoordinator's business
* logic for errors.
*/
interface ErrorCallback {
/**
* Requests the owner to initiate a vibration for this event.
*/
void sendHapticFeedback();
}
private static final CoexCoordinator sInstance = new CoexCoordinator();
@VisibleForTesting
public static class SuccessfulAuth {
final long mAuthTimestamp;
final @SensorType int mSensorType;
final AuthenticationClient<?> mAuthenticationClient;
final Callback mCallback;
final CleanupRunnable mCleanupRunnable;
public static class CleanupRunnable implements Runnable {
@NonNull final LinkedList<SuccessfulAuth> mSuccessfulAuths;
@NonNull final SuccessfulAuth mAuth;
@NonNull final Callback mCallback;
public CleanupRunnable(@NonNull LinkedList<SuccessfulAuth> successfulAuths,
@NonNull SuccessfulAuth auth, @NonNull Callback callback) {
mSuccessfulAuths = successfulAuths;
mAuth = auth;
mCallback = callback;
}
@Override
public void run() {
final boolean removed = mSuccessfulAuths.remove(mAuth);
Slog.w(TAG, "Removing stale successfulAuth: " + mAuth.toString()
+ ", success: " + removed);
mCallback.handleLifecycleAfterAuth();
}
}
public SuccessfulAuth(@NonNull Handler handler,
@NonNull LinkedList<SuccessfulAuth> successfulAuths,
long currentTimeMillis,
@SensorType int sensorType,
@NonNull AuthenticationClient<?> authenticationClient,
@NonNull Callback callback) {
mAuthTimestamp = currentTimeMillis;
mSensorType = sensorType;
mAuthenticationClient = authenticationClient;
mCallback = callback;
mCleanupRunnable = new CleanupRunnable(successfulAuths, this, callback);
handler.postDelayed(mCleanupRunnable, SUCCESSFUL_AUTH_VALID_DURATION_MS);
}
@Override
public String toString() {
return "SensorType: " + sensorTypeToString(mSensorType)
+ ", mAuthTimestamp: " + mAuthTimestamp
+ ", authenticationClient: " + mAuthenticationClient;
}
}
/** The singleton instance. */
@NonNull
public static CoexCoordinator getInstance() {
return sInstance;
}
@VisibleForTesting
public void setAdvancedLogicEnabled(boolean enabled) {
mAdvancedLogicEnabled = enabled;
}
public void setFaceHapticDisabledWhenNonBypass(boolean disabled) {
mFaceHapticDisabledWhenNonBypass = disabled;
}
@VisibleForTesting
void reset() {
mClientMap.clear();
}
// SensorType to AuthenticationClient map
private final Map<Integer, AuthenticationClient<?>> mClientMap = new HashMap<>();
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths = new LinkedList<>();
private boolean mAdvancedLogicEnabled;
private boolean mFaceHapticDisabledWhenNonBypass;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private CoexCoordinator() {}
public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType,
@NonNull AuthenticationClient<?> client) {
if (DEBUG) {
Slog.d(TAG, "addAuthenticationClient(" + sensorTypeToString(sensorType) + ")"
+ ", client: " + client);
}
if (mClientMap.containsKey(sensorType)) {
Slog.w(TAG, "Overwriting existing client: " + mClientMap.get(sensorType)
+ " with new client: " + client);
}
mClientMap.put(sensorType, client);
}
public void removeAuthenticationClient(@BiometricScheduler.SensorType int sensorType,
@NonNull AuthenticationClient<?> client) {
if (DEBUG) {
Slog.d(TAG, "removeAuthenticationClient(" + sensorTypeToString(sensorType) + ")"
+ ", client: " + client);
}
if (!mClientMap.containsKey(sensorType)) {
Slog.e(TAG, "sensorType: " + sensorType + " does not exist in map. Client: " + client);
return;
}
mClientMap.remove(sensorType);
}
/**
* Notify the coordinator that authentication succeeded (accepted)
*/
public void onAuthenticationSucceeded(long currentTimeMillis,
@NonNull AuthenticationClient<?> client,
@NonNull Callback callback) {
final boolean isUsingSingleModality = isSingleAuthOnly(client);
if (client.isBiometricPrompt()) {
if (!isUsingSingleModality && hasMultipleSuccessfulAuthentications()) {
// only send feedback on the first one
} else {
callback.sendHapticFeedback();
}
// For BP, BiometricService will add the authToken to Keystore.
callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
} else if (isUnknownClient(client)) {
// Client doesn't exist in our map for some reason. Give the user feedback so the
// device doesn't feel like it's stuck. All other cases below can assume that the
// client exists in our map.
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
} else if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isUsingSingleModality) {
// Single sensor authentication
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
} else {
// Multi sensor authentication
AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null);
AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
if (isCurrentFaceAuth(client)) {
if (isUdfpsActivelyAuthing(udfps)) {
// Face auth success while UDFPS is actively authing. No callback, no haptic
// Feedback will be provided after UDFPS result:
// 1) UDFPS succeeds - simply remove this from the queue
// 2) UDFPS rejected - use this face auth success to notify clients
mSuccessfulAuths.add(new SuccessfulAuth(mHandler, mSuccessfulAuths,
currentTimeMillis, SENSOR_TYPE_FACE, client, callback));
} else {
if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) {
Slog.w(TAG, "Skipping face success haptic");
} else {
callback.sendHapticFeedback();
}
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
}
} else if (isCurrentUdfps(client)) {
if (isFaceScanning()) {
// UDFPS succeeds while face is still scanning
// Cancel face auth and/or prevent it from invoking haptics/callbacks after
face.cancel();
}
removeAndFinishAllFaceFromQueue();
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
} else {
// Capacitive fingerprint sensor (or other)
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
}
}
} else {
// Non-keyguard authentication. For example, Fingerprint Settings use of
// FingerprintManager for highlighting fingers
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
}
}
/**
* Notify the coordinator that a rejection has occurred.
*/
public void onAuthenticationRejected(long currentTimeMillis,
@NonNull AuthenticationClient<?> client,
@LockoutTracker.LockoutMode int lockoutMode,
@NonNull Callback callback) {
final boolean isUsingSingleModality = isSingleAuthOnly(client);
if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isUsingSingleModality) {
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
} else {
// Multi sensor authentication
AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null);
AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
if (isCurrentFaceAuth(client)) {
if (isUdfpsActivelyAuthing(udfps)) {
// UDFPS should still be running in this case, do not vibrate. However, we
// should notify the callback and finish the client, so that Keyguard and
// BiometricScheduler do not get stuck.
Slog.d(TAG, "Face rejected in multi-sensor auth, udfps: " + udfps);
callback.handleLifecycleAfterAuth();
} else if (isUdfpsAuthAttempted(udfps)) {
// If UDFPS is STATE_STARTED_PAUSED (e.g. finger rejected but can still
// auth after pointer goes down, it means UDFPS encountered a rejection. In
// this case, we need to play the final reject haptic since face auth is
// also done now.
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
} else {
// UDFPS auth has never been attempted.
if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) {
Slog.w(TAG, "Skipping face reject haptic");
} else {
callback.sendHapticFeedback();
}
callback.handleLifecycleAfterAuth();
}
} else if (isCurrentUdfps(client)) {
// Face should either be running, or have already finished
SuccessfulAuth auth = popSuccessfulFaceAuthIfExists(currentTimeMillis);
if (auth != null) {
Slog.d(TAG, "Using recent auth: " + auth);
callback.handleLifecycleAfterAuth();
auth.mCallback.sendHapticFeedback();
auth.mCallback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
auth.mCallback.handleLifecycleAfterAuth();
} else {
Slog.d(TAG, "UDFPS rejected in multi-sensor auth");
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
}
} else {
Slog.d(TAG, "Unknown client rejected: " + client);
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
}
}
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
if (!isCurrentFaceAuth(client)) {
callback.sendHapticFeedback();
}
callback.handleLifecycleAfterAuth();
} else {
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
}
// Always notify keyguard, otherwise the cached "running" state in KeyguardUpdateMonitor
// will get stuck.
if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
// Don't send onAuthenticationFailed if we're in lockout, it causes a
// janky UI on Keyguard/BiometricPrompt since "authentication failed"
// will show briefly and be replaced by "device locked out" message.
callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
}
}
/**
* Notify the coordinator that an error has occurred.
*/
public void onAuthenticationError(@NonNull AuthenticationClient<?> client,
@BiometricConstants.Errors int error, @NonNull ErrorCallback callback) {
final boolean isUsingSingleModality = isSingleAuthOnly(client);
// Figure out non-coex state
final boolean shouldUsuallyVibrate;
if (isCurrentFaceAuth(client)) {
final boolean notDetectedOnKeyguard = client.isKeyguard() && !client.wasUserDetected();
final boolean authAttempted = client.wasAuthAttempted();
switch (error) {
case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
shouldUsuallyVibrate = authAttempted && !notDetectedOnKeyguard;
break;
default:
shouldUsuallyVibrate = false;
break;
}
} else {
shouldUsuallyVibrate = false;
}
// Figure out coex state
final boolean hapticSuppressedByCoex;
if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isUsingSingleModality) {
hapticSuppressedByCoex = false;
} else {
hapticSuppressedByCoex = isCurrentFaceAuth(client)
&& !client.isKeyguardBypassEnabled();
}
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
hapticSuppressedByCoex = isCurrentFaceAuth(client);
} else {
hapticSuppressedByCoex = false;
}
// Combine and send feedback if appropriate
if (shouldUsuallyVibrate && !hapticSuppressedByCoex) {
callback.sendHapticFeedback();
} else {
Slog.v(TAG, "no haptic shouldUsuallyVibrate: " + shouldUsuallyVibrate
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
}
}
@Nullable
private SuccessfulAuth popSuccessfulFaceAuthIfExists(long currentTimeMillis) {
for (SuccessfulAuth auth : mSuccessfulAuths) {
if (currentTimeMillis - auth.mAuthTimestamp >= SUCCESSFUL_AUTH_VALID_DURATION_MS) {
// TODO(b/193089985): This removes the auth but does not notify the client with
// an appropriate lifecycle event (such as ERROR_CANCELED), and violates the
// API contract. However, this might be OK for now since the validity duration
// is way longer than the time it takes to auth with fingerprint.
Slog.e(TAG, "Removing stale auth: " + auth);
mSuccessfulAuths.remove(auth);
} else if (auth.mSensorType == SENSOR_TYPE_FACE) {
mSuccessfulAuths.remove(auth);
return auth;
}
}
return null;
}
private void removeAndFinishAllFaceFromQueue() {
// Note that these auth are all successful, but have never notified the client (e.g.
// keyguard). To comply with the authentication lifecycle, we must notify the client that
// auth is "done". The safest thing to do is to send ERROR_CANCELED.
for (SuccessfulAuth auth : mSuccessfulAuths) {
if (auth.mSensorType == SENSOR_TYPE_FACE) {
Slog.d(TAG, "Removing from queue, canceling, and finishing: " + auth);
auth.mCallback.sendAuthenticationCanceled();
auth.mCallback.handleLifecycleAfterAuth();
mSuccessfulAuths.remove(auth);
}
}
}
private boolean isCurrentFaceAuth(@NonNull AuthenticationClient<?> client) {
return client == mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
}
private boolean isCurrentUdfps(@NonNull AuthenticationClient<?> client) {
return client == mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null);
}
private boolean isFaceScanning() {
AuthenticationClient<?> client = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
return client != null && client.getState() == AuthenticationClient.STATE_STARTED;
}
private static boolean isUdfpsActivelyAuthing(@Nullable AuthenticationClient<?> client) {
if (client instanceof Udfps) {
return client.getState() == AuthenticationClient.STATE_STARTED;
}
return false;
}
private static boolean isUdfpsAuthAttempted(@Nullable AuthenticationClient<?> client) {
if (client instanceof Udfps) {
return client.getState() == AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED;
}
return false;
}
private boolean isUnknownClient(@NonNull AuthenticationClient<?> client) {
for (AuthenticationClient<?> c : mClientMap.values()) {
if (c == client) {
return false;
}
}
return true;
}
private boolean isSingleAuthOnly(@NonNull AuthenticationClient<?> client) {
if (mClientMap.values().size() != 1) {
return false;
}
for (AuthenticationClient<?> c : mClientMap.values()) {
if (c != client) {
return false;
}
}
return true;
}
private boolean hasMultipleSuccessfulAuthentications() {
int count = 0;
for (AuthenticationClient<?> c : mClientMap.values()) {
if (c.wasAuthSuccessful()) {
count++;
}
if (count > 1) {
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Enabled: ").append(mAdvancedLogicEnabled);
sb.append(", Face Haptic Disabled: ").append(mFaceHapticDisabledWhenNonBypass);
sb.append(", Queue size: " ).append(mSuccessfulAuths.size());
for (SuccessfulAuth auth : mSuccessfulAuths) {
sb.append(", Auth: ").append(auth.toString());
}
return sb.toString();
}
}

View File

@@ -95,10 +95,9 @@ public class UserAwareBiometricScheduler extends BiometricScheduler {
@Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
@NonNull IBiometricService biometricService,
@NonNull CurrentUserRetriever currentUserRetriever,
@NonNull UserSwitchCallback userSwitchCallback,
@NonNull CoexCoordinator coexCoordinator) {
@NonNull UserSwitchCallback userSwitchCallback) {
super(tag, handler, sensorType, gestureAvailabilityDispatcher, biometricService,
LOG_NUM_RECENT_OPERATIONS, coexCoordinator);
LOG_NUM_RECENT_OPERATIONS);
mCurrentUserRetriever = currentUserRetriever;
mUserSwitchCallback = userSwitchCallback;
@@ -112,7 +111,7 @@ public class UserAwareBiometricScheduler extends BiometricScheduler {
this(tag, new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher,
IBiometricService.Stub.asInterface(
ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
currentUserRetriever, userSwitchCallback, CoexCoordinator.getInstance());
currentUserRetriever, userSwitchCallback);
}
@Override

View File

@@ -274,7 +274,6 @@ class FaceAuthenticationClient extends AuthenticationClient<AidlSession>
@Override
public void onLockoutTimed(long durationMillis) {
super.onLockoutTimed(durationMillis);
mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_TIMED);
// Lockout metrics are logged as an error code.
final int error = BiometricFaceConstants.FACE_ERROR_LOCKOUT;
@@ -290,7 +289,6 @@ class FaceAuthenticationClient extends AuthenticationClient<AidlSession>
@Override
public void onLockoutPermanent() {
super.onLockoutPermanent();
mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_PERMANENT);
// Lockout metrics are logged as an error code.
final int error = BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;

View File

@@ -424,7 +424,6 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession>
@Override
public void onLockoutTimed(long durationMillis) {
super.onLockoutTimed(durationMillis);
mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_TIMED);
// Lockout metrics are logged as an error code.
final int error = BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
@@ -448,7 +447,6 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession>
@Override
public void onLockoutPermanent() {
super.onLockoutPermanent();
mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_PERMANENT);
// Lockout metrics are logged as an error code.
final int error = BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;

View File

@@ -91,8 +91,7 @@ public class BiometricSchedulerTest {
mToken = new Binder();
mScheduler = new BiometricScheduler(TAG, new Handler(TestableLooper.get(this).getLooper()),
BiometricScheduler.SENSOR_TYPE_UNKNOWN, null /* gestureAvailabilityTracker */,
mBiometricService, LOG_NUM_RECENT_OPERATIONS,
CoexCoordinator.getInstance());
mBiometricService, LOG_NUM_RECENT_OPERATIONS);
}
@Test

View File

@@ -1,573 +0,0 @@
/*
* 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 static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_FACE;
import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_FP_OTHER;
import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_UDFPS;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.biometrics.BiometricConstants;
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.server.biometrics.sensors.fingerprint.Udfps;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.LinkedList;
@Presubmit
@SmallTest
public class CoexCoordinatorTest {
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@Mock
private CoexCoordinator.Callback mCallback;
@Mock
private CoexCoordinator.ErrorCallback mErrorCallback;
@Mock
private AuthenticationClient mFaceClient;
@Mock
private AuthenticationClient mFingerprintClient;
@Mock(extraInterfaces = {Udfps.class})
private AuthenticationClient mUdfpsClient;
private CoexCoordinator mCoexCoordinator;
@Before
public void setUp() {
mCoexCoordinator = CoexCoordinator.getInstance();
mCoexCoordinator.setAdvancedLogicEnabled(true);
mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true);
mCoexCoordinator.reset();
}
@Test
public void testBiometricPrompt_authSuccess() {
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testBiometricPrompt_authReject_whenNotLockedOut() {
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testBiometricPrompt_authReject_whenLockedOut() {
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testBiometricPrompt_coex_success() {
testBiometricPrompt_coex_success(false /* twice */);
}
@Test
public void testBiometricPrompt_coex_successWithoutDouble() {
testBiometricPrompt_coex_success(true /* twice */);
}
private void testBiometricPrompt_coex_success(boolean twice) {
initFaceAndFingerprintForBiometricPrompt();
when(mFaceClient.wasAuthSuccessful()).thenReturn(true);
when(mUdfpsClient.wasAuthSuccessful()).thenReturn(twice, true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mUdfpsClient, mCallback);
if (twice) {
verify(mCallback, never()).sendHapticFeedback();
} else {
verify(mCallback).sendHapticFeedback();
}
}
@Test
public void testBiometricPrompt_coex_reject() {
initFaceAndFingerprintForBiometricPrompt();
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback, never()).sendHapticFeedback();
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
}
@Test
public void testBiometricPrompt_coex_errorNoHaptics() {
initFaceAndFingerprintForBiometricPrompt();
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
mCoexCoordinator.onAuthenticationError(mUdfpsClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
verify(mErrorCallback, never()).sendHapticFeedback();
}
private void initFaceAndFingerprintForBiometricPrompt() {
when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(false);
when(mUdfpsClient.isBiometricPrompt()).thenReturn(true);
when(mUdfpsClient.wasAuthAttempted()).thenReturn(true);
}
@Test
public void testKeyguard_faceAuthOnly_success() {
when(mFaceClient.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
// Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead
// of removed) to keep this context.
// verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testKeyguard_faceAuthSuccess_nonBypass_udfpsRunning_noHaptics() {
testKeyguard_bypass_haptics(false /* bypassEnabled */,
true /* faceAccepted */,
false /* shouldReceiveHaptics */);
}
@Test
public void testKeyguard_faceAuthReject_nonBypass_udfpsRunning_noHaptics() {
testKeyguard_bypass_haptics(false /* bypassEnabled */,
false /* faceAccepted */,
false /* shouldReceiveHaptics */);
}
@Test
public void testKeyguard_faceAuthSuccess_bypass_udfpsRunning_haptics() {
testKeyguard_bypass_haptics(true /* bypassEnabled */,
true /* faceAccepted */,
true /* shouldReceiveHaptics */);
}
@Test
public void testKeyguard_faceAuthReject_bypass_udfpsRunning_haptics() {
testKeyguard_bypass_haptics(true /* bypassEnabled */,
false /* faceAccepted */,
true /* shouldReceiveHaptics */);
}
private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted,
boolean shouldReceiveHaptics) {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
if (faceAccepted) {
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
mCallback);
} else {
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
}
if (shouldReceiveHaptics) {
verify(mCallback).sendHapticFeedback();
} else {
verify(mCallback, never()).sendHapticFeedback();
}
verify(mCallback).sendAuthenticationResult(eq(faceAccepted) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testKeyguard_faceAuth_udfpsTouching_faceSuccess_thenUdfpsRejectedWithinBounds() {
testKeyguard_faceAuth_udfpsTouching_faceSuccess(false /* thenUdfpsAccepted */,
0 /* udfpsRejectedAfterMs */);
}
@Test
public void testKeyguard_faceAuth_udfpsTouching_faceSuccess_thenUdfpsRejectedAfterBounds() {
testKeyguard_faceAuth_udfpsTouching_faceSuccess(false /* thenUdfpsAccepted */,
CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS + 1 /* udfpsRejectedAfterMs */);
}
@Test
public void testKeyguard_faceAuth_udfpsTouching_faceSuccess_thenUdfpsAccepted() {
testKeyguard_faceAuth_udfpsTouching_faceSuccess(true /* thenUdfpsAccepted */,
0 /* udfpsRejectedAfterMs */);
}
private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted,
long udfpsRejectedAfterMs) {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
// For easier reading
final CoexCoordinator.Callback faceCallback = mCallback;
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
faceCallback);
verify(faceCallback, never()).sendHapticFeedback();
verify(faceCallback, never()).sendAuthenticationResult(anyBoolean());
// CoexCoordinator requests the system to hold onto this AuthenticationClient until
// UDFPS result is known
verify(faceCallback, never()).handleLifecycleAfterAuth();
// Reset the mock
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size());
assertEquals(mFaceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
if (thenUdfpsAccepted) {
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
udfpsCallback);
verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */);
verify(udfpsCallback).handleLifecycleAfterAuth();
verify(faceCallback).sendAuthenticationCanceled();
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
} else {
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, mUdfpsClient,
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) {
verify(udfpsCallback, never()).sendHapticFeedback();
verify(faceCallback).sendHapticFeedback();
verify(faceCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(faceCallback).handleLifecycleAfterAuth();
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
} else {
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
verify(faceCallback, never()).sendHapticFeedback();
verify(faceCallback, never()).sendAuthenticationResult(anyBoolean());
verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback)
.sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(udfpsCallback).handleLifecycleAfterAuth();
}
}
}
@Test
public void testKeyguard_udfpsAuthSuccess_whileFaceScanning() {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true));
verify(mFaceClient).cancel();
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback, never()).sendHapticFeedback();
verify(mCallback).handleLifecycleAfterAuth();
// BiometricScheduler removes the face authentication client after rejection
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
// Then UDFPS rejected
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mUdfpsClient,
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback, never()).sendHapticFeedback();
}
@Test
public void testKeyguard_udfpsRejected_thenFaceRejected_noKeyguardBypass() {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
// Auth was attempted
when(mUdfpsClient.getState())
.thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED);
verify(mCallback).sendHapticFeedback();
verify(mCallback).handleLifecycleAfterAuth();
// Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since
// unlike face, its lifecycle becomes "paused" instead of "finished".
CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class);
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, faceCallback);
verify(faceCallback).sendHapticFeedback();
verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).sendHapticFeedback();
}
@Test
public void testKeyguard_capacitiveAccepted_whenFaceScanning() {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFingerprintClient, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testKeyguard_capacitiveRejected_whenFaceScanning() {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mFingerprintClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testNonKeyguard_rejectAndNotLockedOut() {
when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false));
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testNonKeyguard_rejectLockedOut() {
when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testCleanupRunnable() {
LinkedList<CoexCoordinator.SuccessfulAuth> successfulAuths = mock(LinkedList.class);
CoexCoordinator.SuccessfulAuth auth = mock(CoexCoordinator.SuccessfulAuth.class);
CoexCoordinator.Callback callback = mock(CoexCoordinator.Callback.class);
CoexCoordinator.SuccessfulAuth.CleanupRunnable runnable =
new CoexCoordinator.SuccessfulAuth.CleanupRunnable(successfulAuths, auth, callback);
runnable.run();
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
verify(callback).handleLifecycleAfterAuth();
verify(successfulAuths).remove(eq(auth));
}
@Test
public void testBiometricPrompt_FaceError() {
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
verify(mErrorCallback).sendHapticFeedback();
}
@Test
public void testKeyguard_faceAuthOnly_errorWhenBypassEnabled() {
testKeyguard_faceAuthOnly(true /* bypassEnabled */);
}
@Test
public void testKeyguard_faceAuthOnly_errorWhenBypassDisabled() {
testKeyguard_faceAuthOnly(false /* bypassEnabled */);
}
private void testKeyguard_faceAuthOnly(boolean bypassEnabled) {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
when(mFaceClient.wasUserDetected()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
verify(mErrorCallback).sendHapticFeedback();
}
@Test
public void testKeyguard_coex_faceErrorWhenBypassEnabled() {
testKeyguard_coex_faceError(true /* bypassEnabled */);
}
@Test
public void testKeyguard_coex_faceErrorWhenBypassDisabled() {
testKeyguard_coex_faceError(false /* bypassEnabled */);
}
private void testKeyguard_coex_faceError(boolean bypassEnabled) {
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
when(mFaceClient.wasUserDetected()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
if (bypassEnabled) {
verify(mErrorCallback).sendHapticFeedback();
} else {
verify(mErrorCallback, never()).sendHapticFeedback();
}
}
}

View File

@@ -118,8 +118,7 @@ public class UserAwareBiometricSchedulerTest {
TEST_SENSOR_ID, mBiometricLogger, mBiometricContext,
mUserStartedCallback, mStartOperationsFinish);
}
},
CoexCoordinator.getInstance());
});
}
@Test

View File

@@ -35,7 +35,6 @@ import androidx.test.filters.SmallTest;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.CoexCoordinator;
import com.android.server.biometrics.sensors.LockoutCache;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.LockoutTracker;
@@ -91,8 +90,7 @@ public class SensorTest {
null /* gestureAvailabilityDispatcher */,
mBiometricService,
() -> USER_ID,
mUserSwitchCallback,
CoexCoordinator.getInstance());
mUserSwitchCallback);
mHalCallback = new Sensor.HalSessionCallback(mContext, new Handler(mLooper.getLooper()),
TAG, mScheduler, SENSOR_ID,
USER_ID, mLockoutCache, mLockoutResetDispatcher, mHalSessionCallback);

View File

@@ -35,7 +35,6 @@ import androidx.test.filters.SmallTest;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.CoexCoordinator;
import com.android.server.biometrics.sensors.LockoutCache;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.LockoutTracker;
@@ -91,8 +90,7 @@ public class SensorTest {
null /* gestureAvailabilityDispatcher */,
mBiometricService,
() -> USER_ID,
mUserSwitchCallback,
CoexCoordinator.getInstance());
mUserSwitchCallback);
mHalCallback = new Sensor.HalSessionCallback(mContext, new Handler(mLooper.getLooper()),
TAG, mScheduler, SENSOR_ID,
USER_ID, mLockoutCache, mLockoutResetDispatcher, mHalSessionCallback);