Start UDFPS sensors only after BiometricPrompt UI is showing

Since UDFPS sensors have a UI affordance, and the affordance is
controlled by FingerprintAuthenticationClient, start the prepared
sensor only after the BiometricPrompt dialog is done animating.

Plumbing-wise, adds:
1) Adds AuthSession.STATE_AUTH_STARTED_UI_SHOWING
2) Adds IBiometricSysuiReceiver#onDialogAnimatedIn

Since not all sensors are started immediately anymore, adds
logic in BiometricScheduler when attempting to cancel auth
for a sensor that's waiting for cookie (HAL still idle).

Fixes: 171931476
Test: atest com.android.server.biometrics
Test: atest com.android.systemui.biometrics

Change-Id: Iaedc26c51e274614ac2af6ebd98afe26263dad81
diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 7a006c3..492ceeb 100644
--- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
@@ -28,4 +28,6 @@
     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/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 07e1f1b..2b33f8c 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 @@
         }
         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 a6b1b90..5738327 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -202,6 +202,20 @@
     }
 
     @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 d3bd4fb..d8d07e7 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 @@
      * @param event
      */
     void onSystemEvent(int event);
+
+    /**
+     * Notifies when the dialog has finished animating in.
+     */
+    void onDialogAnimatedIn();
 }
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index 3c18cd4..637a896 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.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 @@
      */
     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 @@
     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 @@
     // 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 @@
         mCallingPid = callingPid;
         mCallingUserId = callingUserId;
         mDebugEnabled = debugEnabled;
+        mFingerprintSensorProperties = fingerprintSensorProperties;
 
         try {
             mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);
@@ -267,7 +293,10 @@
 
         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 @@
         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 @@
                 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 @@
         }
     }
 
+    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 @@
      */
     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 @@
      * @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 @@
      * {@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 3e0a40f..4d555e9 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.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 @@
     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 @@
                     break;
                 }
 
+                case MSG_ON_DIALOG_ANIMATED_IN: {
+                    handleOnDialogAnimatedIn();
+                    break;
+                }
+
                 default:
                     Slog.e(TAG, "Unknown message: " + msg);
                     break;
@@ -451,6 +459,11 @@
         public void onSystemEvent(int event) {
             mHandler.obtainMessage(MSG_ON_SYSTEM_EVENT, event).sendToTarget();
         }
+
+        @Override
+        public void onDialogAnimatedIn() {
+            mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget();
+        }
     };
 
     private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> {
@@ -763,6 +776,16 @@
         public DevicePolicyManager getDevicePolicyManager(Context context) {
             return context.getSystemService(DevicePolicyManager.class);
         }
+
+        public List<FingerprintSensorPropertiesInternal> getFingerprintSensorProperties(
+                Context context) {
+            final FingerprintManager fpm = context.getSystemService(FingerprintManager.class);
+            if (fpm != null) {
+                return fpm.getSensorPropertiesInternal();
+            } else {
+                return new ArrayList<>();
+            }
+        }
     }
 
     /**
@@ -946,7 +969,7 @@
 
     private void handleClientDied() {
         if (mCurrentAuthSession == null) {
-            Slog.e(TAG, "Auth session null");
+            Slog.e(TAG, "handleClientDied: AuthSession is null");
             return;
         }
 
@@ -957,6 +980,15 @@
         }
     }
 
+    private void handleOnDialogAnimatedIn() {
+        if (mCurrentAuthSession == null) {
+            Slog.e(TAG, "handleOnDialogAnimatedIn: AuthSession is null");
+            return;
+        }
+
+        mCurrentAuthSession.onDialogAnimatedIn();
+    }
+
     /**
      * Invoked when each service has notified that its client is ready to be started. When
      * all biometrics are ready, this invokes the SystemUI dialog through StatusBar.
@@ -1040,7 +1072,8 @@
         mCurrentAuthSession = new AuthSession(getContext(), mStatusBarService, mSysuiReceiver,
                 mKeyStore, mRandom, mClientDeathReceiver, preAuthInfo, token, operationId, userId,
                 mBiometricSensorReceiver, receiver, opPackageName, promptInfo, callingUid,
-                callingPid, callingUserId, debugEnabled);
+                callingPid, callingUserId, debugEnabled,
+                mInjector.getFingerprintSensorProperties(getContext()));
         try {
             mCurrentAuthSession.goToInitialState();
         } catch (RemoteException e) {
@@ -1067,5 +1100,12 @@
             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 ce2d340..2784f46 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.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 @@
             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 @@
             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 @@
                 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 c87f62f..61e7c89 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 @@
  * 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 e8c9697..6b000f3 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.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.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 @@
     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 @@
         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 @@
     }
 
     @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 @@
         }
     }
 
+    @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 @@
 
         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 @@
         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 @@
                 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 c890c52..24e7d7d 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 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 @@
 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 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mToken = new Binder();
         mScheduler = new BiometricScheduler(TAG, null /* gestureAvailabilityTracker */,
                 mBiometricService);
     }
@@ -63,8 +73,8 @@
     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 @@
         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 @@
     }
 
     @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 @@
         // 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 @@
         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 @@
         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 */);
         }