Start UDFPS sensors only after BiometricPrompt UI is showing
Since UDFPS sensors have a UI affordance, and the affordance is
controlled by FingerprintAuthenticationClient, start the prepared
sensor only after the BiometricPrompt dialog is done animating.
Plumbing-wise, adds:
1) Adds AuthSession.STATE_AUTH_STARTED_UI_SHOWING
2) Adds IBiometricSysuiReceiver#onDialogAnimatedIn
Since not all sensors are started immediately anymore, adds
logic in BiometricScheduler when attempting to cancel auth
for a sensor that's waiting for cookie (HAL still idle).
Fixes: 171931476
Test: atest com.android.server.biometrics
Test: atest com.android.systemui.biometrics
Change-Id: Iaedc26c51e274614ac2af6ebd98afe26263dad81
diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 7a006c3..492ceeb 100644
--- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
@@ -28,4 +28,6 @@
void onDeviceCredentialPressed();
// Notifies the client that an internal event, e.g. back button has occurred.
void onSystemEvent(int event);
+ // Notifies that the dialog has finished animating in.
+ void onDialogAnimatedIn();
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 07e1f1b..2b33f8c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -640,6 +640,7 @@
}
mContainerState = STATE_SHOWING;
if (mBiometricView != null) {
+ mConfig.mCallback.onDialogAnimatedIn();
mBiometricView.onDialogAnimatedIn();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index a6b1b90..5738327 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -202,6 +202,20 @@
}
@Override
+ public void onDialogAnimatedIn() {
+ if (mReceiver == null) {
+ Log.e(TAG, "onDialogAnimatedIn: Receiver is null");
+ return;
+ }
+
+ try {
+ mReceiver.onDialogAnimatedIn();
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
+ }
+ }
+
+ @Override
public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
switch (reason) {
case AuthDialogCallback.DISMISSED_USER_CANCELED:
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index d3bd4fb..d8d07e7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
@@ -65,4 +65,9 @@
* @param event
*/
void onSystemEvent(int event);
+
+ /**
+ * Notifies when the dialog has finished animating in.
+ */
+ void onDialogAnimatedIn();
}
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index 3c18cd4..637a896 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -17,9 +17,11 @@
package com.android.server.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
@@ -33,6 +35,8 @@
import android.hardware.biometrics.PromptInfo;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.IBinder;
import android.os.RemoteException;
import android.security.KeyStore;
@@ -68,47 +72,54 @@
*/
static final int STATE_AUTH_CALLED = 1;
/**
- * Authentication started, BiometricPrompt is showing and the hardware is authenticating.
+ * Authentication started, BiometricPrompt is showing and the hardware is authenticating. At
+ * this point, the BiometricPrompt UI has been requested, but is not necessarily done animating
+ * in yet.
*/
static final int STATE_AUTH_STARTED = 2;
/**
+ * Same as {@link #STATE_AUTH_STARTED}, except the BiometricPrompt UI is done animating in.
+ */
+ static final int STATE_AUTH_STARTED_UI_SHOWING = 3;
+ /**
* Authentication is paused, waiting for the user to press "try again" button. Only
* passive modalities such as Face or Iris should have this state. Note that for passive
* modalities, the HAL enters the idle state after onAuthenticated(false) which differs from
* fingerprint.
*/
- static final int STATE_AUTH_PAUSED = 3;
+ static final int STATE_AUTH_PAUSED = 4;
/**
* Paused, but "try again" was pressed. Sensors have new cookies and we're now waiting for all
* cookies to be returned.
*/
- static final int STATE_AUTH_PAUSED_RESUMING = 4;
+ static final int STATE_AUTH_PAUSED_RESUMING = 5;
/**
* Authentication is successful, but we're waiting for the user to press "confirm" button.
*/
- static final int STATE_AUTH_PENDING_CONFIRM = 5;
+ static final int STATE_AUTH_PENDING_CONFIRM = 6;
/**
* Biometric authenticated, waiting for SysUI to finish animation
*/
- static final int STATE_AUTHENTICATED_PENDING_SYSUI = 6;
+ static final int STATE_AUTHENTICATED_PENDING_SYSUI = 7;
/**
* Biometric error, waiting for SysUI to finish animation
*/
- static final int STATE_ERROR_PENDING_SYSUI = 7;
+ static final int STATE_ERROR_PENDING_SYSUI = 8;
/**
* Device credential in AuthController is showing
*/
- static final int STATE_SHOWING_DEVICE_CREDENTIAL = 8;
+ static final int STATE_SHOWING_DEVICE_CREDENTIAL = 9;
/**
* The client binder died, and sensors were authenticating at the time. Cancel has been
* requested and we're waiting for the HAL(s) to send ERROR_CANCELED.
*/
- static final int STATE_CLIENT_DIED_CANCELLING = 9;
+ static final int STATE_CLIENT_DIED_CANCELLING = 10;
@IntDef({
STATE_AUTH_IDLE,
STATE_AUTH_CALLED,
STATE_AUTH_STARTED,
+ STATE_AUTH_STARTED_UI_SHOWING,
STATE_AUTH_PAUSED,
STATE_AUTH_PAUSED_RESUMING,
STATE_AUTH_PENDING_CONFIRM,
@@ -150,6 +161,7 @@
private final int mCallingPid;
private final int mCallingUserId;
private final boolean mDebugEnabled;
+ private final List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
@@ -165,12 +177,25 @@
// Timestamp when hardware authentication occurred
private long mAuthenticatedTimeMs;
- AuthSession(Context context, IStatusBarService statusBarService,
- IBiometricSysuiReceiver sysuiReceiver, KeyStore keystore, Random random,
- ClientDeathReceiver clientDeathReceiver, PreAuthInfo preAuthInfo, IBinder token,
- long operationId, int userId, IBiometricSensorReceiver sensorReceiver,
- IBiometricServiceReceiver clientReceiver, String opPackageName, PromptInfo promptInfo,
- int callingUid, int callingPid, int callingUserId, boolean debugEnabled) {
+ AuthSession(@NonNull Context context,
+ @NonNull IStatusBarService statusBarService,
+ @NonNull IBiometricSysuiReceiver sysuiReceiver,
+ @NonNull KeyStore keystore,
+ @NonNull Random random,
+ @NonNull ClientDeathReceiver clientDeathReceiver,
+ @NonNull PreAuthInfo preAuthInfo,
+ @NonNull IBinder token,
+ long operationId,
+ int userId,
+ @NonNull IBiometricSensorReceiver sensorReceiver,
+ @NonNull IBiometricServiceReceiver clientReceiver,
+ @NonNull String opPackageName,
+ @NonNull PromptInfo promptInfo,
+ int callingUid,
+ int callingPid,
+ int callingUserId,
+ boolean debugEnabled,
+ @NonNull List<FingerprintSensorPropertiesInternal> fingerprintSensorProperties) {
mContext = context;
mStatusBarService = statusBarService;
mSysuiReceiver = sysuiReceiver;
@@ -189,6 +214,7 @@
mCallingPid = callingPid;
mCallingUserId = callingUserId;
mDebugEnabled = debugEnabled;
+ mFingerprintSensorProperties = fingerprintSensorProperties;
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);
@@ -267,7 +293,10 @@
if (allCookiesReceived()) {
mStartTimeMs = System.currentTimeMillis();
- startAllPreparedSensors();
+
+ // For UDFPS, do not start until BiometricPrompt UI is shown. Otherwise, the UDFPS
+ // affordance will be shown before the BP UI is finished animating in.
+ startAllPreparedSensorsExceptUdfps();
// No need to request the UI if we're coming from the paused state.
if (mState != STATE_AUTH_PAUSED_RESUMING) {
@@ -311,13 +340,41 @@
return false;
}
- private void startAllPreparedSensors() {
+ private boolean isUdfpsSensor(@NonNull BiometricSensor sensor) {
+ if (sensor.modality != TYPE_FINGERPRINT) {
+ return false;
+ }
+
+ for (FingerprintSensorPropertiesInternal prop : mFingerprintSensorProperties) {
+ if (sensor.id == prop.sensorId && prop.isAnyUdfpsType()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void startAllPreparedSensorsExceptUdfps() {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
+ if (isUdfpsSensor(sensor)) {
+ Slog.d(TAG, "Skipping UDFPS, sensorId: " + sensor.id);
+ continue;
+ }
try {
sensor.startSensor();
} catch (RemoteException e) {
- Slog.e(TAG, "Unable to start prepared client, sensor ID: "
- + sensor.id, e);
+ Slog.e(TAG, "Unable to start prepared client, sensor: " + sensor, e);
+ }
+ }
+ }
+
+ private void startPreparedUdfpsSensors() {
+ for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
+ if (isUdfpsSensor(sensor)) {
+ try {
+ sensor.startSensor();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to start UDFPS sensor: " + sensor, e);
+ }
}
}
}
@@ -390,7 +447,8 @@
break;
}
- case STATE_AUTH_STARTED: {
+ case STATE_AUTH_STARTED:
+ case STATE_AUTH_STARTED_UI_SHOWING: {
final boolean errorLockout = error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT
|| error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
if (isAllowDeviceCredential() && errorLockout) {
@@ -463,6 +521,17 @@
}
}
+ void onDialogAnimatedIn() {
+ if (mState != STATE_AUTH_STARTED) {
+ Slog.w(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
+ }
+
+ mState = STATE_AUTH_STARTED_UI_SHOWING;
+
+ // For UDFPS devices, we can now start the sensor.
+ startPreparedUdfpsSensors();
+ }
+
void onTryAgainPressed() {
if (mState != STATE_AUTH_PAUSED) {
Slog.w(TAG, "onTryAgainPressed, state: " + mState);
@@ -543,13 +612,15 @@
*/
boolean onClientDied() {
try {
- if (mState == STATE_AUTH_STARTED) {
- mState = STATE_CLIENT_DIED_CANCELLING;
- cancelAllSensors();
- return false;
- } else {
- mStatusBarService.hideAuthenticationDialog();
- return true;
+ switch (mState) {
+ case STATE_AUTH_STARTED:
+ case STATE_AUTH_STARTED_UI_SHOWING:
+ mState = STATE_CLIENT_DIED_CANCELLING;
+ cancelAllSensors();
+ return false;
+ default:
+ mStatusBarService.hideAuthenticationDialog();
+ return true;
}
} catch (RemoteException e) {
Slog.e(TAG, "Remote Exception: " + e);
@@ -676,7 +747,10 @@
* @return true if this AuthSession is finished, e.g. should be set to null
*/
boolean onCancelAuthSession(boolean force) {
- if (mState == STATE_AUTH_STARTED && !force) {
+ final boolean authStarted = mState == STATE_AUTH_STARTED
+ || mState == STATE_AUTH_STARTED_UI_SHOWING;
+
+ if (authStarted && !force) {
cancelAllSensors();
// Wait for ERROR_CANCELED to be returned from the sensors
return false;
@@ -705,7 +779,7 @@
* {@link #STATE_SHOWING_DEVICE_CREDENTIAL} or dismissed.
*/
private void cancelBiometricOnly() {
- if (mState == STATE_AUTH_STARTED) {
+ if (mState == STATE_AUTH_STARTED || mState == STATE_AUTH_STARTED_UI_SHOWING) {
cancelAllSensors();
}
}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 3e0a40f..4d555e9 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -39,6 +39,8 @@
import android.hardware.biometrics.IBiometricServiceReceiver;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.net.Uri;
import android.os.Binder;
import android.os.DeadObjectException;
@@ -90,6 +92,7 @@
private static final int MSG_ON_DEVICE_CREDENTIAL_PRESSED = 12;
private static final int MSG_ON_SYSTEM_EVENT = 13;
private static final int MSG_CLIENT_DIED = 14;
+ private static final int MSG_ON_DIALOG_ANIMATED_IN = 15;
private final Injector mInjector;
private final DevicePolicyManager mDevicePolicyManager;
@@ -221,6 +224,11 @@
break;
}
+ case MSG_ON_DIALOG_ANIMATED_IN: {
+ handleOnDialogAnimatedIn();
+ break;
+ }
+
default:
Slog.e(TAG, "Unknown message: " + msg);
break;
@@ -451,6 +459,11 @@
public void onSystemEvent(int event) {
mHandler.obtainMessage(MSG_ON_SYSTEM_EVENT, event).sendToTarget();
}
+
+ @Override
+ public void onDialogAnimatedIn() {
+ mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget();
+ }
};
private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> {
@@ -763,6 +776,16 @@
public DevicePolicyManager getDevicePolicyManager(Context context) {
return context.getSystemService(DevicePolicyManager.class);
}
+
+ public List<FingerprintSensorPropertiesInternal> getFingerprintSensorProperties(
+ Context context) {
+ final FingerprintManager fpm = context.getSystemService(FingerprintManager.class);
+ if (fpm != null) {
+ return fpm.getSensorPropertiesInternal();
+ } else {
+ return new ArrayList<>();
+ }
+ }
}
/**
@@ -946,7 +969,7 @@
private void handleClientDied() {
if (mCurrentAuthSession == null) {
- Slog.e(TAG, "Auth session null");
+ Slog.e(TAG, "handleClientDied: AuthSession is null");
return;
}
@@ -957,6 +980,15 @@
}
}
+ private void handleOnDialogAnimatedIn() {
+ if (mCurrentAuthSession == null) {
+ Slog.e(TAG, "handleOnDialogAnimatedIn: AuthSession is null");
+ return;
+ }
+
+ mCurrentAuthSession.onDialogAnimatedIn();
+ }
+
/**
* Invoked when each service has notified that its client is ready to be started. When
* all biometrics are ready, this invokes the SystemUI dialog through StatusBar.
@@ -1040,7 +1072,8 @@
mCurrentAuthSession = new AuthSession(getContext(), mStatusBarService, mSysuiReceiver,
mKeyStore, mRandom, mClientDeathReceiver, preAuthInfo, token, operationId, userId,
mBiometricSensorReceiver, receiver, opPackageName, promptInfo, callingUid,
- callingPid, callingUserId, debugEnabled);
+ callingPid, callingUserId, debugEnabled,
+ mInjector.getFingerprintSensorProperties(getContext()));
try {
mCurrentAuthSession.goToInitialState();
} catch (RemoteException e) {
@@ -1067,5 +1100,12 @@
pw.println(" " + sensor);
}
pw.println("CurrentSession: " + mCurrentAuthSession);
+
+ final List<FingerprintSensorPropertiesInternal> fpProps =
+ mInjector.getFingerprintSensorProperties(getContext());
+ pw.println("FingerprintSensorProperties: " + fpProps.size());
+ for (FingerprintSensorPropertiesInternal prop : fpProps) {
+ pw.println(" " + prop);
+ }
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index ce2d340..2784f46 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.IBiometricService;
import android.os.Handler;
import android.os.IBinder;
@@ -378,9 +379,20 @@
return;
}
if (mCurrentOperation.state != Operation.STATE_WAITING_FOR_COOKIE) {
- Slog.e(getTag(), "Operation is in the wrong state: " + mCurrentOperation
- + ", expected STATE_WAITING_FOR_COOKIE");
- return;
+ if (mCurrentOperation.state == Operation.STATE_WAITING_IN_QUEUE_CANCELING) {
+ Slog.d(getTag(), "Operation was marked for cancellation, cancelling now: "
+ + mCurrentOperation);
+ // This should trigger the internal onClientFinished callback, which clears the
+ // operation and starts the next one.
+ final Interruptable interruptable = (Interruptable) mCurrentOperation.clientMonitor;
+ interruptable.onError(BiometricConstants.BIOMETRIC_ERROR_CANCELED,
+ 0 /* vendorCode */);
+ return;
+ } else {
+ Slog.e(getTag(), "Operation is in the wrong state: " + mCurrentOperation
+ + ", expected STATE_WAITING_FOR_COOKIE");
+ return;
+ }
}
if (mCurrentOperation.clientMonitor.getCookie() != cookie) {
Slog.e(getTag(), "Mismatched cookie for operation: " + mCurrentOperation
@@ -461,6 +473,13 @@
Slog.w(getTag(), "Cancel already invoked for operation: " + operation);
return;
}
+ if (operation.state == Operation.STATE_WAITING_FOR_COOKIE) {
+ Slog.w(getTag(), "Skipping cancellation for non-started operation: " + operation);
+ // We can set it to null immediately, since the HAL was never notified to start.
+ mCurrentOperation = null;
+ startNextOperationIfIdle();
+ return;
+ }
Slog.d(getTag(), "[Cancelling] Current client: " + operation.clientMonitor);
final Interruptable interruptable = (Interruptable) operation.clientMonitor;
interruptable.cancel();
@@ -505,8 +524,9 @@
mCurrentOperation.clientMonitor instanceof AuthenticationConsumer;
final boolean tokenMatches = mCurrentOperation.clientMonitor.getToken() == token;
if (!isAuthenticating || !tokenMatches) {
- Slog.w(getTag(), "Not cancelling authentication, isEnrolling: " + isAuthenticating
- + " tokenMatches: " + tokenMatches);
+ Slog.w(getTag(), "Not cancelling authentication"
+ + ", current operation : " + mCurrentOperation
+ + ", tokenMatches: " + tokenMatches);
return;
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java
index c87f62f..61e7c89 100644
--- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java
@@ -37,7 +37,7 @@
* It may be possible at some point in the future to combine I<Sensor>ServiceReceivers to share
* a common interface.
*/
-public final class ClientMonitorCallbackConverter {
+public class ClientMonitorCallbackConverter {
private IBiometricSensorReceiver mSensorReceiver; // BiometricService
private IFaceServiceReceiver mFaceServiceReceiver; // FaceManager
private IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManager
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index e8c9697..6b000f3 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -24,12 +24,16 @@
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
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.annotation.NonNull;
import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager;
import android.content.Context;
@@ -39,6 +43,9 @@
import android.hardware.biometrics.IBiometricServiceReceiver;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
+import android.hardware.biometrics.SensorProperties;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
@@ -79,7 +86,8 @@
private IBinder mToken;
// Assume all tests can be done with the same set of sensors for now.
- private List<BiometricSensor> mSensors;
+ @NonNull private List<BiometricSensor> mSensors;
+ @NonNull private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProps;
@Before
public void setUp() throws Exception {
@@ -88,11 +96,12 @@
mRandom = new Random();
mToken = new Binder();
mSensors = new ArrayList<>();
+ mFingerprintSensorProps = new ArrayList<>();
}
@Test
public void testNewAuthSession_eligibleSensorsSetToStateUnknown() throws RemoteException {
- setupFingerprint(0 /* id */);
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_REAR);
setupFace(1 /* id */, false /* confirmationAlwaysRequired */);
final AuthSession session = createAuthSession(mSensors,
@@ -110,10 +119,9 @@
}
@Test
- public void testStartNewAuthSession()
- throws RemoteException {
+ public void testStartNewAuthSession() throws RemoteException {
setupFace(0 /* id */, false /* confirmationAlwaysRequired */);
- setupFingerprint(1 /* id */);
+ setupFingerprint(1 /* id */, FingerprintSensorProperties.TYPE_REAR);
final boolean requireConfirmation = true;
final long operationId = 123;
@@ -175,6 +183,60 @@
}
}
+ @Test
+ public void testUdfpsAuth_sensorStartsAfterDialogAnimationCompletes() throws RemoteException {
+ // For UDFPS-only setups, ensure that the sensor does not start auth until after the
+ // BiometricPrompt UI is finished animating. Otherwise, the UDFPS affordance will be
+ // shown before the BiometricPrompt is shown.
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+
+ final long operationId = 123;
+ final int userId = 10;
+ final int callingUid = 100;
+ final int callingPid = 1000;
+ final int callingUserId = 10000;
+
+ final AuthSession session = createAuthSession(mSensors,
+ false /* checkDevicePolicyManager */,
+ Authenticators.BIOMETRIC_STRONG,
+ operationId,
+ userId,
+ callingUid,
+ callingPid,
+ callingUserId);
+ assertEquals(mSensors.size(), session.mPreAuthInfo.eligibleSensors.size());
+
+ for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) {
+ assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
+ assertEquals(0, sensor.getCookie());
+ }
+
+ session.goToInitialState();
+
+ final int cookie1 = session.mPreAuthInfo.eligibleSensors.get(0).getCookie();
+ session.onCookieReceived(cookie1);
+ for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) {
+ if (cookie1 == sensor.getCookie()) {
+ assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, sensor.getSensorState());
+ } else {
+ assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState());
+ }
+ }
+ assertTrue(session.allCookiesReceived());
+
+ // UDFPS does not start even if all cookies are received
+ assertEquals(AuthSession.STATE_AUTH_STARTED, session.getState());
+ verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(),
+ anyBoolean(), anyBoolean(), anyInt(), any(), anyLong());
+
+ // Notify AuthSession that the UI is shown. Then, UDFPS sensor should be started.
+ session.onDialogAnimatedIn();
+ assertEquals(AuthSession.STATE_AUTH_STARTED_UI_SHOWING, session.getState());
+ assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+ session.mPreAuthInfo.eligibleSensors.get(0).getSensorState());
+
+ }
+
private PreAuthInfo createPreAuthInfo(List<BiometricSensor> sensors, int userId,
PromptInfo promptInfo, boolean checkDevicePolicyManager) throws RemoteException {
return PreAuthInfo.create(mTrustManager,
@@ -197,11 +259,10 @@
final PreAuthInfo preAuthInfo = createPreAuthInfo(sensors, userId, promptInfo,
checkDevicePolicyManager);
-
return new AuthSession(mContext, mStatusBarService, mSysuiReceiver, mKeyStore,
mRandom, mClientDeathReceiver, preAuthInfo, mToken, operationId, userId,
mSensorReceiver, mClientReceiver, TEST_PACKAGE, promptInfo, callingUid,
- callingPid, callingUserId, false /* debugEnabled */);
+ callingPid, callingUserId, false /* debugEnabled */, mFingerprintSensorProps);
}
private PromptInfo createPromptInfo(@Authenticators.Types int authenticators) {
@@ -210,8 +271,8 @@
return promptInfo;
}
-
- private void setupFingerprint(int id) throws RemoteException {
+ private void setupFingerprint(int id, @FingerprintSensorProperties.SensorType int type)
+ throws RemoteException {
IBiometricAuthenticator fingerprintAuthenticator = mock(IBiometricAuthenticator.class);
when(fingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
when(fingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true);
@@ -229,6 +290,12 @@
return false; // fingerprint does not support confirmation
}
});
+
+ mFingerprintSensorProps.add(new FingerprintSensorPropertiesInternal(id,
+ SensorProperties.STRENGTH_STRONG,
+ 5 /* maxEnrollmentsPerUser */,
+ type,
+ false /* resetLockoutRequiresHardwareAuthToken */));
}
private void setupFace(int id, boolean confirmationAlwaysRequired) throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index c890c52..24e7d7d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -20,20 +20,27 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
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 android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.IBiometricService;
+import android.os.Binder;
+import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.server.biometrics.sensors.BiometricScheduler.Operation;
+
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
@@ -44,8 +51,10 @@
public class BiometricSchedulerTest {
private static final String TAG = "BiometricSchedulerTest";
+ private static final int TEST_SENSOR_ID = 1;
private BiometricScheduler mScheduler;
+ private IBinder mToken;
@Mock
private Context mContext;
@@ -55,6 +64,7 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ mToken = new Binder();
mScheduler = new BiometricScheduler(TAG, null /* gestureAvailabilityTracker */,
mBiometricService);
}
@@ -63,8 +73,8 @@
public void testClientDuplicateFinish_ignoredBySchedulerAndDoesNotCrash() {
final ClientMonitor.LazyDaemon<Object> nonNullDaemon = () -> mock(Object.class);
- final ClientMonitor<Object> client1 = new TestClientMonitor(mContext, nonNullDaemon);
- final ClientMonitor<Object> client2 = new TestClientMonitor(mContext, nonNullDaemon);
+ final ClientMonitor<Object> client1 = new TestClientMonitor(mContext, mToken, nonNullDaemon);
+ final ClientMonitor<Object> client2 = new TestClientMonitor(mContext, mToken, nonNullDaemon);
mScheduler.scheduleClientMonitor(client1);
mScheduler.scheduleClientMonitor(client2);
@@ -80,8 +90,8 @@
final ClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> null;
final ClientMonitor.LazyDaemon<Object> lazyDaemon2 = () -> daemon2;
- final TestClientMonitor client1 = new TestClientMonitor(mContext, lazyDaemon1);
- final TestClientMonitor client2 = new TestClientMonitor(mContext, lazyDaemon2);
+ final TestClientMonitor client1 = new TestClientMonitor(mContext, mToken, lazyDaemon1);
+ final TestClientMonitor client2 = new TestClientMonitor(mContext, mToken, lazyDaemon2);
final ClientMonitor.Callback callback1 = mock(ClientMonitor.Callback.class);
final ClientMonitor.Callback callback2 = mock(ClientMonitor.Callback.class);
@@ -110,16 +120,18 @@
}
@Test
- public void testRemovesOnlyBiometricPromptOperation_whenNullHal() {
+ public void testRemovesOnlyBiometricPromptOperation_whenNullHal() throws Exception {
// Second non-BiometricPrompt client has a valid daemon
final Object daemon2 = mock(Object.class);
final ClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> null;
final ClientMonitor.LazyDaemon<Object> lazyDaemon2 = () -> daemon2;
- final TestClientMonitor client1 =
- new TestBiometricPromptClientMonitor(mContext, lazyDaemon1);
- final TestClientMonitor client2 = new TestClientMonitor(mContext, lazyDaemon2);
+ final ClientMonitorCallbackConverter listener1 = mock(ClientMonitorCallbackConverter.class);
+
+ final BiometricPromptClientMonitor client1 =
+ new BiometricPromptClientMonitor(mContext, mToken, lazyDaemon1, listener1);
+ final TestClientMonitor client2 = new TestClientMonitor(mContext, mToken, lazyDaemon2);
final ClientMonitor.Callback callback1 = mock(ClientMonitor.Callback.class);
final ClientMonitor.Callback callback2 = mock(ClientMonitor.Callback.class);
@@ -139,8 +151,10 @@
// Simulate that the BiometricPrompt client's sensor is ready
mScheduler.startPreparedClient(client1.getCookie());
- assertTrue(client1.wasUnableToStart());
- verify(callback1).onClientFinished(eq(client1), eq(false) /* success */);
+ // Client 1 cleans up properly
+ verify(listener1).onError(eq(TEST_SENSOR_ID), anyInt(),
+ eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(0));
+ verify(callback1).onClientFinished(eq(client1), eq(true) /* success */);
verify(callback1, never()).onClientStarted(any());
// Client 2 was able to start
@@ -149,10 +163,45 @@
verify(callback2).onClientStarted(eq(client2));
}
- private static class TestBiometricPromptClientMonitor extends TestClientMonitor {
- public TestBiometricPromptClientMonitor(@NonNull Context context,
- @NonNull LazyDaemon<Object> lazyDaemon) {
- super(context, lazyDaemon, 1 /* cookie */);
+ @Test
+ public void testCancelNotInvoked_whenOperationWaitingForCookie() {
+ final ClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> mock(Object.class);
+ final BiometricPromptClientMonitor client1 = new BiometricPromptClientMonitor(mContext,
+ mToken, lazyDaemon1, mock(ClientMonitorCallbackConverter.class));
+ final ClientMonitor.Callback callback1 = mock(ClientMonitor.Callback.class);
+
+ // Schedule a BiometricPrompt authentication request
+ mScheduler.scheduleClientMonitor(client1, callback1);
+
+ assertEquals(Operation.STATE_WAITING_FOR_COOKIE, mScheduler.mCurrentOperation.state);
+ assertEquals(client1, mScheduler.mCurrentOperation.clientMonitor);
+ assertEquals(0, mScheduler.mPendingOperations.size());
+
+ // Request it to be canceled. The operation can be canceled immediately, and the scheduler
+ // should go back to idle, since in this case the framework has not even requested the HAL
+ // to authenticate yet.
+ mScheduler.cancelAuthentication(mToken);
+ assertNull(mScheduler.mCurrentOperation);
+ }
+
+ private static class BiometricPromptClientMonitor extends AuthenticationClient<Object> {
+
+ public BiometricPromptClientMonitor(@NonNull Context context, @NonNull IBinder token,
+ @NonNull LazyDaemon<Object> lazyDaemon, ClientMonitorCallbackConverter listener) {
+ super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
+ false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
+ TEST_SENSOR_ID, true /* isStrongBiometric */, 0 /* statsModality */,
+ 0 /* statsClient */, null /* taskStackListener */, mock(LockoutTracker.class));
+ }
+
+ @Override
+ protected void stopHalOperation() {
+
+ }
+
+ @Override
+ protected void startHalOperation() {
+
}
}
@@ -160,16 +209,15 @@
private boolean mUnableToStart;
private boolean mStarted;
- public TestClientMonitor(@NonNull Context context, @NonNull LazyDaemon<Object> lazyDaemon) {
- super(context, lazyDaemon, null /* token */, null /* listener */, 0 /* userId */,
- TAG, 0 /* cookie */, 0 /* sensorId */, 0 /* statsModality */,
- 0 /* statsAction */, 0 /* statsClient */);
+ public TestClientMonitor(@NonNull Context context, @NonNull IBinder token,
+ @NonNull LazyDaemon<Object> lazyDaemon) {
+ this(context, token, lazyDaemon, 0 /* cookie */);
}
- public TestClientMonitor(@NonNull Context context, @NonNull LazyDaemon<Object> lazyDaemon,
- int cookie) {
- super(context, lazyDaemon, null /* token */, null /* listener */, 0 /* userId */,
- TAG, cookie, 0 /* sensorId */, 0 /* statsModality */,
+ public TestClientMonitor(@NonNull Context context, @NonNull IBinder token,
+ @NonNull LazyDaemon<Object> lazyDaemon, int cookie) {
+ super(context, lazyDaemon, token /* token */, null /* listener */, 0 /* userId */,
+ TAG, cookie, TEST_SENSOR_ID, 0 /* statsModality */,
0 /* statsAction */, 0 /* statsClient */);
}