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
This commit is contained in:
Kevin Chyn
2020-11-11 16:31:07 -08:00
parent 38c45bdfc2
commit bf80ecae16
10 changed files with 338 additions and 67 deletions

View File

@@ -28,4 +28,6 @@ oneway interface IBiometricSysuiReceiver {
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();
}

View File

@@ -640,6 +640,7 @@ public class AuthContainerView extends LinearLayout
}
mContainerState = STATE_SHOWING;
if (mBiometricView != null) {
mConfig.mCallback.onDialogAnimatedIn();
mBiometricView.onDialogAnimatedIn();
}
}

View File

@@ -201,6 +201,20 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
}
}
@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) {

View File

@@ -65,4 +65,9 @@ public interface AuthDialogCallback {
* @param event
*/
void onSystemEvent(int event);
/**
* Notifies when the dialog has finished animating in.
*/
void onDialogAnimatedIn();
}

View File

@@ -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.IBiometricSysuiReceiver;
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
*/
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
// 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 @@ public final class AuthSession implements IBinder.DeathRecipient {
mCallingPid = callingPid;
mCallingUserId = callingUserId;
mDebugEnabled = debugEnabled;
mFingerprintSensorProperties = fingerprintSensorProperties;
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);
@@ -267,7 +293,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
}
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
*/
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 @@ public final class AuthSession implements IBinder.DeathRecipient {
* @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 @@ public final class AuthSession implements IBinder.DeathRecipient {
* {@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();
}
}

View File

@@ -39,6 +39,8 @@ import android.hardware.biometrics.IBiometricService;
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 @@ public class BiometricService extends SystemService {
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 @@ public class BiometricService extends SystemService {
break;
}
case MSG_ON_DIALOG_ANIMATED_IN: {
handleOnDialogAnimatedIn();
break;
}
default:
Slog.e(TAG, "Unknown message: " + msg);
break;
@@ -451,6 +459,11 @@ public class BiometricService extends SystemService {
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 class BiometricService extends SystemService {
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 @@ public class BiometricService extends SystemService {
private void handleClientDied() {
if (mCurrentAuthSession == null) {
Slog.e(TAG, "Auth session null");
Slog.e(TAG, "handleClientDied: AuthSession is null");
return;
}
@@ -957,6 +980,15 @@ public class BiometricService extends SystemService {
}
}
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 @@ public class BiometricService extends SystemService {
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 @@ public class BiometricService extends SystemService {
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);
}
}
}

View File

@@ -20,6 +20,7 @@ import android.annotation.IntDef;
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 @@ public class BiometricScheduler {
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 @@ public class BiometricScheduler {
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 @@ public class BiometricScheduler {
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;
}

View File

@@ -37,7 +37,7 @@ import android.os.RemoteException;
* 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

View File

@@ -24,12 +24,16 @@ import static junit.framework.Assert.assertFalse;
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.IBiometricSensorReceiver;
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 @@ public class AuthSessionTest {
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 @@ public class AuthSessionTest {
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 @@ public class AuthSessionTest {
}
@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 @@ public class AuthSessionTest {
}
}
@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 @@ public class AuthSessionTest {
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 @@ public class AuthSessionTest {
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 @@ public class AuthSessionTest {
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 {

View File

@@ -20,20 +20,27 @@ import static junit.framework.Assert.assertTrue;
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 @@ import org.mockito.MockitoAnnotations;
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 @@ public class BiometricSchedulerTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mToken = new Binder();
mScheduler = new BiometricScheduler(TAG, null /* gestureAvailabilityTracker */,
mBiometricService);
}
@@ -63,8 +73,8 @@ public class BiometricSchedulerTest {
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 @@ public class BiometricSchedulerTest {
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 @@ public class BiometricSchedulerTest {
}
@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 @@ public class BiometricSchedulerTest {
// 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 @@ public class BiometricSchedulerTest {
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 @@ public class BiometricSchedulerTest {
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 */);
}