diff options
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 */); } |