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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -640,6 +640,7 @@ public class AuthContainerView extends LinearLayout
|
||||
}
|
||||
mContainerState = STATE_SHOWING;
|
||||
if (mBiometricView != null) {
|
||||
mConfig.mCallback.onDialogAnimatedIn();
|
||||
mBiometricView.onDialogAnimatedIn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -65,4 +65,9 @@ public interface AuthDialogCallback {
|
||||
* @param event
|
||||
*/
|
||||
void onSystemEvent(int event);
|
||||
|
||||
/**
|
||||
* Notifies when the dialog has finished animating in.
|
||||
*/
|
||||
void onDialogAnimatedIn();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user