Merge "Removed Coexcoordinator."
This commit is contained in:
committed by
Android (Google) Code Review
commit
a2255a1f86
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,8 +118,7 @@ public class UserAwareBiometricSchedulerTest {
|
||||
TEST_SENSOR_ID, mBiometricLogger, mBiometricContext,
|
||||
mUserStartedCallback, mStartOperationsFinish);
|
||||
}
|
||||
},
|
||||
CoexCoordinator.getInstance());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user