From fdc48a48c5fe648becd11798fd29918348d76b73 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Tue, 18 May 2021 17:24:02 -0700 Subject: [PATCH 1/2] Delay starting fingerprint sensor when using multi-sensor devices. Fix: 183951321 Test: atest com.android.server.biometrics Test: atest com.android.systemui.biometrics Test: manual with integration app Change-Id: Ib1e92278d695b0ce5b6f799f51007262ade31f05 --- .../hardware/biometrics/BiometricManager.java | 22 +- .../biometrics/IBiometricSysuiReceiver.aidl | 4 +- .../internal/statusbar/IStatusBar.aidl | 2 +- .../internal/statusbar/IStatusBarService.aidl | 2 +- core/proto/android/server/biometrics.proto | 14 ++ .../AuthBiometricFaceToFingerprintView.java | 12 ++ .../biometrics/AuthBiometricView.java | 11 +- .../biometrics/AuthContainerView.java | 55 +++-- .../systemui/biometrics/AuthController.java | 30 ++- .../biometrics/AuthDialogCallback.java | 7 +- .../systemui/statusbar/CommandQueue.java | 14 +- .../biometrics/AuthControllerTest.java | 8 +- .../systemui/statusbar/CommandQueueTest.java | 7 +- .../server/biometrics/AuthSession.java | 197 ++++++++++++++---- .../server/biometrics/BiometricService.java | 23 +- .../statusbar/StatusBarManagerService.java | 6 +- .../server/biometrics/AuthSessionTest.java | 60 ++++-- .../biometrics/BiometricServiceTest.java | 42 +++- 18 files changed, 411 insertions(+), 105 deletions(-) diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index f3a83422469ce..0ec508a8c9bf8 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -34,7 +34,6 @@ import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; -import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.util.Slog; @@ -97,6 +96,27 @@ public class BiometricManager { @Retention(RetentionPolicy.SOURCE) public @interface BiometricError {} + /** + * Single sensor or unspecified multi-sensor behavior (prefer an explicit choice if the + * device is multi-sensor). + * @hide + */ + public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0; + + /** + * Prefer the face sensor and fall back to fingerprint when needed. + * @hide + */ + public static final int BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT = 1; + + /** + * @hide + */ + @IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT, + BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT}) + @Retention(RetentionPolicy.SOURCE) + public @interface BiometricMultiSensorMode {} + /** * Types of authenticators, defined at a level of granularity supported by * {@link BiometricManager} and {@link BiometricPrompt}. diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl index 492ceebe4d066..5d9b5f3bcc056 100644 --- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl +++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl @@ -28,6 +28,8 @@ 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. + // Notifies that the dialog has finished animating. void onDialogAnimatedIn(); + // For multi-sensor devices, notifies that the fingerprint should start now. + void onStartFingerprintNow(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 5ba192814b31e..4d7139ca24fea 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -146,7 +146,7 @@ oneway interface IStatusBar // Used to show the authentication dialog (Biometrics, Device Credential) void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver, in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId, - String opPackageName, long operationId); + String opPackageName, long operationId, int multiSensorConfig); // Used to notify the authentication dialog that a biometric has been authenticated void onBiometricAuthenticated(); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index c8a91d8cee10e..6a8d9838fcd23 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -110,7 +110,7 @@ interface IStatusBarService // Used to show the authentication dialog (Biometrics, Device Credential) void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver, in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, - int userId, String opPackageName,long operationId); + int userId, String opPackageName, long operationId, int multiSensorConfig); // Used to notify the authentication dialog that a biometric has been authenticated void onBiometricAuthenticated(); diff --git a/core/proto/android/server/biometrics.proto b/core/proto/android/server/biometrics.proto index ac9e3e001d504..fc9da90ccd59d 100644 --- a/core/proto/android/server/biometrics.proto +++ b/core/proto/android/server/biometrics.proto @@ -91,9 +91,23 @@ message BiometricServiceStateProto { STATE_CLIENT_DIED_CANCELLING = 10; } + enum MultiSensorState { + // Initializing or not yet started. + MULTI_SENSOR_STATE_UNKNOWN = 0; + // Sensors are in the process of being transitioned and there is no active sensor. + MULTI_SENSOR_STATE_SWITCHING = 1; + // Face sensor is being used as the primary input. + MULTI_SENSOR_STATE_FACE_SCANNING = 2; + // Fingerprint sensor is being used as the primary input. + MULTI_SENSOR_STATE_FP_SCANNING = 3; + } + repeated SensorServiceStateProto sensor_service_states = 1; optional AuthSessionState auth_session_state = 2; + + // Additional session state information, when the device has multiple sensors. + optional MultiSensorState auth_session_multi_sensor_state = 3; } // Overall state for an instance of a Service, for example FingerprintService or diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java index 71260decf47a2..76929cfa07ab1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java @@ -118,6 +118,18 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { return false; } + @Override + @BiometricState + protected int getStateForAfterError() { + if (mActiveSensorType == TYPE_FACE) { + mHandler.post(() -> mCallback.onAction( + Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)); + return STATE_AUTHENTICATING; + } + + return super.getStateForAfterError(); + } + @Override @NonNull protected IconController getIconController() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index a40af70adb8f5..99050160f38f0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -99,6 +99,13 @@ public abstract class AuthBiometricView extends LinearLayout { int ACTION_BUTTON_TRY_AGAIN = 4; int ACTION_ERROR = 5; int ACTION_USE_DEVICE_CREDENTIAL = 6; + /** + * Notify the receiver to start the fingerprint sensor. + * + * This is only applicable to multi-sensor devices that need to delay fingerprint auth + * (i.e face -> fingerprint). + */ + int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7; /** * When an action has occurred. The caller will only invoke this when the callback should @@ -166,7 +173,7 @@ public abstract class AuthBiometricView extends LinearLayout { } private final Injector mInjector; - private final Handler mHandler; + protected final Handler mHandler; private final AccessibilityManager mAccessibilityManager; protected final int mTextColorError; protected final int mTextColorHint; @@ -199,7 +206,7 @@ public abstract class AuthBiometricView extends LinearLayout { // Measurements when biometric view is showing text, buttons, etc. @Nullable @VisibleForTesting AuthDialog.LayoutParams mLayoutParams; - private Callback mCallback; + protected Callback mCallback; protected @BiometricState int mState; private float mIconOriginalY; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 9e72310ede313..6f1a387db4966 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -16,6 +16,8 @@ package com.android.systemui.biometrics; +import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -121,6 +123,7 @@ public class AuthContainerView extends LinearLayout boolean mCredentialAllowed; boolean mSkipIntro; long mOperationId; + @BiometricMultiSensorMode int mMultiSensorConfig; } public static class Builder { @@ -166,6 +169,12 @@ public class AuthContainerView extends LinearLayout return this; } + /** The multi-sensor mode. */ + public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) { + mConfig.mMultiSensorConfig = multiSensorConfig; + return this; + } + public AuthContainerView build(int[] sensorIds, boolean credentialAllowed, @Nullable List fpProps, @Nullable List faceProps) { @@ -237,6 +246,9 @@ public class AuthContainerView extends LinearLayout addCredentialView(false /* animatePanel */, true /* animateContents */); }, mInjector.getAnimateCredentialStartDelayMs()); break; + case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR: + mConfig.mCallback.onStartFingerprintNow(); + break; default: Log.e(TAG, "Unhandled action: " + action); } @@ -316,21 +328,9 @@ public class AuthContainerView extends LinearLayout return; } } else if (sensorCount == 2) { - int fingerprintSensorId = -1; - int faceSensorId = -1; - for (final int sensorId : config.mSensorIds) { - if (Utils.containsSensorId(mFpProps, sensorId)) { - fingerprintSensorId = sensorId; - continue; - } else if (Utils.containsSensorId(mFaceProps, sensorId)) { - faceSensorId = sensorId; - continue; - } - - if (fingerprintSensorId != -1 && faceSensorId != -1) { - break; - } - } + final int[] allSensors = findFaceAndFingerprintSensors(); + final int faceSensorId = allSensors[0]; + final int fingerprintSensorId = allSensors[1]; if (fingerprintSensorId == -1 || faceSensorId == -1) { Log.e(TAG, "Missing fingerprint or face for dual-sensor config"); @@ -747,4 +747,29 @@ public class AuthContainerView extends LinearLayout lp.token = windowToken; return lp; } + + private boolean hasFaceAndFingerprintSensors() { + final int[] ids = findFaceAndFingerprintSensors(); + return ids[0] >= 0 && ids[1] >= 0; + } + + // returns [face, fingerprint] sensor ids (id is -1 if not present) + private int[] findFaceAndFingerprintSensors() { + int faceSensorId = -1; + int fingerprintSensorId = -1; + + for (final int sensorId : mConfig.mSensorIds) { + if (Utils.containsSensorId(mFpProps, sensorId)) { + fingerprintSensorId = sensorId; + } else if (Utils.containsSensorId(mFaceProps, sensorId)) { + faceSensorId = sensorId; + } + + if (fingerprintSensorId != -1 && faceSensorId != -1) { + break; + } + } + + return new int[] {faceSensorId, fingerprintSensorId}; + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 179b077a3714c..9b8184b6870fc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -19,6 +19,7 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; +import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import android.annotation.NonNull; import android.annotation.Nullable; @@ -242,6 +243,20 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } } + @Override + public void onStartFingerprintNow() { + if (mReceiver == null) { + Log.e(TAG, "onStartUdfpsNow: Receiver is null"); + return; + } + + try { + mReceiver.onStartFingerprintNow(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e); + } + } + @Override public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { switch (reason) { @@ -409,7 +424,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @Override public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver, int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, - int userId, String opPackageName, long operationId) { + int userId, String opPackageName, long operationId, + @BiometricMultiSensorMode int multiSensorConfig) { @Authenticators.Types final int authenticators = promptInfo.getAuthenticators(); if (DEBUG) { @@ -421,7 +437,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, + ", sensorIds: " + ids.toString() + ", credentialAllowed: " + credentialAllowed + ", requireConfirmation: " + requireConfirmation - + ", operationId: " + operationId); + + ", operationId: " + operationId + + ", multiSensorConfig: " + multiSensorConfig); } SomeArgs args = SomeArgs.obtain(); args.arg1 = promptInfo; @@ -432,6 +449,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, args.argi1 = userId; args.arg6 = opPackageName; args.arg7 = operationId; + args.argi2 = multiSensorConfig; boolean skipAnimation = false; if (mCurrentDialog != null) { @@ -562,6 +580,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, final int userId = args.argi1; final String opPackageName = (String) args.arg6; final long operationId = (long) args.arg7; + final @BiometricMultiSensorMode int multiSensorConfig = args.argi2; // Create a new dialog but do not replace the current one yet. final AuthDialog newDialog = buildDialog( @@ -572,7 +591,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, credentialAllowed, opPackageName, skipAnimation, - operationId); + operationId, + multiSensorConfig); if (newDialog == null) { Log.e(TAG, "Unsupported type configuration"); @@ -646,7 +666,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, boolean credentialAllowed, String opPackageName, - boolean skipIntro, long operationId) { + boolean skipIntro, long operationId, + @BiometricMultiSensorMode int multiSensorConfig) { return new AuthContainerView.Builder(mContext) .setCallback(this) .setPromptInfo(promptInfo) @@ -655,6 +676,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, .setOpPackageName(opPackageName) .setSkipIntro(skipIntro) .setOperationId(operationId) + .setMultiSensorConfig(multiSensorConfig) .build(sensorIds, credentialAllowed, mFpProps, mFaceProps); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index d8d07e7dd24ac..9f40ca7b03460 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -67,7 +67,12 @@ public interface AuthDialogCallback { void onSystemEvent(int event); /** - * Notifies when the dialog has finished animating in. + * Notifies when the dialog has finished animating. */ void onDialogAnimatedIn(); + + /** + * Notifies that the fingerprint sensor should be started now. + */ + void onStartFingerprintNow(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 6baacb931a689..56941b9c02e5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar; import static android.app.StatusBarManager.DISABLE2_NONE; import static android.app.StatusBarManager.DISABLE_NONE; +import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT; import static android.inputmethodservice.InputMethodService.IME_INVISIBLE; import static android.view.Display.DEFAULT_DISPLAY; @@ -289,7 +290,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< IBiometricSysuiReceiver receiver, int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId, String opPackageName, - long operationId) { + long operationId, @BiometricMultiSensorMode int multiSensorConfig) { } default void onBiometricAuthenticated() { @@ -838,17 +839,19 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< @Override public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver, int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, - int userId, String opPackageName, long operationId) { + int userId, String opPackageName, long operationId, + @BiometricMultiSensorMode int multiSensorConfig) { synchronized (mLock) { SomeArgs args = SomeArgs.obtain(); args.arg1 = promptInfo; args.arg2 = receiver; - args.arg3 = sensorIds; // - args.arg4 = credentialAllowed; // + args.arg3 = sensorIds; + args.arg4 = credentialAllowed; args.arg5 = requireConfirmation; args.argi1 = userId; args.arg6 = opPackageName; args.arg7 = operationId; + args.argi2 = multiSensorConfig; mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args) .sendToTarget(); } @@ -1303,7 +1306,8 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< (boolean) someArgs.arg5 /* requireConfirmation */, someArgs.argi1 /* userId */, (String) someArgs.arg6 /* opPackageName */, - (long) someArgs.arg7 /* operationId */); + (long) someArgs.arg7 /* operationId */, + someArgs.argi2 /* multiSensorConfig */); } someArgs.recycle(); break; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 1b464a9b971b4..2695dd00d5763 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -17,6 +17,7 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricManager.Authenticators; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; @@ -41,6 +42,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricSysuiReceiver; @@ -526,7 +528,8 @@ public class AuthControllerTest extends SysuiTestCase { true /* requireConfirmation */, 0 /* userId */, "testPackage", - 0 /* operationId */); + 0 /* operationId */, + BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT); } private PromptInfo createTestPromptInfo() { @@ -568,7 +571,8 @@ public class AuthControllerTest extends SysuiTestCase { @Override protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, boolean credentialAllowed, - String opPackageName, boolean skipIntro, long operationId) { + String opPackageName, boolean skipIntro, long operationId, + @BiometricManager.BiometricMultiSensorMode int multiSensorConfig) { mLastBiometricPromptInfo = promptInfo; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index 58738e734a064..d1b846fb5236d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.content.ComponentName; import android.graphics.Rect; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsHbmListener; @@ -423,13 +424,15 @@ public class CommandQueueTest extends SysuiTestCase { final int userId = 10; final String packageName = "test"; final long operationId = 1; + final int multiSensorConfig = BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds, - credentialAllowed, requireConfirmation , userId, packageName, operationId); + credentialAllowed, requireConfirmation , userId, packageName, operationId, + multiSensorConfig); waitForIdleSync(); verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds), eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(packageName), - eq(operationId)); + eq(operationId), eq(multiSensorConfig)); } @Test diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index e11878104db62..1e3eec87aedec 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -19,8 +19,25 @@ 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 static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; -import static com.android.server.biometrics.BiometricServiceStateProto.*; +import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FACE_SCANNING; +import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FP_SCANNING; +import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_SWITCHING; +import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_UNKNOWN; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PAUSED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PAUSED_RESUMING; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PENDING_CONFIRM; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_CLIENT_DIED_CANCELLING; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_ERROR_PENDING_SYSUI; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_SHOWING_DEVICE_CREDENTIAL; import android.annotation.IntDef; import android.annotation.NonNull; @@ -49,8 +66,10 @@ import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collection; import java.util.List; import java.util.Random; +import java.util.function.Function; /** * Class that defines the states of an authentication session invoked via @@ -78,6 +97,14 @@ public final class AuthSession implements IBinder.DeathRecipient { @Retention(RetentionPolicy.SOURCE) @interface SessionState {} + /** Defined in biometrics.proto */ + @IntDef({ + MULTI_SENSOR_STATE_UNKNOWN, + MULTI_SENSOR_STATE_FACE_SCANNING, + MULTI_SENSOR_STATE_FP_SCANNING}) + @Retention(RetentionPolicy.SOURCE) + @interface MultiSensorState {} + /** * Notify the holder of the AuthSession that the caller/client's binder has died. The * holder (BiometricService) should schedule {@link AuthSession#onClientDied()} to be run @@ -111,6 +138,9 @@ public final class AuthSession implements IBinder.DeathRecipient { // The current state, which can be either idle, called, or started private @SessionState int mState = STATE_AUTH_IDLE; + private @BiometricMultiSensorMode int mMultiSensorMode; + private @MultiSensorState int mMultiSensorState; + private int[] mSensors; // For explicit confirmation, do not send to keystore until the user has confirmed // the authentication. private byte[] mTokenEscrow; @@ -186,6 +216,9 @@ public final class AuthSession implements IBinder.DeathRecipient { // with the cookie. Once all cookies are received, we can show the prompt // and let the services start authenticating. The cookie should be non-zero. for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { + if (DEBUG) { + Slog.v(TAG, "set to unknown state sensor: " + sensor.id); + } sensor.goToStateUnknown(); } } @@ -194,6 +227,10 @@ public final class AuthSession implements IBinder.DeathRecipient { for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1; final boolean requireConfirmation = isConfirmationRequired(sensor); + + if (DEBUG) { + Slog.v(TAG, "waiting for cooking for sensor: " + sensor.id); + } sensor.goToStateWaitingForCookie(requireConfirmation, mToken, mOperationId, mUserId, mSensorReceiver, mOpPackageName, cookie, mPromptInfo.isAllowBackgroundAuthentication()); @@ -206,16 +243,20 @@ public final class AuthSession implements IBinder.DeathRecipient { // since LockSettingsService/Gatekeeper is always ready to check for credential. // SystemUI invokes that path. mState = STATE_SHOWING_DEVICE_CREDENTIAL; + mSensors = new int[0]; + mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT; + mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN; mStatusBarService.showAuthenticationDialog( mPromptInfo, mSysuiReceiver, - new int[0] /* sensorIds */, + mSensors /* sensorIds */, true /* credentialAllowed */, false /* requireConfirmation */, mUserId, mOpPackageName, - mOperationId); + mOperationId, + mMultiSensorMode); } else if (!mPreAuthInfo.eligibleSensors.isEmpty()) { // Some combination of biometric or biometric|credential is requested setSensorsToStateWaitingForCookie(); @@ -235,9 +276,9 @@ public final class AuthSession implements IBinder.DeathRecipient { if (allCookiesReceived()) { mStartTimeMs = System.currentTimeMillis(); - // 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(); + // Do not start fingerprint sensors until BiometricPrompt UI is shown. Otherwise, + // the affordance may be shown before the BP UI is finished animating in. + startAllPreparedSensorsExceptFingerprint(); // No need to request the UI if we're coming from the paused state. if (mState != STATE_AUTH_PAUSED_RESUMING) { @@ -245,19 +286,23 @@ public final class AuthSession implements IBinder.DeathRecipient { // If any sensor requires confirmation, request it to be shown. final boolean requireConfirmation = isConfirmationRequiredByAnyEligibleSensor(); - final int[] sensorIds = new int[mPreAuthInfo.eligibleSensors.size()]; + mSensors = new int[mPreAuthInfo.eligibleSensors.size()]; for (int i = 0; i < mPreAuthInfo.eligibleSensors.size(); i++) { - sensorIds[i] = mPreAuthInfo.eligibleSensors.get(i).id; + mSensors[i] = mPreAuthInfo.eligibleSensors.get(i).id; } + mMultiSensorMode = getMultiSensorModeForNewSession( + mPreAuthInfo.eligibleSensors); + mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN; mStatusBarService.showAuthenticationDialog(mPromptInfo, mSysuiReceiver, - sensorIds, + mSensors, mPreAuthInfo.shouldShowCredential(), requireConfirmation, mUserId, mOpPackageName, - mOperationId); + mOperationId, + mMultiSensorMode); mState = STATE_AUTH_STARTED; } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); @@ -266,7 +311,8 @@ public final class AuthSession implements IBinder.DeathRecipient { // The UI was already showing :) mState = STATE_AUTH_STARTED_UI_SHOWING; } - + } else { + Slog.v(TAG, "onCookieReceived: still waiting"); } } @@ -285,53 +331,50 @@ public final class AuthSession implements IBinder.DeathRecipient { return false; } - 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 startAllPreparedSensorsExceptFingerprint() { + startAllPreparedSensors(sensor -> sensor.modality != TYPE_FINGERPRINT); } - 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: " + sensor, e); - } - } + private void startAllPreparedFingerprintSensors() { + startAllPreparedSensors(sensor -> sensor.modality == TYPE_FINGERPRINT); } - private void startPreparedUdfpsSensors() { + private void startAllPreparedSensors(Function filter) { for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { - if (isUdfpsSensor(sensor)) { + if (filter.apply(sensor)) { try { + if (DEBUG) { + Slog.v(TAG, "Starting sensor: " + sensor.id); + } sensor.startSensor(); } catch (RemoteException e) { - Slog.e(TAG, "Unable to start UDFPS sensor: " + sensor, e); + Slog.e(TAG, "Unable to start prepared client, sensor: " + sensor, e); } } } } + private void cancelAllFingerprintSensors() { + cancelAllSensors(sensor -> sensor.modality == TYPE_FINGERPRINT); + } + private void cancelAllSensors() { + cancelAllSensors(sensor -> true); + } + + private void cancelAllSensors(Function filter) { // TODO: For multiple modalities, send a single ERROR_CANCELED only when all // drivers have canceled authentication. We'd probably have to add a state for // STATE_CANCELING for when we're waiting for final ERROR_CANCELED before // sending the final error callback to the application. for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { try { - sensor.goToStateCancelling(mToken, mOpPackageName); + if (filter.apply(sensor)) { + if (DEBUG) { + Slog.v(TAG, "Canceling sensor: " + sensor.id); + } + sensor.goToStateCancelling(mToken, mOpPackageName); + } } catch (RemoteException e) { Slog.e(TAG, "Unable to cancel authentication"); } @@ -374,16 +417,20 @@ public final class AuthSession implements IBinder.DeathRecipient { mPromptInfo.setAuthenticators(authenticators); mState = STATE_SHOWING_DEVICE_CREDENTIAL; + mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT; + mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN; + mSensors = new int[0]; mStatusBarService.showAuthenticationDialog( mPromptInfo, mSysuiReceiver, - new int[0] /* sensorIds */, + mSensors /* sensorIds */, true /* credentialAllowed */, false /* requireConfirmation */, mUserId, mOpPackageName, - mOperationId); + mOperationId, + mMultiSensorMode); } else { mClientReceiver.onError(modality, error, vendorCode); return true; @@ -406,6 +453,10 @@ public final class AuthSession implements IBinder.DeathRecipient { // a round trip to SystemUI. mClientReceiver.onError(modality, error, vendorCode); return true; + } else if (shouldErrorTriggerMultiSensorTransition()) { + // wait for the UI to signal when modality should switch + mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING; + Slog.d(TAG, "onErrorReceived: waiting for modality switch callback"); } else { mState = STATE_ERROR_PENDING_SYSUI; mStatusBarService.onBiometricError(modality, error, vendorCode); @@ -472,8 +523,36 @@ public final class AuthSession implements IBinder.DeathRecipient { mState = STATE_AUTH_STARTED_UI_SHOWING; - // For UDFPS devices, we can now start the sensor. - startPreparedUdfpsSensors(); + if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) { + mMultiSensorState = MULTI_SENSOR_STATE_FACE_SCANNING; + } else { + startFingerprintSensorsNow(); + } + } + + // call anytime after onDialogAnimatedIn() to indicate it's appropriate to start the + // fingerprint sensor (i.e. face auth has failed or is not available) + void onStartFingerprint() { + if (mMultiSensorMode != BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) { + Slog.e(TAG, "onStartFingerprint, unexpected mode: " + mMultiSensorMode); + return; + } + + if (mState != STATE_AUTH_STARTED + && mState != STATE_AUTH_STARTED_UI_SHOWING + && mState != STATE_AUTH_PAUSED) { + Slog.e(TAG, "onStartFingerprint, unexpected state: " + mState); + return; + } + + mMultiSensorState = MULTI_SENSOR_STATE_FP_SCANNING; + startFingerprintSensorsNow(); + } + + // unguarded helper for the above methods only + private void startFingerprintSensorsNow() { + startAllPreparedFingerprintSensors(); + mState = STATE_AUTH_STARTED_UI_SHOWING; } void onTryAgainPressed() { @@ -613,6 +692,7 @@ public final class AuthSession implements IBinder.DeathRecipient { + ", IsCrypto: " + isCrypto() + ", Action: " + BiometricsProtoEnums.ACTION_AUTHENTICATE + ", Client: " + BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT + + ", Reason: " + reason + ", Error: " + error + ", Latency: " + latency); } @@ -652,6 +732,7 @@ public final class AuthSession implements IBinder.DeathRecipient { } mClientReceiver.onAuthenticationSucceeded( Utils.getAuthenticationTypeForResult(reason)); + cancelBiometricOnly(); break; case BiometricPrompt.DISMISSED_REASON_NEGATIVE: @@ -729,6 +810,12 @@ public final class AuthSession implements IBinder.DeathRecipient { private void cancelBiometricOnly() { if (mState == STATE_AUTH_STARTED || mState == STATE_AUTH_STARTED_UI_SHOWING) { cancelAllSensors(); + } else if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) { + cancelAllFingerprintSensors(); + } else { + if (DEBUG) { + Slog.v(TAG, "nothing to cancel - wrong state: " + mState); + } } } @@ -808,6 +895,32 @@ public final class AuthSession implements IBinder.DeathRecipient { } } + private boolean shouldErrorTriggerMultiSensorTransition() { + if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) { + return mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING; + } + return false; + } + + @BiometricMultiSensorMode + private static int getMultiSensorModeForNewSession(Collection sensors) { + boolean hasFace = false; + boolean hasFingerprint = false; + + for (BiometricSensor sensor: sensors) { + if (sensor.modality == TYPE_FACE) { + hasFace = true; + } else if (sensor.modality == TYPE_FINGERPRINT) { + hasFingerprint = true; + } + } + + if (hasFace && hasFingerprint) { + return BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; + } + return BIOMETRIC_MULTI_SENSOR_DEFAULT; + } + @Override public String toString() { return "State: " + mState diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index a546a60e20efd..e8e25f16d5afb 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -104,6 +104,7 @@ public class BiometricService extends SystemService { 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 static final int MSG_ON_START_FINGERPRINT_NOW = 16; private final Injector mInjector; private final DevicePolicyManager mDevicePolicyManager; @@ -237,6 +238,11 @@ public class BiometricService extends SystemService { break; } + case MSG_ON_START_FINGERPRINT_NOW: { + handleOnStartFingerprintNow(); + break; + } + default: Slog.e(TAG, "Unknown message: " + msg); break; @@ -618,6 +624,11 @@ public class BiometricService extends SystemService { public void onDialogAnimatedIn() { mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget(); } + + @Override + public void onStartFingerprintNow() { + mHandler.obtainMessage(MSG_ON_START_FINGERPRINT_NOW).sendToTarget(); + } }; private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> { @@ -1284,6 +1295,7 @@ public class BiometricService extends SystemService { } private void handleOnDialogAnimatedIn() { + Slog.d(TAG, "handleOnDialogAnimatedIn"); if (mCurrentAuthSession == null) { Slog.e(TAG, "handleOnDialogAnimatedIn: AuthSession is null"); return; @@ -1292,6 +1304,16 @@ public class BiometricService extends SystemService { mCurrentAuthSession.onDialogAnimatedIn(); } + private void handleOnStartFingerprintNow() { + Slog.d(TAG, "handleOnStartFingerprintNow"); + if (mCurrentAuthSession == null) { + Slog.e(TAG, "handleOnStartFingerprintNow: AuthSession is null"); + return; + } + + mCurrentAuthSession.onStartFingerprint(); + } + /** * 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. @@ -1310,7 +1332,6 @@ public class BiometricService extends SystemService { private void handleAuthenticate(IBinder token, long operationId, int userId, IBiometricServiceReceiver receiver, String opPackageName, PromptInfo promptInfo) { - mHandler.post(() -> { try { final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index d6e75746c004a..8926af48783d6 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -18,6 +18,7 @@ package com.android.server.statusbar; import static android.app.StatusBarManager.DISABLE2_GLOBAL_ACTIONS; import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE; +import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import static android.view.Display.DEFAULT_DISPLAY; import android.Manifest; @@ -789,12 +790,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver, int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, - int userId, String opPackageName, long operationId) { + int userId, String opPackageName, long operationId, + @BiometricMultiSensorMode int multiSensorConfig) { enforceBiometricDialog(); if (mBar != null) { try { mBar.showAuthenticationDialog(promptInfo, receiver, sensorIds, credentialAllowed, - requireConfirmation, userId, opPackageName, operationId); + requireConfirmation, userId, opPackageName, operationId, multiSensorConfig); } catch (RemoteException ex) { } } 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 96bab61191549..4afb7dd2ba82e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -38,6 +38,7 @@ import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; import android.app.trust.ITrustManager; import android.content.Context; +import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricAuthenticator; @@ -170,21 +171,42 @@ public class AuthSessionTest { session.onCookieReceived(cookie2); assertTrue(session.allCookiesReceived()); + + // for multi-sensor face then fingerprint is the default policy for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { - verify(sensor.impl).startPreparedClient(eq(sensor.getCookie())); - assertEquals(BiometricSensor.STATE_AUTHENTICATING, sensor.getSensorState()); + if (sensor.modality == TYPE_FACE) { + verify(sensor.impl).startPreparedClient(eq(sensor.getCookie())); + assertEquals(BiometricSensor.STATE_AUTHENTICATING, sensor.getSensorState()); + } else if (sensor.modality == TYPE_FINGERPRINT) { + assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, sensor.getSensorState()); + } } } @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. + public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes() + throws Exception { setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); + testMultiAuth_fingerprintSensorStartsAfter(false /* fingerprintStartsAfterDelay */); + } + @Test + public void testMultiAuth_fingerprintSensorStartsAfterDialogAnimationCompletes() + throws Exception { + setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); + setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class)); + testMultiAuth_fingerprintSensorStartsAfter(true /* fingerprintStartsAfterDelay */); + } + + public void testMultiAuth_fingerprintSensorStartsAfter(boolean fingerprintStartsAfterDelay) + throws Exception { final long operationId = 123; final int userId = 10; + final int fingerprintSensorId = mSensors.stream() + .filter(s -> s.modality == TYPE_FINGERPRINT) + .map(s -> s.id) + .findFirst() + .orElse(-1); final AuthSession session = createAuthSession(mSensors, false /* checkDevicePolicyManager */, @@ -200,27 +222,37 @@ public class AuthSessionTest { 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_WAITING_FOR_COOKIE, sensor.getSensorState()); + session.onCookieReceived( + session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie()); + if (fingerprintSensorId == sensor.id) { assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, sensor.getSensorState()); } else { - assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); + assertEquals(BiometricSensor.STATE_AUTHENTICATING, sensor.getSensorState()); } } assertTrue(session.allCookiesReceived()); - // UDFPS does not start even if all cookies are received + // fingerprint sensor does not start even if all cookies are received assertEquals(STATE_AUTH_STARTED, session.getState()); verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(), - anyBoolean(), anyBoolean(), anyInt(), any(), anyLong()); + anyBoolean(), anyBoolean(), anyInt(), any(), anyLong(), anyInt()); - // Notify AuthSession that the UI is shown. Then, UDFPS sensor should be started. + // Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started. session.onDialogAnimatedIn(); + if (fingerprintStartsAfterDelay) { + assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); + assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, + session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); + session.onErrorReceived(fingerprintSensorId, + session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getCookie(), + BiometricConstants.BIOMETRIC_ERROR_VENDOR, 0 /* vendorCode */); + session.onStartFingerprint(); + } assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); assertEquals(BiometricSensor.STATE_AUTHENTICATING, - session.mPreAuthInfo.eligibleSensors.get(0).getSensorState()); + session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index ec3bea33c8dbd..98777acc808fe 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -17,8 +17,17 @@ package com.android.server.biometrics; import static android.hardware.biometrics.BiometricManager.Authenticators; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; -import static com.android.server.biometrics.BiometricServiceStateProto.*; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PAUSED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PAUSED_RESUMING; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PENDING_CONFIRM; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_CLIENT_DIED_CANCELLING; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_ERROR_PENDING_SYSUI; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_SHOWING_DEVICE_CREDENTIAL; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -264,7 +273,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test @@ -348,7 +358,8 @@ public class BiometricServiceTest { eq(false) /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test @@ -465,6 +476,7 @@ public class BiometricServiceTest { assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); // startPreparedClient invoked + mBiometricService.mCurrentAuthSession.onDialogAnimatedIn(); verify(mBiometricService.mSensors.get(0).impl) .startPreparedClient(cookieCaptor.getValue()); @@ -477,7 +489,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); // Hardware authenticated final byte[] HAT = generateRandomHAT(); @@ -531,7 +544,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test @@ -692,7 +706,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, anyString(), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test @@ -791,7 +806,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test @@ -870,7 +886,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test @@ -1367,7 +1384,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); // Requesting strong and credential, when credential is setup resetReceivers(); @@ -1388,7 +1406,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); // Un-downgrading the authenticator allows successful strong auth for (BiometricSensor sensor : mBiometricService.mSensors) { @@ -1412,7 +1431,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + eq(BIOMETRIC_MULTI_SENSOR_DEFAULT)); } @Test(expected = IllegalStateException.class) From 47897b33f6d6ad8f7ca6cbd32782a0e5f0b980d7 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Wed, 19 May 2021 20:36:44 -0700 Subject: [PATCH 2/2] Remove confirm button when falling back to fingerprint auth. Fix: 186283063 Test: atest AuthBiometricFaceToFingerprintViewTest Test: manual (fail face auth and verify button disappers) Change-Id: I57f018e41b598fe8b2a9bccb37cdd369491e266b --- .../AuthBiometricFaceToFingerprintView.java | 18 +- .../biometrics/AuthBiometricFaceView.java | 6 + .../biometrics/AuthBiometricView.java | 2 +- ...uthBiometricFaceToFingerprintViewTest.java | 205 ++++++++++++++++++ 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java index 76929cfa07ab1..ebfd2068c55b4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java @@ -26,9 +26,11 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.util.AttributeSet; import android.util.Log; +import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; /** @@ -97,6 +99,11 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { super(context, attrs); } + @VisibleForTesting + AuthBiometricFaceToFingerprintView(Context context, AttributeSet attrs, Injector injector) { + super(context, attrs, injector); + } + void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { if (!sensorProps.isAnyUdfpsType()) { return; @@ -135,19 +142,28 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { protected IconController getIconController() { if (mActiveSensorType == TYPE_FINGERPRINT) { if (!(mIconController instanceof UdfpsIconController)) { - mIconController = new UdfpsIconController(getContext(), mIconView, mIndicatorView); + mIconController = createUdfpsIconController(); } return mIconController; } return super.getIconController(); } + @NonNull + protected IconController createUdfpsIconController() { + return new UdfpsIconController(getContext(), mIconView, mIndicatorView); + } + @Override public void updateState(int newState) { if (mState == STATE_HELP || mState == STATE_ERROR) { mActiveSensorType = TYPE_FINGERPRINT; + setRequireConfirmation(false); + mConfirmButton.setEnabled(false); + mConfirmButton.setVisibility(View.GONE); } + super.updateState(newState); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java index 4c5ca691939c4..f7d2d8c6d2ea9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java @@ -29,6 +29,7 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; public class AuthBiometricFaceView extends AuthBiometricView { @@ -148,6 +149,11 @@ public class AuthBiometricFaceView extends AuthBiometricView { super(context, attrs); } + @VisibleForTesting + AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector) { + super(context, attrs, injector); + } + @Override protected int getDelayAfterAuthenticatedDurationMs() { return HIDE_DELAY_MS; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index 99050160f38f0..1ac1df148000c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -612,7 +612,7 @@ public abstract class AuthBiometricView extends LinearLayout { mIndicatorView.setTextColor(mTextColorError); mIndicatorView.setVisibility(View.VISIBLE); mIndicatorView.setSelected(true); - mHandler.postDelayed(resetMessageRunnable, BiometricPrompt.HIDE_DIALOG_DELAY); + mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError()); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java new file mode 100644 index 0000000000000..5f3d3cb2f7d3f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase { + + @Mock AuthBiometricView.Callback mCallback; + + private AuthBiometricFaceToFingerprintView mFaceToFpView; + + @Mock private Button mNegativeButton; + @Mock private Button mCancelButton; + @Mock private Button mConfirmButton; + @Mock private Button mUseCredentialButton; + @Mock private Button mTryAgainButton; + + @Mock private TextView mTitleView; + @Mock private TextView mSubtitleView; + @Mock private TextView mDescriptionView; + @Mock private TextView mIndicatorView; + @Mock private ImageView mIconView; + @Mock private View mIconHolderView; + + @Mock private TextView mErrorView; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFaceToFpView = new TestableView(mContext); + mFaceToFpView.mIconController = mock(AuthBiometricFaceView.IconController.class); + mFaceToFpView.setCallback(mCallback); + + mFaceToFpView.mNegativeButton = mNegativeButton; + mFaceToFpView.mCancelButton = mCancelButton; + mFaceToFpView.mUseCredentialButton = mUseCredentialButton; + mFaceToFpView.mConfirmButton = mConfirmButton; + mFaceToFpView.mTryAgainButton = mTryAgainButton; + + mFaceToFpView.mIndicatorView = mErrorView; + } + + @Test + public void testStateUpdated_whenDialogAnimatedIn() { + mFaceToFpView.onDialogAnimatedIn(); + verify(mFaceToFpView.mIconController) + .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); + } + + @Test + public void testIconUpdatesState_whenDialogStateUpdated() { + mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING); + verify(mFaceToFpView.mIconController) + .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); + + mFaceToFpView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED); + verify(mFaceToFpView.mIconController).updateState( + eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING), + eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED)); + } + + @Test + public void testStateUpdated_whenSwitchToFingerprint() { + mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING); + verify(mFaceToFpView.mIconController) + .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); + + mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR); + mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING); + + InOrder order = inOrder(mFaceToFpView.mIconController); + order.verify(mFaceToFpView.mIconController).updateState( + eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING), + eq(AuthBiometricFaceToFingerprintView.STATE_ERROR)); + order.verify(mFaceToFpView.mIconController).updateState( + eq(AuthBiometricFaceToFingerprintView.STATE_ERROR), + eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); + + verify(mConfirmButton).setVisibility(eq(View.GONE)); + } + + public class TestableView extends AuthBiometricFaceToFingerprintView { + public TestableView(Context context) { + super(context, null, new MockInjector()); + } + + @Override + protected int getDelayAfterAuthenticatedDurationMs() { + return 0; + } + + @Override + protected IconController createUdfpsIconController() { + return mIconController; + } + } + + private class MockInjector extends AuthBiometricView.Injector { + @Override + public Button getNegativeButton() { + return mNegativeButton; + } + + @Override + public Button getCancelButton() { + return mCancelButton; + } + + @Override + public Button getUseCredentialButton() { + return mUseCredentialButton; + } + + @Override + public Button getConfirmButton() { + return mConfirmButton; + } + + @Override + public Button getTryAgainButton() { + return mTryAgainButton; + } + + @Override + public TextView getTitleView() { + return mTitleView; + } + + @Override + public TextView getSubtitleView() { + return mSubtitleView; + } + + @Override + public TextView getDescriptionView() { + return mDescriptionView; + } + + @Override + public TextView getIndicatorView() { + return mIndicatorView; + } + + @Override + public ImageView getIconView() { + return mIconView; + } + + @Override + public View getIconHolderView() { + return mIconHolderView; + } + + @Override + public int getDelayAfterError() { + return 0; + } + + @Override + public int getMediumToLargeAnimationDurationMs() { + return 0; + } + } +}