summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl2
-rw-r--r--packages/SystemUI/res/layout/auth_biometric_contents.xml25
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java99
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java69
-rw-r--r--services/core/java/com/android/server/biometrics/AuthSession.java130
-rw-r--r--services/core/java/com/android/server/biometrics/BiometricService.java44
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java30
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java85
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java92
15 files changed, 498 insertions, 115 deletions
diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 7a006c34e1e1..492ceebe4d06 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 2439fedd0518..aed067c30253 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" />
- <!-- Negative Button -->
+
+ <!-- Negative Button, reserved for app -->
<Button android:id="@+id/button_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -97,13 +98,32 @@
android:ellipsize="end"
android:maxLines="2"
android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"/>
+ <!-- Cancel Button, replaces negative button when biometric is accepted -->
+ <Button android:id="@+id/button_cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:text="@string/cancel"
+ android:visibility="gone"/>
+ <!-- "Use Credential" Button, replaces if device credential is allowed -->
+ <Button android:id="@+id/button_use_credential"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:visibility="gone"/>
+
<Space android:id="@+id/middleSpacer"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="visible"/>
+
<!-- Positive Button -->
- <Button android:id="@+id/button_positive"
+ <Button android:id="@+id/button_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@*android:style/Widget.DeviceDefault.Button.Colored"
@@ -124,6 +144,7 @@
android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
android:text="@string/biometric_dialog_try_again"
android:visibility="gone"/>
+
<Space android:id="@+id/rightSpacer"
android:layout_width="8dp"
android:layout_height="match_parent"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
index 9a40541ac754..e4f6d6cc6887 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
@@ -196,7 +196,7 @@ public class AuthBiometricFaceView extends AuthBiometricView {
public void onAuthenticationFailed(String failureReason) {
if (getSize() == AuthDialog.SIZE_MEDIUM) {
mTryAgainButton.setVisibility(View.VISIBLE);
- mPositiveButton.setVisibility(View.GONE);
+ mConfirmButton.setVisibility(View.GONE);
}
// Do this last since wa want to know if the button is being animated (in the case of
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 0608ca236f86..c748ab21b822 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -116,8 +116,16 @@ public abstract class AuthBiometricView extends LinearLayout {
return mBiometricView.findViewById(R.id.button_negative);
}
- public Button getPositiveButton() {
- return mBiometricView.findViewById(R.id.button_positive);
+ public Button getCancelButton() {
+ return mBiometricView.findViewById(R.id.button_cancel);
+ }
+
+ public Button getUseCredentialButton() {
+ return mBiometricView.findViewById(R.id.button_use_credential);
+ }
+
+ public Button getConfirmButton() {
+ return mBiometricView.findViewById(R.id.button_confirm);
}
public Button getTryAgainButton() {
@@ -176,8 +184,16 @@ public abstract class AuthBiometricView extends LinearLayout {
private View mIconHolderView;
protected ImageView mIconView;
@VisibleForTesting protected TextView mIndicatorView;
+
+ // Negative button position, exclusively for the app-specified behavior
@VisibleForTesting Button mNegativeButton;
- @VisibleForTesting Button mPositiveButton;
+ // Negative button position, exclusively for cancelling auth after passive auth success
+ @VisibleForTesting Button mCancelButton;
+ // Negative button position, shown if device credentials are allowed
+ @VisibleForTesting Button mUseCredentialButton;
+
+ // Positive button position,
+ @VisibleForTesting Button mConfirmButton;
@VisibleForTesting Button mTryAgainButton;
// Measurements when biometric view is showing text, buttons, etc.
@@ -303,6 +319,7 @@ public abstract class AuthBiometricView extends LinearLayout {
mDescriptionView.setVisibility(View.GONE);
mIndicatorView.setVisibility(View.GONE);
mNegativeButton.setVisibility(View.GONE);
+ mUseCredentialButton.setVisibility(View.GONE);
final float iconPadding = getResources()
.getDimension(R.dimen.biometric_dialog_icon_padding);
@@ -336,6 +353,7 @@ public abstract class AuthBiometricView extends LinearLayout {
mTitleView.setAlpha(opacity);
mIndicatorView.setAlpha(opacity);
mNegativeButton.setAlpha(opacity);
+ mCancelButton.setAlpha(opacity);
mTryAgainButton.setAlpha(opacity);
if (!TextUtils.isEmpty(mSubtitleView.getText())) {
@@ -355,7 +373,12 @@ public abstract class AuthBiometricView extends LinearLayout {
super.onAnimationStart(animation);
mTitleView.setVisibility(View.VISIBLE);
mIndicatorView.setVisibility(View.VISIBLE);
- mNegativeButton.setVisibility(View.VISIBLE);
+
+ if (isDeviceCredentialAllowed()) {
+ mUseCredentialButton.setVisibility(View.VISIBLE);
+ } else {
+ mNegativeButton.setVisibility(View.VISIBLE);
+ }
mTryAgainButton.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(mSubtitleView.getText())) {
@@ -447,15 +470,17 @@ public abstract class AuthBiometricView extends LinearLayout {
case STATE_AUTHENTICATING:
removePendingAnimations();
if (mRequireConfirmation) {
- mPositiveButton.setEnabled(false);
- mPositiveButton.setVisibility(View.VISIBLE);
+ mConfirmButton.setEnabled(false);
+ mConfirmButton.setVisibility(View.VISIBLE);
}
break;
case STATE_AUTHENTICATED:
if (mSize != AuthDialog.SIZE_SMALL) {
- mPositiveButton.setVisibility(View.GONE);
+ mConfirmButton.setVisibility(View.GONE);
mNegativeButton.setVisibility(View.GONE);
+ mUseCredentialButton.setVisibility(View.GONE);
+ mCancelButton.setVisibility(View.GONE);
mIndicatorView.setVisibility(View.INVISIBLE);
}
announceForAccessibility(getResources()
@@ -466,10 +491,11 @@ public abstract class AuthBiometricView extends LinearLayout {
case STATE_PENDING_CONFIRMATION:
removePendingAnimations();
- mNegativeButton.setText(R.string.cancel);
- mNegativeButton.setContentDescription(getResources().getString(R.string.cancel));
- mPositiveButton.setEnabled(true);
- mPositiveButton.setVisibility(View.VISIBLE);
+ mNegativeButton.setVisibility(View.GONE);
+ mCancelButton.setVisibility(View.VISIBLE);
+ mUseCredentialButton.setVisibility(View.GONE);
+ mConfirmButton.setEnabled(true);
+ mConfirmButton.setVisibility(View.VISIBLE);
mIndicatorView.setTextColor(mTextColorHint);
mIndicatorView.setText(R.string.biometric_dialog_tap_confirm);
mIndicatorView.setVisibility(View.VISIBLE);
@@ -595,23 +621,29 @@ public abstract class AuthBiometricView extends LinearLayout {
mIconView = mInjector.getIconView();
mIconHolderView = mInjector.getIconHolderView();
mIndicatorView = mInjector.getIndicatorView();
+
+ // Negative-side (left) buttons
mNegativeButton = mInjector.getNegativeButton();
- mPositiveButton = mInjector.getPositiveButton();
+ mCancelButton = mInjector.getCancelButton();
+ mUseCredentialButton = mInjector.getUseCredentialButton();
+
+ // Positive-side (right) buttons
+ mConfirmButton = mInjector.getConfirmButton();
mTryAgainButton = mInjector.getTryAgainButton();
mNegativeButton.setOnClickListener((view) -> {
- 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);
+ });
+
+ mCancelButton.setOnClickListener((view) -> {
+ mCallback.onAction(Callback.ACTION_USER_CANCELED);
+ });
+
+ mUseCredentialButton.setOnClickListener((view) -> {
+ startTransitionToCredentialUI();
});
- mPositiveButton.setOnClickListener((view) -> {
+ 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 07e1f1b7f4c3..2b33f8cd036e 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 3f66c0812533..ecbe5f4d3ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -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) {
case AuthDialogCallback.DISMISSED_USER_CANCELED:
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index d3bd4fbd921c..d8d07e7dd24a 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 b907cdb54bbf..043bd5cd6ba5 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 e2517f27c0ea..49282ee360e2 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 3c18cd4ed231..637a8963402e 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<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();
}
}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 29424b438963..196582a6c0d6 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<FingerprintSensorPropertiesInternal> 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<FingerprintSensorPropertiesInternal> fpProps =
+ mInjector.getFingerprintSensorProperties(getContext());
+ pw.println("FingerprintSensorProperties: " + fpProps.size());
+ for (FingerprintSensorPropertiesInternal prop : fpProps) {
+ pw.println(" " + prop);
+ }
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index ce2d3405ddc6..2784f46a96c2 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 c87f62f94499..61e7c8922f39 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 I<Sensor>ServiceReceivers to share
* a common interface.
*/
-public final class ClientMonitorCallbackConverter {
+public class ClientMonitorCallbackConverter {
private IBiometricSensorReceiver mSensorReceiver; // BiometricService
private IFaceServiceReceiver mFaceServiceReceiver; // FaceManager
private IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManager
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index e8c969741fc2..6b000f39aba2 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<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 {
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 c890c52f2181..24e7d7d19e8d 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<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 */);
}