diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 7a006c34e1e16..492ceebe4d066 100644
--- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
@@ -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();
}
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index 2439fedd05182..aed067c302536 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -88,7 +88,8 @@
android:layout_width="8dp"
android:layout_height="match_parent"
android:visibility="visible" />
-
+
+
+
+
+
+
+
+
-
+
{
- if (mState == STATE_PENDING_CONFIRMATION) {
- mCallback.onAction(Callback.ACTION_USER_CANCELED);
- } else {
- if (isDeviceCredentialAllowed()) {
- startTransitionToCredentialUI();
- } else {
- mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
- }
- }
+ mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
});
- mPositiveButton.setOnClickListener((view) -> {
+ mCancelButton.setOnClickListener((view) -> {
+ mCallback.onAction(Callback.ACTION_USER_CANCELED);
+ });
+
+ mUseCredentialButton.setOnClickListener((view) -> {
+ startTransitionToCredentialUI();
+ });
+
+ mConfirmButton.setOnClickListener((view) -> {
updateState(STATE_AUTHENTICATED);
});
@@ -645,31 +677,36 @@ public abstract class AuthBiometricView extends LinearLayout {
void onAttachedToWindowInternal() {
setText(mTitleView, mPromptInfo.getTitle());
- final CharSequence negativeText;
if (isDeviceCredentialAllowed()) {
-
+ final CharSequence credentialButtonText;
final @Utils.CredentialType int credentialType =
Utils.getCredentialType(mContext, mEffectiveUserId);
-
switch (credentialType) {
case Utils.CREDENTIAL_PIN:
- negativeText = getResources().getString(R.string.biometric_dialog_use_pin);
+ credentialButtonText =
+ getResources().getString(R.string.biometric_dialog_use_pin);
break;
case Utils.CREDENTIAL_PATTERN:
- negativeText = getResources().getString(R.string.biometric_dialog_use_pattern);
+ credentialButtonText =
+ getResources().getString(R.string.biometric_dialog_use_pattern);
break;
case Utils.CREDENTIAL_PASSWORD:
- negativeText = getResources().getString(R.string.biometric_dialog_use_password);
+ credentialButtonText =
+ getResources().getString(R.string.biometric_dialog_use_password);
break;
default:
- negativeText = getResources().getString(R.string.biometric_dialog_use_password);
+ credentialButtonText =
+ getResources().getString(R.string.biometric_dialog_use_password);
break;
}
+ mNegativeButton.setVisibility(View.GONE);
+
+ mUseCredentialButton.setText(credentialButtonText);
+ mUseCredentialButton.setVisibility(View.VISIBLE);
} else {
- negativeText = mPromptInfo.getNegativeButtonText();
+ setText(mNegativeButton, mPromptInfo.getNegativeButtonText());
}
- setText(mNegativeButton, negativeText);
setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle());
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 07e1f1b7f4c34..2b33f8cd036e3 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 @@ public class AuthContainerView extends LinearLayout
}
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 3f66c08125331..ecbe5f4d3ac9d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -200,6 +200,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) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index d3bd4fbd921c4..d8d07e7dd24ac 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 @@ public interface AuthDialogCallback {
* @param event
*/
void onSystemEvent(int event);
+
+ /**
+ * Notifies when the dialog has finished animating in.
+ */
+ void onDialogAnimatedIn();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java
index b907cdb54bbff..043bd5cd6ba5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java
@@ -50,8 +50,12 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
private TestableFaceView mFaceView;
@Mock private Button mNegativeButton;
- @Mock private Button mPositiveButton;
+ @Mock private Button mCancelButton;
+ @Mock private Button mUseCredentialButton;
+
+ @Mock private Button mConfirmButton;
@Mock private Button mTryAgainButton;
+
@Mock private TextView mErrorView;
@Before
@@ -60,9 +64,14 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
mFaceView = new TestableFaceView(mContext);
mFaceView.mIconController = mock(TestableFaceView.TestableIconController.class);
mFaceView.setCallback(mCallback);
+
mFaceView.mNegativeButton = mNegativeButton;
- mFaceView.mPositiveButton = mPositiveButton;
+ mFaceView.mCancelButton = mCancelButton;
+ mFaceView.mUseCredentialButton = mUseCredentialButton;
+
+ mFaceView.mConfirmButton = mConfirmButton;
mFaceView.mTryAgainButton = mTryAgainButton;
+
mFaceView.mIndicatorView = mErrorView;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
index e2517f27c0ea0..49282ee360e25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
@@ -60,8 +60,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Mock private AuthPanelController mPanelController;
@Mock private Button mNegativeButton;
+ @Mock private Button mCancelButton;
+ @Mock private Button mUseCredentialButton;
+
@Mock private Button mPositiveButton;
@Mock private Button mTryAgainButton;
+
@Mock private TextView mTitleView;
@Mock private TextView mSubtitleView;
@Mock private TextView mDescriptionView;
@@ -89,15 +93,31 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() {
- initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
+ final Button negativeButton = new Button(mContext);
+ final Button cancelButton = new Button(mContext);
+ initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
+ @Override
+ public Button getNegativeButton() {
+ return negativeButton;
+ }
+
+ @Override
+ public Button getCancelButton() {
+ return cancelButton;
+ }
+ });
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
waitForIdleSync();
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
verify(mCallback, never()).onAction(anyInt());
- verify(mBiometricView.mNegativeButton).setText(eq(R.string.cancel));
- verify(mBiometricView.mPositiveButton).setEnabled(eq(true));
+
+ assertEquals(View.GONE, negativeButton.getVisibility());
+ assertEquals(View.VISIBLE, cancelButton.getVisibility());
+ assertTrue(cancelButton.isEnabled());
+
+ verify(mBiometricView.mConfirmButton).setEnabled(eq(true));
verify(mIndicatorView).setText(eq(R.string.biometric_dialog_tap_confirm));
verify(mIndicatorView).setVisibility(eq(View.VISIBLE));
}
@@ -107,7 +127,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
Button button = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
- public Button getPositiveButton() {
+ public Button getConfirmButton() {
return button;
}
});
@@ -137,18 +157,26 @@ public class AuthBiometricViewTest extends SysuiTestCase {
}
@Test
- public void testNegativeButton_whenPendingConfirmation_sendsActionUserCanceled() {
- Button button = new Button(mContext);
+ public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() {
+ Button cancelButton = new Button(mContext);
+ Button negativeButton = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
- return button;
+ return negativeButton;
+ }
+ @Override
+ public Button getCancelButton() {
+ return cancelButton;
}
});
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
- button.performClick();
+
+ assertEquals(View.GONE, negativeButton.getVisibility());
+
+ cancelButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED);
@@ -282,16 +310,23 @@ public class AuthBiometricViewTest extends SysuiTestCase {
}
@Test
- public void testNegativeButton_whenDeviceCredentialAllowed() {
- Button negativeButton = new Button(mContext);
+ public void testCredentialButton_whenDeviceCredentialAllowed() {
+ final Button negativeButton = new Button(mContext);
+ final Button useCredentialButton = new Button(mContext);
initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return negativeButton;
}
+
+ @Override
+ public Button getUseCredentialButton() {
+ return useCredentialButton;
+ }
});
- negativeButton.performClick();
+ assertEquals(View.GONE, negativeButton.getVisibility());
+ useCredentialButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
@@ -361,7 +396,17 @@ public class AuthBiometricViewTest extends SysuiTestCase {
}
@Override
- public Button getPositiveButton() {
+ public Button getCancelButton() {
+ return mCancelButton;
+ }
+
+ @Override
+ public Button getUseCredentialButton() {
+ return mUseCredentialButton;
+ }
+
+ @Override
+ public Button getConfirmButton() {
return mPositiveButton;
}
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index 3c18cd4ed231b..637a8963402ea 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.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 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 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();
}
}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 29424b438963e..196582a6c0d66 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.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 = () -> {
@@ -749,6 +762,16 @@ public class BiometricService extends SystemService {
public DevicePolicyManager getDevicePolicyManager(Context context) {
return context.getSystemService(DevicePolicyManager.class);
}
+
+ public List getFingerprintSensorProperties(
+ Context context) {
+ final FingerprintManager fpm = context.getSystemService(FingerprintManager.class);
+ if (fpm != null) {
+ return fpm.getSensorPropertiesInternal();
+ } else {
+ return new ArrayList<>();
+ }
+ }
}
/**
@@ -932,7 +955,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;
}
@@ -943,6 +966,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.
@@ -1026,7 +1058,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) {
@@ -1053,5 +1086,12 @@ public class BiometricService extends SystemService {
pw.println(" " + sensor);
}
pw.println("CurrentSession: " + mCurrentAuthSession);
+
+ final List 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 ce2d3405ddc68..2784f46a96c2a 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.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;
}
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 c87f62f944999..61e7c8922f392 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 @@ import android.os.RemoteException;
* It may be possible at some point in the future to combine IServiceReceivers 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 e8c969741fc2a..6b000f39aba26 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.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 mSensors;
+ @NonNull private List mSensors;
+ @NonNull private List 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 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 {
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 c890c52f2181b..24e7d7d19e8df 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 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