Merge changes from topic "sunfish-udfps"

* changes:
  Add anti burn-in logic for UdfpsView
  Update Fingerprint21, add Fingerprint21UdfpsMock
diff --git a/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl b/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
index 32530da..a57726c 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
@@ -25,4 +25,7 @@
 
     // Hides the overlay.
     void hideUdfpsOverlay();
+
+    // Shows debug messages on the UDFPS overlay.
+    void setDebugMessage(String message);
 }
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f99be88..613111d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4189,6 +4189,8 @@
     <string-array name="config_biometric_sensors" translatable="false" >
         <!-- <item>0:2:15</item>  ID0:Fingerprint:Strong -->
     </string-array>
+    <!--If true, allows the device to load udfps components on older HIDL implementations -->
+    <bool name="allow_test_udfps" translatable="false" >false</bool>
 
     <!-- Messages that should not be shown to the user during face auth enrollment. This should be
          used to hide messages that may be too chatty or messages that the user can't do much about.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 040fad5..781e7cf 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2506,6 +2506,7 @@
   <java-symbol type="string" name="face_error_security_update_required" />
 
   <java-symbol type="array" name="config_biometric_sensors" />
+  <java-symbol type="bool" name="allow_test_udfps" />
 
   <java-symbol type="array" name="config_face_acquire_enroll_ignorelist" />
   <java-symbol type="array" name="config_face_acquire_vendor_enroll_ignorelist" />
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 122fcb2..765a942 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1036,6 +1036,11 @@
     <!-- The maximum offset in either direction that icons move to prevent burn-in on AOD. -->
     <dimen name="default_burn_in_prevention_offset">15dp</dimen>
 
+    <!-- The maximum offset for the under-display fingerprint sensor (UDFPS) icon in either
+         direction that elements aer moved to prevent burn-in on AOD-->
+    <dimen name="udfps_burn_in_offset_x">8dp</dimen>
+    <dimen name="udfps_burn_in_offset_y">8dp</dimen>
+
     <dimen name="corner_size">8dp</dimen>
     <dimen name="top_padding">0dp</dimen>
     <dimen name="bottom_padding">48dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index ade106f..3cd4fca 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -49,6 +49,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
 import com.android.systemui.SystemUI;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 
 import java.util.List;
@@ -62,12 +64,13 @@
  */
 @Singleton
 public class AuthController extends SystemUI implements CommandQueue.Callbacks,
-        AuthDialogCallback {
+        AuthDialogCallback, DozeReceiver {
 
-    private static final String TAG = "BiometricPrompt/AuthController";
+    private static final String TAG = "AuthController";
     private static final boolean DEBUG = true;
 
     private final CommandQueue mCommandQueue;
+    private final StatusBarStateController mStatusBarStateController;
     private final Injector mInjector;
 
     // TODO: These should just be saved from onSaveState
@@ -77,6 +80,7 @@
 
     private Handler mHandler = new Handler(Looper.getMainLooper());
     private WindowManager mWindowManager;
+    @Nullable
     private UdfpsController mUdfpsController;
     @VisibleForTesting
     IActivityTaskManager mActivityTaskManager;
@@ -143,6 +147,13 @@
     };
 
     @Override
+    public void dozeTimeTick() {
+        if (mUdfpsController != null) {
+            mUdfpsController.dozeTimeTick();
+        }
+    }
+
+    @Override
     public void onTryAgainPressed() {
         if (mReceiver == null) {
             Log.e(TAG, "onTryAgainPressed: Receiver is null");
@@ -251,14 +262,17 @@
     }
 
     @Inject
-    public AuthController(Context context, CommandQueue commandQueue) {
-        this(context, commandQueue, new Injector());
+    public AuthController(Context context, CommandQueue commandQueue,
+            StatusBarStateController statusBarStateController) {
+        this(context, commandQueue, statusBarStateController, new Injector());
     }
 
     @VisibleForTesting
-    AuthController(Context context, CommandQueue commandQueue, Injector injector) {
+    AuthController(Context context, CommandQueue commandQueue,
+            StatusBarStateController statusBarStateController, Injector injector) {
         super(context);
         mCommandQueue = commandQueue;
+        mStatusBarStateController = statusBarStateController;
         mInjector = injector;
 
         IntentFilter filter = new IntentFilter();
@@ -280,7 +294,7 @@
                     fpm.getSensorProperties();
             for (FingerprintSensorProperties props : fingerprintSensorProperties) {
                 if (props.sensorType == FingerprintSensorProperties.TYPE_UDFPS) {
-                    mUdfpsController = new UdfpsController(mContext);
+                    mUdfpsController = new UdfpsController(mContext, mStatusBarStateController);
                     break;
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index d29a8fb..82fb808 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics;
 
+import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -29,6 +30,7 @@
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Spline;
@@ -38,6 +40,8 @@
 
 import com.android.internal.BrightnessSynchronizer;
 import com.android.systemui.R;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 
 import java.io.FileWriter;
 import java.io.IOException;
@@ -46,7 +50,7 @@
  * Shows and hides the under-display fingerprint sensor (UDFPS) overlay, handles UDFPS touch events,
  * and coordinates triggering of the high-brightness mode (HBM).
  */
-class UdfpsController {
+class UdfpsController implements DozeReceiver {
     private static final String TAG = "UdfpsController";
     // Gamma approximation for the sRGB color space.
     private static final float DISPLAY_GAMMA = 2.2f;
@@ -61,6 +65,7 @@
     private final String mHbmPath;
     private final String mHbmEnableCommand;
     private final String mHbmDisableCommand;
+    private final boolean mHbmSupported;
     // Brightness in nits in the high-brightness mode.
     private final float mHbmNits;
     // A spline mapping from the device's backlight value, normalized to the range [0, 1.0], to a
@@ -85,6 +90,11 @@
         public void hideUdfpsOverlay() {
             UdfpsController.this.hideUdfpsOverlay();
         }
+
+        @Override
+        public void setDebugMessage(String message) {
+            mView.setDebugMessage(message);
+        }
     }
 
     @SuppressLint("ClickableViewAccessibility")
@@ -116,7 +126,8 @@
         }
     };
 
-    UdfpsController(Context context) {
+    UdfpsController(@NonNull Context context,
+            @NonNull StatusBarStateController statusBarStateController) {
         mFingerprintManager = context.getSystemService(FingerprintManager.class);
         mWindowManager = context.getSystemService(WindowManager.class);
         mContentResolver = context.getContentResolver();
@@ -129,6 +140,10 @@
         mHbmEnableCommand = context.getResources().getString(R.string.udfps_hbm_enable_command);
         mHbmDisableCommand = context.getResources().getString(R.string.udfps_hbm_disable_command);
 
+        mHbmSupported = !TextUtils.isEmpty(mHbmPath);
+        mView.setHbmSupported(mHbmSupported);
+        statusBarStateController.addCallback(mView);
+
         // This range only consists of the minimum and maximum values, which only cover
         // non-high-brightness mode.
         float[] nitsRange = toFloatArray(context.getResources().obtainTypedArray(
@@ -162,6 +177,11 @@
         mIsOverlayShowing = false;
     }
 
+    @Override
+    public void dozeTimeTick() {
+        mView.dozeTimeTick();
+    }
+
     private void showUdfpsOverlay() {
         mHandler.post(() -> {
             if (!mIsOverlayShowing) {
@@ -224,9 +244,11 @@
         mView.setScrimAlpha(computeScrimOpacity());
         mView.showScrimAndDot();
         try {
-            FileWriter fw = new FileWriter(mHbmPath);
-            fw.write(mHbmEnableCommand);
-            fw.close();
+            if (mHbmSupported) {
+                FileWriter fw = new FileWriter(mHbmPath);
+                fw.write(mHbmEnableCommand);
+                fw.close();
+            }
             mFingerprintManager.onFingerDown(x, y, minor, major);
         } catch (IOException e) {
             mView.hideScrimAndDot();
@@ -238,13 +260,15 @@
         mFingerprintManager.onFingerUp();
         // Hiding the scrim before disabling HBM results in less noticeable flicker.
         mView.hideScrimAndDot();
-        try {
-            FileWriter fw = new FileWriter(mHbmPath);
-            fw.write(mHbmDisableCommand);
-            fw.close();
-        } catch (IOException e) {
-            mView.showScrimAndDot();
-            Log.e(TAG, "onFingerUp | failed to disable HBM: " + e.getMessage());
+        if (mHbmSupported) {
+            try {
+                FileWriter fw = new FileWriter(mHbmPath);
+                fw.write(mHbmDisableCommand);
+                fw.close();
+            } catch (IOException e) {
+                mView.showScrimAndDot();
+                Log.e(TAG, "onFingerUp | failed to disable HBM: " + e.getMessage());
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
index 45afc76..36353fa 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.biometrics;
 
+import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
@@ -23,25 +25,33 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.MathUtils;
 import android.view.View;
 import android.view.ViewTreeObserver;
 
 import com.android.systemui.R;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 
 /**
  * A full screen view with a configurable illumination dot and scrim.
  */
-public class UdfpsView extends View {
+public class UdfpsView extends View implements DozeReceiver,
+        StatusBarStateController.StateListener {
     private static final String TAG = "UdfpsView";
 
     // Values in pixels.
-    private static final float SENSOR_SHADOW_OFFSET = 2.0f;
+    private static final float SENSOR_SHADOW_RADIUS = 2.0f;
     private static final float SENSOR_OUTLINE_WIDTH = 2.0f;
 
+    private static final int DEBUG_TEXT_SIZE_PX = 32;
+
     private final Rect mScrimRect;
     private final Paint mScrimPaint;
+    private final Paint mDebugTextPaint;
 
     private float mSensorX;
     private float mSensorY;
@@ -50,11 +60,20 @@
     private final float mSensorRadius;
     private final float mSensorMarginBottom;
     private final float mSensorTouchAreaCoefficient;
+    private final int mMaxBurnInOffsetX;
+    private final int mMaxBurnInOffsetY;
 
     private final Rect mTouchableRegion;
     private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener;
 
+    // AOD anti-burn-in offsets
+    private float mInterpolatedDarkAmount;
+    private float mBurnInOffsetX;
+    private float mBurnInOffsetY;
+
     private boolean mIsScrimShowing;
+    private boolean mHbmSupported;
+    private String mDebugMessage;
 
     public UdfpsView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -80,18 +99,29 @@
             a.recycle();
         }
 
+        mMaxBurnInOffsetX = getResources()
+                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
+        mMaxBurnInOffsetY = getResources()
+                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
+
         mScrimRect = new Rect();
         mScrimPaint = new Paint(0 /* flags */);
         mScrimPaint.setColor(Color.BLACK);
 
         mSensorRect = new RectF();
         mSensorPaint = new Paint(0 /* flags */);
+        mSensorPaint.setAntiAlias(true);
         mSensorPaint.setColor(Color.WHITE);
         mSensorPaint.setStyle(Paint.Style.STROKE);
         mSensorPaint.setStrokeWidth(SENSOR_OUTLINE_WIDTH);
-        mSensorPaint.setShadowLayer(SENSOR_OUTLINE_WIDTH, 0, 0, Color.BLACK);
+        mSensorPaint.setShadowLayer(SENSOR_SHADOW_RADIUS, 0, 0, Color.BLACK);
         mSensorPaint.setAntiAlias(true);
 
+        mDebugTextPaint = new Paint();
+        mDebugTextPaint.setAntiAlias(true);
+        mDebugTextPaint.setColor(Color.BLUE);
+        mDebugTextPaint.setTextSize(DEBUG_TEXT_SIZE_PX);
+
         mTouchableRegion = new Rect();
         mInsetsListener = internalInsetsInfo -> {
             internalInsetsInfo.setTouchableInsets(
@@ -103,6 +133,29 @@
     }
 
     @Override
+    public void dozeTimeTick() {
+        updateAodPosition();
+    }
+
+    @Override
+    public void onDozeAmountChanged(float linear, float eased) {
+        mInterpolatedDarkAmount = eased;
+        updateAodPosition();
+    }
+
+    private void updateAodPosition() {
+        mBurnInOffsetX = MathUtils.lerp(0f,
+                getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
+                        - mMaxBurnInOffsetX,
+                mInterpolatedDarkAmount);
+        mBurnInOffsetY = MathUtils.lerp(0f,
+                getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
+                        - 0.5f * mMaxBurnInOffsetY,
+                mInterpolatedDarkAmount);
+        postInvalidate();
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         Log.v(TAG, "onAttachedToWindow");
@@ -131,10 +184,29 @@
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
-        if (mIsScrimShowing) {
+
+        if (mIsScrimShowing && mHbmSupported) {
+            // Only draw the scrim if HBM is supported.
             canvas.drawRect(mScrimRect, mScrimPaint);
         }
+
+        // Translation should affect everything but the scrim.
+        canvas.save();
+        canvas.translate(mBurnInOffsetX, mBurnInOffsetY);
+        if (!TextUtils.isEmpty(mDebugMessage)) {
+            canvas.drawText(mDebugMessage, 0, 160, mDebugTextPaint);
+        }
         canvas.drawOval(mSensorRect, mSensorPaint);
+        canvas.restore();
+    }
+
+    void setHbmSupported(boolean hbmSupported) {
+        mHbmSupported = hbmSupported;
+    }
+
+    void setDebugMessage(String message) {
+        mDebugMessage = message;
+        postInvalidate();
     }
 
     boolean isValidTouch(float x, float y, float pressure) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index de74d4e..5aca0df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -31,6 +31,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.doze.DozeReceiver;
@@ -88,6 +89,7 @@
     private final NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
     private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
     private final LockscreenLockIconController mLockscreenLockIconController;
+    private final AuthController mAuthController;
     private NotificationIconAreaController mNotificationIconAreaController;
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private NotificationPanelViewController mNotificationPanel;
@@ -110,7 +112,8 @@
             PulseExpansionHandler pulseExpansionHandler,
             NotificationShadeWindowController notificationShadeWindowController,
             NotificationWakeUpCoordinator notificationWakeUpCoordinator,
-            LockscreenLockIconController lockscreenLockIconController) {
+            LockscreenLockIconController lockscreenLockIconController,
+            AuthController authController) {
         super();
         mDozeLog = dozeLog;
         mPowerManager = powerManager;
@@ -130,6 +133,7 @@
         mNotificationShadeWindowController = notificationShadeWindowController;
         mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
         mLockscreenLockIconController = lockscreenLockIconController;
+        mAuthController = authController;
     }
 
     // TODO: we should try to not pass status bar in here if we can avoid it.
@@ -297,6 +301,7 @@
     @Override
     public void dozeTimeTick() {
         mNotificationPanel.dozeTimeTick();
+        mAuthController.dozeTimeTick();
         if (mAmbientIndicationContainer instanceof DozeReceiver) {
             ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
         }
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 d4a94c5..c8566c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -54,6 +54,7 @@
 
 import com.android.internal.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 
 import org.junit.Before;
@@ -103,8 +104,8 @@
         when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
         when(mDialog2.isAllowDeviceCredentials()).thenReturn(false);
 
-        mAuthController = new TestableAuthController(
-                context, mock(CommandQueue.class), new MockInjector());
+        mAuthController = new TestableAuthController(context, mock(CommandQueue.class),
+                mock(StatusBarStateController.class), new MockInjector());
 
         mAuthController.start();
     }
@@ -502,8 +503,9 @@
         private int mBuildCount = 0;
         private PromptInfo mLastBiometricPromptInfo;
 
-        TestableAuthController(Context context, CommandQueue commandQueue, Injector injector) {
-            super(context, commandQueue, injector);
+        TestableAuthController(Context context, CommandQueue commandQueue,
+                StatusBarStateController statusBarStateController, Injector injector) {
+            super(context, commandQueue, statusBarStateController, injector);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index a5f4e51..713a7c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -36,6 +36,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -90,6 +91,7 @@
     @Mock private View mAmbientIndicationContainer;
     @Mock private BiometricUnlockController mBiometricUnlockController;
     @Mock private LockscreenLockIconController mLockscreenLockIconController;
+    @Mock private AuthController mAuthController;
 
     @Before
     public void setup() {
@@ -100,7 +102,7 @@
                 mKeyguardViewMediator, () -> mAssistManager, mDozeScrimController,
                 mKeyguardUpdateMonitor, mVisualStabilityManager, mPulseExpansionHandler,
                 mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
-                mLockscreenLockIconController);
+                mLockscreenLockIconController, mAuthController);
 
         mDozeServiceHost.initialize(mStatusBar, mNotificationIconAreaController,
                 mStatusBarKeyguardViewManager, mNotificationShadeWindowViewController,
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index e87c8fe..73fc17a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -45,7 +45,7 @@
     private final PowerManager mPowerManager;
     private final VibrationEffect mSuccessVibrationEffect;
     private final VibrationEffect mErrorVibrationEffect;
-    private boolean mShouldSendErrorToClient;
+    private boolean mShouldSendErrorToClient = true;
     private boolean mAlreadyCancelled;
 
     /**
@@ -85,11 +85,11 @@
         // case (success, failure, or error) is received from the HAL (e.g. versions of fingerprint
         // that do not handle lockout under the HAL. In these cases, ensure that the framework only
         // sends errors once per ClientMonitor.
-        if (!mShouldSendErrorToClient) {
+        if (mShouldSendErrorToClient) {
             logOnError(getContext(), errorCode, vendorCode, getTargetUserId());
             try {
                 if (getListener() != null) {
-                    mShouldSendErrorToClient = true;
+                    mShouldSendErrorToClient = false;
                     getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
                 }
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 47c839c..9128359 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -99,6 +99,14 @@
         return getCookie() != 0;
     }
 
+    public long getOperationId() {
+        return mOperationId;
+    }
+
+    public boolean isRestricted() {
+        return mIsRestricted;
+    }
+
     @Override
     protected boolean isCryptoOperation() {
         return mOperationId != 0;
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 fffc073..588e865 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -175,7 +175,7 @@
     @Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
     @NonNull private final IBiometricService mBiometricService;
     @NonNull private final Handler mHandler = new Handler(Looper.getMainLooper());
-    @NonNull protected final InternalCallback mInternalCallback;
+    @NonNull private final InternalCallback mInternalCallback;
     @NonNull private final Queue<Operation> mPendingOperations;
     @Nullable private Operation mCurrentOperation;
     @NonNull private final ArrayDeque<CrashState> mCrashStates;
@@ -185,7 +185,15 @@
     // starting the next client).
     public class InternalCallback implements ClientMonitor.Callback {
         @Override
-        public void onClientFinished(ClientMonitor<?> clientMonitor, boolean success) {
+        public void onClientStarted(@NonNull ClientMonitor<?> clientMonitor) {
+            Slog.d(getTag(), "[Started] " + clientMonitor);
+            if (mCurrentOperation.mClientCallback != null) {
+                mCurrentOperation.mClientCallback.onClientStarted(clientMonitor);
+            }
+        }
+
+        @Override
+        public void onClientFinished(@NonNull ClientMonitor<?> clientMonitor, boolean success) {
             mHandler.post(() -> {
                 if (mCurrentOperation == null) {
                     Slog.e(getTag(), "[Finishing] " + clientMonitor
@@ -235,6 +243,14 @@
         mCrashStates = new ArrayDeque<>();
     }
 
+    /**
+     * @return A reference to the internal callback that should be invoked whenever the scheduler
+     *         needs to (e.g. client started, client finished).
+     */
+    @NonNull protected InternalCallback getInternalCallback() {
+        return mInternalCallback;
+    }
+
     private String getTag() {
         return BASE_TAG + "/" + mBiometricTag;
     }
@@ -262,7 +278,7 @@
             }
 
             final Interruptable interruptable = (Interruptable) currentClient;
-            interruptable.cancelWithoutStarting(mInternalCallback);
+            interruptable.cancelWithoutStarting(getInternalCallback());
             // Now we wait for the client to send its FinishCallback, which kicks off the next
             // operation.
             return;
@@ -280,10 +296,7 @@
         final boolean shouldStartNow = currentClient.getCookie() == 0;
         if (shouldStartNow) {
             Slog.d(getTag(), "[Starting] " + mCurrentOperation);
-            if (mCurrentOperation.mClientCallback != null) {
-                mCurrentOperation.mClientCallback.onClientStarted(currentClient);
-            }
-            currentClient.start(mInternalCallback);
+            currentClient.start(getInternalCallback());
             mCurrentOperation.state = Operation.STATE_STARTED;
         } else {
             try {
@@ -326,11 +339,8 @@
         }
 
         Slog.d(getTag(), "[Starting] Prepared client: " + mCurrentOperation);
-        if (mCurrentOperation.mClientCallback != null) {
-            mCurrentOperation.mClientCallback.onClientStarted(mCurrentOperation.clientMonitor);
-        }
         mCurrentOperation.state = Operation.STATE_STARTED;
-        mCurrentOperation.clientMonitor.start(mInternalCallback);
+        mCurrentOperation.clientMonitor.start(getInternalCallback());
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java
index 81867b0..dec40e3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java
@@ -144,6 +144,7 @@
      */
     public void start(@NonNull Callback callback) {
         mCallback = callback;
+        mCallback.onClientStarted(this);
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java b/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java
index 1a4216f..d85ab25 100644
--- a/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java
@@ -58,6 +58,10 @@
         mStatsClient = statsClient;
     }
 
+    public int getStatsClient() {
+        return mStatsClient;
+    }
+
     private boolean isAnyFieldUnknown() {
         return mStatsModality == BiometricsProtoEnums.MODALITY_UNKNOWN
                 || mStatsAction == BiometricsProtoEnums.ACTION_UNKNOWN
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java
index 20a1807..a12b54c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java
@@ -84,7 +84,7 @@
     private static final String TAG = "Fingerprint21";
     private static final int ENROLL_TIMEOUT_SEC = 60;
 
-    private final Context mContext;
+    final Context mContext;
     private final IActivityTaskManager mActivityTaskManager;
     private final FingerprintSensorProperties mSensorProperties;
     private final BiometricScheduler mScheduler;
@@ -96,6 +96,7 @@
     private final Map<Integer, Long> mAuthenticatorIds;
 
     @Nullable private IBiometricsFingerprint mDaemon;
+    @NonNull private final HalResultController mHalResultController;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     private int mCurrentUserId = UserHandle.USER_NULL;
 
@@ -146,15 +147,37 @@
         }
     };
 
-    private final IBiometricsFingerprintClientCallback mDaemonCallback =
-            new IBiometricsFingerprintClientCallback.Stub() {
+    public static class HalResultController extends IBiometricsFingerprintClientCallback.Stub {
+
+        /**
+         * Interface to sends results to the HalResultController's owner.
+         */
+        public interface Callback {
+            /**
+             * Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
+             */
+            void onHardwareUnavailable();
+        }
+
+        @NonNull private final Context mContext;
+        @NonNull final Handler mHandler;
+        @NonNull final BiometricScheduler mScheduler;
+        @Nullable private Callback mCallback;
+
+        HalResultController(@NonNull Context context, @NonNull Handler handler,
+                @NonNull BiometricScheduler scheduler) {
+            mContext = context;
+            mHandler = handler;
+            mScheduler = scheduler;
+        }
+
+        public void setCallback(@Nullable Callback callback) {
+            mCallback = callback;
+        }
+
         @Override
         public void onEnrollResult(long deviceId, int fingerId, int groupId, int remaining) {
             mHandler.post(() -> {
-                final CharSequence name = FingerprintUtils.getInstance()
-                        .getUniqueName(mContext, mCurrentUserId);
-                final Fingerprint fingerprint = new Fingerprint(name, groupId, fingerId, deviceId);
-
                 final ClientMonitor<?> client = mScheduler.getCurrentClient();
                 if (!(client instanceof FingerprintEnrollClient)) {
                     Slog.e(TAG, "onEnrollResult for non-enroll client: "
@@ -162,6 +185,11 @@
                     return;
                 }
 
+                final int currentUserId = client.getTargetUserId();
+                final CharSequence name = FingerprintUtils.getInstance()
+                        .getUniqueName(mContext, currentUserId);
+                final Fingerprint fingerprint = new Fingerprint(name, groupId, fingerId, deviceId);
+
                 final FingerprintEnrollClient enrollClient = (FingerprintEnrollClient) client;
                 enrollClient.onEnrollResult(fingerprint, remaining);
             });
@@ -224,8 +252,9 @@
 
                 if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE) {
                     Slog.e(TAG, "Got ERROR_HW_UNAVAILABLE");
-                    mDaemon = null;
-                    mCurrentUserId = UserHandle.USER_NULL;
+                    if (mCallback != null) {
+                        mCallback.onHardwareUnavailable();
+                    }
                 }
             });
         }
@@ -262,20 +291,27 @@
                 enumerateConsumer.onEnumerationResult(fp, remaining);
             });
         }
-    };
+    }
 
-    Fingerprint21(@NonNull Context context, int sensorId,
+    Fingerprint21(@NonNull Context context, @NonNull BiometricScheduler scheduler,
+            @NonNull Handler handler, int sensorId,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+            @NonNull HalResultController controller) {
         mContext = context;
+        mScheduler = scheduler;
+        mHandler = handler;
         mActivityTaskManager = ActivityTaskManager.getService();
-        mHandler = new Handler(Looper.getMainLooper());
+
         mTaskStackListener = new BiometricTaskStackListener();
         mAuthenticatorIds = Collections.synchronizedMap(new HashMap<>());
         mLazyDaemon = Fingerprint21.this::getDaemon;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mLockoutTracker = new LockoutFrameworkImpl(context, mLockoutResetCallback);
-        mScheduler = new BiometricScheduler(TAG, gestureAvailabilityDispatcher);
+        mHalResultController = controller;
+        mHalResultController.setCallback(() -> {
+            mDaemon = null;
+            mCurrentUserId = UserHandle.USER_NULL;
+        });
 
         try {
             ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
@@ -303,6 +339,17 @@
         mSensorProperties = new FingerprintSensorProperties(sensorId, sensorType);
     }
 
+    static Fingerprint21 newInstance(@NonNull Context context, int sensorId,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final BiometricScheduler scheduler =
+                new BiometricScheduler(TAG, gestureAvailabilityDispatcher);
+        final HalResultController controller = new HalResultController(context, handler, scheduler);
+        return new Fingerprint21(context, scheduler, handler, sensorId, lockoutResetDispatcher,
+                controller);
+    }
+
     @Override
     public void serviceDied(long cookie) {
         Slog.e(TAG, "HAL died");
@@ -355,7 +402,7 @@
         // successfully set.
         long halId = 0;
         try {
-            halId = mDaemon.setNotify(mDaemonCallback);
+            halId = mDaemon.setNotify(mHalResultController);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to set callback for fingerprint HAL", e);
             mDaemon = null;
@@ -373,6 +420,9 @@
         return mDaemon;
     }
 
+    @Nullable IUdfpsOverlayController getUdfpsOverlayController() {
+        return mUdfpsOverlayController;
+    }
     @LockoutTracker.LockoutMode int getLockoutModeForUser(int userId) {
         return mLockoutTracker.getLockoutModeForUser(userId);
     }
@@ -492,7 +542,7 @@
 
     void scheduleAuthenticate(@NonNull IBinder token, long operationId, int userId, int cookie,
             @NonNull ClientMonitorCallbackConverter listener, @NonNull String opPackageName,
-            @Nullable Surface surface, boolean restricted, int statsClient) {
+            boolean restricted, int statsClient, boolean isKeyguard) {
         mHandler.post(() -> {
             scheduleUpdateActiveUserWithoutHandler(userId);
 
@@ -500,8 +550,8 @@
             final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient(
                     mContext, mLazyDaemon, token, listener, userId, operationId, restricted,
                     opPackageName, cookie, false /* requireConfirmation */,
-                    mSensorProperties.sensorId, isStrongBiometric, surface, statsClient,
-                    mTaskStackListener, mLockoutTracker, mUdfpsOverlayController);
+                    mSensorProperties.sensorId, isStrongBiometric, statsClient,
+                    mTaskStackListener, mLockoutTracker, mUdfpsOverlayController, isKeyguard);
             mScheduler.scheduleClientMonitor(client);
         });
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21UdfpsMock.java
new file mode 100644
index 0000000..c556774
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21UdfpsMock.java
@@ -0,0 +1,555 @@
+/*
+ * Copyright (C) 2020 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.server.biometrics.sensors.fingerprint;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.trust.TrustManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+
+import com.android.server.biometrics.sensors.AuthenticationConsumer;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.ClientMonitor;
+import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+/**
+ * A mockable/testable provider of the {@link android.hardware.biometrics.fingerprint.V2_3} HIDL
+ * interface. This class is intended simulate UDFPS logic for devices that do not have an actual
+ * fingerprint@2.3 HAL (where UDFPS starts to become supported)
+ *
+ * UDFPS "accept" can only happen within a set amount of time after a sensor authentication. This is
+ * specified by {@link MockHalResultController#AUTH_VALIDITY_MS}. Touches after this duration will
+ * be treated as "reject".
+ *
+ * This class provides framework logic to emulate, for testing only, the UDFPS functionalies below:
+ *
+ * 1) IF either A) the caller is keyguard, and the device is not in a trusted state (authenticated
+ *    via biometric sensor or unlocked with a trust agent {@see android.app.trust.TrustManager}, OR
+ *    B) the caller is not keyguard, and regardless of trusted state, AND (following applies to both
+ *    (A) and (B) above) {@link FingerprintManager#onFingerDown(int, int, float, float)} is
+ *    received, this class will respond with {@link AuthenticationCallback#onAuthenticationFailed()}
+ *    after a tunable flat_time + variance_time.
+ *
+ *    In the case above (1), callers must not receive a successful authentication event here because
+ *    the sensor has not actually been authenticated.
+ *
+ * 2) IF A) the caller is keyguard and the device is not in a trusted state, OR B) the caller is not
+ *    keyguard and regardless of trusted state, AND (following applies to both (A) and (B)) the
+ *    sensor is touched and the fingerprint is accepted by the HAL, and then
+ *    {@link FingerprintManager#onFingerDown(int, int, float, float)} is received, this class will
+ *    respond with {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)}
+ *    after a tunable flat_time + variance_time. Note that the authentication callback from the
+ *    sensor is held until {@link FingerprintManager#onFingerDown(int, int, float, float)} is
+ *    invoked.
+ *
+ *    In the case above (2), callers can receive a successful authentication callback because the
+ *    real sensor was authenticated. Note that even though the real sensor was touched, keyguard
+ *    fingerprint authentication does not put keyguard into a trusted state because the
+ *    authentication callback is held until onFingerDown was invoked. This allows callers such as
+ *    keyguard to simulate a realistic path.
+ *
+ * 3) IF the caller is keyguard AND the device in a trusted state and then
+ *    {@link FingerprintManager#onFingerDown(int, int, float, float)} is received, this class will
+ *    respond with {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)}
+ *    after a tunable flat_time + variance time.
+ *
+ *    In the case above (3), since the device is already unlockable via trust agent, it's fine to
+ *    simulate the successful auth path. Non-keyguard clients will fall into either (1) or (2)
+ *    above.
+ *
+ *  This class currently does not simulate false rejection. Conversely, this class relies on the
+ *  real hardware sensor so does not affect false acceptance.
+ */
+@SuppressWarnings("deprecation")
+public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManager.TrustListener {
+
+    private static final String TAG = "Fingerprint21UdfpsMock";
+
+    // Secure setting integer. If true, the system will load this class to enable udfps testing.
+    public static final String CONFIG_ENABLE_TEST_UDFPS =
+            "com.android.server.biometrics.sensors.fingerprint.test_udfps.enable";
+    // Secure setting integer. A fixed duration intended to simulate something like the duration
+    // required for image capture.
+    private static final String CONFIG_AUTH_DELAY_PT1 =
+            "com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_pt1";
+    // Secure setting integer. A fixed duration intended to simulate something like the duration
+    // required for template matching to complete.
+    private static final String CONFIG_AUTH_DELAY_PT2 =
+            "com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_pt2";
+    // Secure setting integer. A random value between [-randomness, randomness] will be added to the
+    // capture delay above for each accept/reject.
+    private static final String CONFIG_AUTH_DELAY_RANDOMNESS =
+            "com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_randomness";
+
+    private static final int DEFAULT_AUTH_DELAY_PT1_MS = 300;
+    private static final int DEFAULT_AUTH_DELAY_PT2_MS = 400;
+    private static final int DEFAULT_AUTH_DELAY_RANDOMNESS_MS = 100;
+
+    @NonNull private final TestableBiometricScheduler mScheduler;
+    @NonNull private final Handler mHandler;
+    @NonNull private final FingerprintSensorProperties mSensorProperties;
+    @NonNull private final MockHalResultController mMockHalResultController;
+    @NonNull private final TrustManager mTrustManager;
+    @NonNull private final SparseBooleanArray mUserHasTrust;
+    @NonNull private final Random mRandom;
+    @NonNull private final FakeRejectRunnable mFakeRejectRunnable;
+    @NonNull private final FakeAcceptRunnable mFakeAcceptRunnable;
+    @NonNull private final RestartAuthRunnable mRestartAuthRunnable;
+
+    private static class TestableBiometricScheduler extends BiometricScheduler {
+        @NonNull private final TestableInternalCallback mInternalCallback;
+        @NonNull private Fingerprint21UdfpsMock mFingerprint21;
+
+        TestableBiometricScheduler(@NonNull String tag,
+                @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+            super(tag, gestureAvailabilityDispatcher);
+            mInternalCallback = new TestableInternalCallback();
+        }
+
+        class TestableInternalCallback extends InternalCallback {
+            @Override
+            public void onClientStarted(ClientMonitor<?> clientMonitor) {
+                super.onClientStarted(clientMonitor);
+                Slog.d(TAG, "Client started: " + clientMonitor);
+                mFingerprint21.setDebugMessage("Started: " + clientMonitor);
+            }
+
+            @Override
+            public void onClientFinished(ClientMonitor<?> clientMonitor, boolean success) {
+                super.onClientFinished(clientMonitor, success);
+                Slog.d(TAG, "Client finished: " + clientMonitor);
+                mFingerprint21.setDebugMessage("Finished: " + clientMonitor);
+            }
+        }
+
+        void init(@NonNull Fingerprint21UdfpsMock fingerprint21) {
+            mFingerprint21 = fingerprint21;
+        }
+
+        /**
+         * Expose the internal finish callback so it can be used for testing
+         */
+        @Override
+        @NonNull protected InternalCallback getInternalCallback() {
+            return mInternalCallback;
+        }
+    }
+
+    /**
+     * All of the mocking/testing should happen in here. This way we don't need to modify the
+     * {@link com.android.server.biometrics.sensors.ClientMonitor} implementations and can run the
+     * real path there.
+     */
+    private static class MockHalResultController extends HalResultController {
+
+        // Duration for which a sensor authentication can be treated as UDFPS success.
+        private static final int AUTH_VALIDITY_MS = 10 * 1000; // 10 seconds
+
+        static class LastAuthArgs {
+            @NonNull final AuthenticationConsumer lastAuthenticatedClient;
+            final long deviceId;
+            final int fingerId;
+            final int groupId;
+            @Nullable final ArrayList<Byte> token;
+
+            LastAuthArgs(@NonNull AuthenticationConsumer authenticationConsumer, long deviceId,
+                    int fingerId, int groupId, @Nullable ArrayList<Byte> token) {
+                lastAuthenticatedClient = authenticationConsumer;
+                this.deviceId = deviceId;
+                this.fingerId = fingerId;
+                this.groupId = groupId;
+                if (token == null) {
+                    this.token = null;
+                } else {
+                    // Store a copy so the owner can be GC'd
+                    this.token = new ArrayList<>(token);
+                }
+            }
+        }
+
+        // Initialized after the constructor, but before it's ever used.
+        @NonNull private RestartAuthRunnable mRestartAuthRunnable;
+        @NonNull private Fingerprint21UdfpsMock mFingerprint21;
+        @Nullable private LastAuthArgs mLastAuthArgs;
+
+        MockHalResultController(@NonNull Context context, @NonNull Handler handler,
+                @NonNull BiometricScheduler scheduler) {
+            super(context, handler, scheduler);
+        }
+
+        void init(@NonNull RestartAuthRunnable restartAuthRunnable,
+                @NonNull Fingerprint21UdfpsMock fingerprint21) {
+            mRestartAuthRunnable = restartAuthRunnable;
+            mFingerprint21 = fingerprint21;
+        }
+
+        @Nullable AuthenticationConsumer getLastAuthenticatedClient() {
+            return mLastAuthArgs != null ? mLastAuthArgs.lastAuthenticatedClient : null;
+        }
+
+        /**
+         * Intercepts the HAL authentication and holds it until the UDFPS simulation decides
+         * that authentication finished.
+         */
+        @Override
+        public void onAuthenticated(long deviceId, int fingerId, int groupId,
+                ArrayList<Byte> token) {
+            mHandler.post(() -> {
+                final ClientMonitor<?> client = mScheduler.getCurrentClient();
+                if (!(client instanceof AuthenticationConsumer)) {
+                    Slog.e(TAG, "Non authentication consumer: " + client);
+                    return;
+                }
+
+                final boolean accepted = fingerId != 0;
+                if (accepted) {
+                    mFingerprint21.setDebugMessage("Finger accepted");
+                } else {
+                    mFingerprint21.setDebugMessage("Finger rejected");
+                }
+
+                final AuthenticationConsumer authenticationConsumer =
+                        (AuthenticationConsumer) client;
+                mLastAuthArgs = new LastAuthArgs(authenticationConsumer, deviceId, fingerId,
+                        groupId, token);
+
+                // Remove any existing restart runnbables since auth just started, and put a new
+                // one on the queue.
+                mHandler.removeCallbacks(mRestartAuthRunnable);
+                mRestartAuthRunnable.setLastAuthReference(authenticationConsumer);
+                mHandler.postDelayed(mRestartAuthRunnable, AUTH_VALIDITY_MS);
+            });
+        }
+
+        /**
+         * Calls through to the real interface and notifies clients of accept/reject.
+         */
+        void sendAuthenticated(long deviceId, int fingerId, int groupId,
+                ArrayList<Byte> token) {
+            Slog.d(TAG, "sendAuthenticated: " + (fingerId != 0));
+            mFingerprint21.setDebugMessage("Udfps match: " + (fingerId != 0));
+            super.onAuthenticated(deviceId, fingerId, groupId, token);
+        }
+    }
+
+    static Fingerprint21UdfpsMock newInstance(@NonNull Context context, int sensorId,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+        Slog.d(TAG, "Creating Fingerprint23Mock!");
+
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final TestableBiometricScheduler scheduler =
+                new TestableBiometricScheduler(TAG, gestureAvailabilityDispatcher);
+        final MockHalResultController controller =
+                new MockHalResultController(context, handler, scheduler);
+        return new Fingerprint21UdfpsMock(context, scheduler, handler, sensorId,
+                lockoutResetDispatcher, controller);
+    }
+
+    private static abstract class FakeFingerRunnable implements Runnable {
+        private long mFingerDownTime;
+        private int mCaptureDuration;
+
+        /**
+         * @param fingerDownTime System time when onFingerDown occurred
+         * @param captureDuration Duration that the finger needs to be down for
+         */
+        void setSimulationTime(long fingerDownTime, int captureDuration) {
+            mFingerDownTime = fingerDownTime;
+            mCaptureDuration = captureDuration;
+        }
+
+        @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+        boolean isImageCaptureComplete() {
+            return System.currentTimeMillis() - mFingerDownTime > mCaptureDuration;
+        }
+    }
+
+    private final class FakeRejectRunnable extends FakeFingerRunnable {
+        @Override
+        public void run() {
+            mMockHalResultController.sendAuthenticated(0, 0, 0, null);
+        }
+    }
+
+    private final class FakeAcceptRunnable extends FakeFingerRunnable {
+        @Override
+        public void run() {
+            if (mMockHalResultController.mLastAuthArgs == null) {
+                // This can happen if the user has trust agents enabled, which make lockscreen
+                // dismissable. Send a fake non-zero (accept) finger.
+                Slog.d(TAG, "Sending fake finger");
+                mMockHalResultController.sendAuthenticated(1 /* deviceId */,
+                        1 /* fingerId */, 1 /* groupId */, null /* token */);
+            } else {
+                mMockHalResultController.sendAuthenticated(
+                        mMockHalResultController.mLastAuthArgs.deviceId,
+                        mMockHalResultController.mLastAuthArgs.fingerId,
+                        mMockHalResultController.mLastAuthArgs.groupId,
+                        mMockHalResultController.mLastAuthArgs.token);
+            }
+        }
+    }
+
+    /**
+     * The fingerprint HAL allows multiple (5) fingerprint attempts per HIDL invocation of the
+     * authenticate method. However, valid fingerprint authentications are invalidated after
+     * {@link MockHalResultController#AUTH_VALIDITY_MS}, meaning UDFPS touches will be reported as
+     * rejects if touched after that duration. However, since a valid fingerprint was detected, the
+     * HAL and FingerprintService will not look for subsequent fingerprints.
+     *
+     * In order to keep the FingerprintManager API consistent (that multiple fingerprint attempts
+     * are allowed per auth lifecycle), we internally cancel and restart authentication so that the
+     * sensor is responsive again.
+     */
+    private static final class RestartAuthRunnable implements Runnable {
+        @NonNull private final Fingerprint21UdfpsMock mFingerprint21;
+        @NonNull private final TestableBiometricScheduler mScheduler;
+
+        // Store a reference to the auth consumer that should be invalidated.
+        private AuthenticationConsumer mLastAuthConsumer;
+
+        RestartAuthRunnable(@NonNull Fingerprint21UdfpsMock fingerprint21,
+                @NonNull TestableBiometricScheduler scheduler) {
+            mFingerprint21 = fingerprint21;
+            mScheduler = scheduler;
+        }
+
+        void setLastAuthReference(AuthenticationConsumer lastAuthConsumer) {
+            mLastAuthConsumer = lastAuthConsumer;
+        }
+
+        @Override
+        public void run() {
+            final ClientMonitor<?> client = mScheduler.getCurrentClient();
+
+            // We don't care about FingerprintDetectClient, since accept/rejects are both OK. UDFPS
+            // rejects will just simulate the path where non-enrolled fingers are presented.
+            if (!(client instanceof FingerprintAuthenticationClient)) {
+                Slog.e(TAG, "Non-FingerprintAuthenticationClient client: " + client);
+                return;
+            }
+
+            // Perhaps the runnable is stale, or the user stopped/started auth manually. Do not
+            // restart auth in this case.
+            if (client != mLastAuthConsumer) {
+                Slog.e(TAG, "Current client: " + client
+                        + " does not match mLastAuthConsumer: " + mLastAuthConsumer);
+                return;
+            }
+
+            Slog.d(TAG, "Restarting auth, current: " + client);
+            mFingerprint21.setDebugMessage("Auth timed out");
+
+            final FingerprintAuthenticationClient authClient =
+                    (FingerprintAuthenticationClient) client;
+            // Store the authClient parameters so it can be rescheduled
+            final IBinder token = client.getToken();
+            final long operationId = authClient.getOperationId();
+            final int user = client.getTargetUserId();
+            final int cookie = client.getCookie();
+            final ClientMonitorCallbackConverter listener = client.getListener();
+            final String opPackageName = client.getOwnerString();
+            final boolean restricted = authClient.isRestricted();
+            final int statsClient = client.getStatsClient();
+            final boolean isKeyguard = authClient.isKeyguard();
+
+            // Don't actually send cancel() to the HAL, since successful auth already finishes
+            // HAL authenticate() lifecycle. Just
+            mScheduler.getInternalCallback().onClientFinished(client, true /* success */);
+
+            // Schedule this only after we invoke onClientFinished for the previous client, so that
+            // internal preemption logic is not run.
+            mFingerprint21.scheduleAuthenticate(token, operationId, user, cookie,
+                    listener, opPackageName, restricted, statsClient, isKeyguard);
+        }
+    }
+
+    private Fingerprint21UdfpsMock(@NonNull Context context,
+            @NonNull TestableBiometricScheduler scheduler,
+            @NonNull Handler handler, int sensorId,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull MockHalResultController controller) {
+        super(context, scheduler, handler, sensorId, lockoutResetDispatcher, controller);
+        mScheduler = scheduler;
+        mScheduler.init(this);
+        mHandler = handler;
+        mSensorProperties = new FingerprintSensorProperties(sensorId,
+                FingerprintSensorProperties.TYPE_UDFPS);
+        mMockHalResultController = controller;
+        mUserHasTrust = new SparseBooleanArray();
+        mTrustManager = context.getSystemService(TrustManager.class);
+        mTrustManager.registerTrustListener(this);
+        mRandom = new Random();
+        mFakeRejectRunnable = new FakeRejectRunnable();
+        mFakeAcceptRunnable = new FakeAcceptRunnable();
+        mRestartAuthRunnable = new RestartAuthRunnable(this, mScheduler);
+
+        // We can't initialize this during MockHalresultController's constructor due to a circular
+        // dependency.
+        mMockHalResultController.init(mRestartAuthRunnable, this);
+    }
+
+    @Override
+    public void onTrustChanged(boolean enabled, int userId, int flags) {
+        mUserHasTrust.put(userId, enabled);
+    }
+
+    @Override
+    public void onTrustManagedChanged(boolean enabled, int userId) {
+
+    }
+
+    @Override
+    public void onTrustError(CharSequence message) {
+
+    }
+
+    @Override
+    @NonNull
+    FingerprintSensorProperties getFingerprintSensorProperties() {
+        return mSensorProperties;
+    }
+
+    @Override
+    void onFingerDown(int x, int y, float minor, float major) {
+        mHandler.post(() -> {
+            Slog.d(TAG, "onFingerDown");
+            final AuthenticationConsumer lastAuthenticatedConsumer =
+                    mMockHalResultController.getLastAuthenticatedClient();
+            final ClientMonitor<?> currentScheduledClient = mScheduler.getCurrentClient();
+
+            if (currentScheduledClient == null) {
+                Slog.d(TAG, "Not authenticating");
+                return;
+            }
+
+            mHandler.removeCallbacks(mFakeRejectRunnable);
+            mHandler.removeCallbacks(mFakeAcceptRunnable);
+
+            // The sensor was authenticated, is still the currently scheduled client, and the
+            // user touched the UDFPS affordance. Pretend that auth succeeded.
+            final boolean authenticatedClientIsCurrent = lastAuthenticatedConsumer != null
+                    && lastAuthenticatedConsumer == currentScheduledClient;
+            // User is unlocked on keyguard via Trust Agent
+            final boolean keyguardAndTrusted;
+            if (currentScheduledClient instanceof FingerprintAuthenticationClient) {
+                keyguardAndTrusted = ((FingerprintAuthenticationClient) currentScheduledClient)
+                        .isKeyguard()
+                        && mUserHasTrust.get(currentScheduledClient.getTargetUserId(), false);
+            } else {
+                keyguardAndTrusted = false;
+            }
+
+            final int captureDuration = getNewCaptureDuration();
+            final int matchingDuration = getMatchingDuration();
+            final int totalDuration = captureDuration + matchingDuration;
+            setDebugMessage("Duration: " + totalDuration
+                    + " (" + captureDuration + " + " + matchingDuration + ")");
+            if (authenticatedClientIsCurrent || keyguardAndTrusted) {
+                mFakeAcceptRunnable.setSimulationTime(System.currentTimeMillis(), captureDuration);
+                mHandler.postDelayed(mFakeAcceptRunnable, totalDuration);
+            } else if (currentScheduledClient instanceof AuthenticationConsumer) {
+                // Something is authenticating but authentication has not succeeded yet. Pretend
+                // that auth rejected.
+                mFakeRejectRunnable.setSimulationTime(System.currentTimeMillis(), captureDuration);
+                mHandler.postDelayed(mFakeRejectRunnable, totalDuration);
+            }
+        });
+    }
+
+    @Override
+    void onFingerUp() {
+        mHandler.post(() -> {
+            Slog.d(TAG, "onFingerUp");
+
+            // Only one of these can be on the handler at any given time (see onFingerDown). If
+            // image capture is not complete, send ACQUIRED_TOO_FAST and remove the runnable from
+            // the handler. Image capture (onFingerDown) needs to happen again.
+            if (mHandler.hasCallbacks(mFakeRejectRunnable)
+                    && !mFakeRejectRunnable.isImageCaptureComplete()) {
+                mHandler.removeCallbacks(mFakeRejectRunnable);
+                mMockHalResultController.onAcquired(0 /* deviceId */,
+                        FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST,
+                        0 /* vendorCode */);
+            } else if (mHandler.hasCallbacks(mFakeAcceptRunnable)
+                    && !mFakeAcceptRunnable.isImageCaptureComplete()) {
+                mHandler.removeCallbacks(mFakeAcceptRunnable);
+                mMockHalResultController.onAcquired(0 /* deviceId */,
+                        FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST,
+                        0 /* vendorCode */);
+            }
+        });
+    }
+
+    private int getNewCaptureDuration() {
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        final int captureTime = Settings.Secure.getIntForUser(contentResolver,
+                CONFIG_AUTH_DELAY_PT1,
+                DEFAULT_AUTH_DELAY_PT1_MS,
+                UserHandle.USER_CURRENT);
+        final int randomDelayRange = Settings.Secure.getIntForUser(contentResolver,
+                CONFIG_AUTH_DELAY_RANDOMNESS,
+                DEFAULT_AUTH_DELAY_RANDOMNESS_MS,
+                UserHandle.USER_CURRENT);
+        final int randomDelay = mRandom.nextInt(randomDelayRange * 2) - randomDelayRange;
+
+        // Must be at least 0
+        return Math.max(captureTime + randomDelay, 0);
+    }
+
+    private int getMatchingDuration() {
+        final int matchingTime = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                CONFIG_AUTH_DELAY_PT2,
+                DEFAULT_AUTH_DELAY_PT2_MS,
+                UserHandle.USER_CURRENT);
+
+        // Must be at least 0
+        return Math.max(matchingTime, 0);
+    }
+
+    private void setDebugMessage(String message) {
+        try {
+            final IUdfpsOverlayController controller = getUdfpsOverlayController();
+            // Things can happen before SysUI loads and sets the controller.
+            if (controller != null) {
+                Slog.d(TAG, "setDebugMessage: " + message);
+                controller.setDebugMessage(message);
+            }
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception when sending message: " + message, e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java
index 751df8e..99d348a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java
@@ -29,7 +29,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
-import android.view.Surface;
 
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -47,6 +46,7 @@
 
     private static final String TAG = "Biometrics/FingerprintAuthClient";
 
+    private final boolean mIsKeyguard;
     private final LockoutFrameworkImpl mLockoutFrameworkImpl;
     @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
 
@@ -54,16 +54,17 @@
             @NonNull LazyDaemon<IBiometricsFingerprint> lazyDaemon, @NonNull IBinder token,
             @NonNull ClientMonitorCallbackConverter listener, int targetUserId, long operationId,
             boolean restricted, @NonNull String owner, int cookie, boolean requireConfirmation,
-            int sensorId, boolean isStrongBiometric, @Nullable Surface surface, int statsClient,
+            int sensorId, boolean isStrongBiometric, int statsClient,
             @NonNull TaskStackListener taskStackListener,
             @NonNull LockoutFrameworkImpl lockoutTracker,
-            @Nullable IUdfpsOverlayController udfpsOverlayController) {
+            @Nullable IUdfpsOverlayController udfpsOverlayController, boolean isKeyguard) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, isStrongBiometric,
                 BiometricsProtoEnums.MODALITY_FINGERPRINT, statsClient, taskStackListener,
                 lockoutTracker);
         mLockoutFrameworkImpl = lockoutTracker;
         mUdfpsOverlayController = udfpsOverlayController;
+        mIsKeyguard = isKeyguard;
     }
 
     @Override
@@ -145,4 +146,8 @@
     public void onFingerUp() {
         UdfpsHelper.onFingerUp(getFreshDaemon());
     }
+
+    public boolean isKeyguard() {
+        return mIsKeyguard;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index e4387c9..be8528c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -38,14 +38,17 @@
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.NativeHandle;
 import android.os.Process;
 import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.EventLog;
 import android.util.Slog;
 import android.view.Surface;
 
+import com.android.internal.R;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
@@ -158,8 +161,8 @@
             final int statsClient = isKeyguard ? BiometricsProtoEnums.CLIENT_KEYGUARD
                     : BiometricsProtoEnums.CLIENT_FINGERPRINT_MANAGER;
             mFingerprint21.scheduleAuthenticate(token, operationId, userId, 0 /* cookie */,
-                    new ClientMonitorCallbackConverter(receiver), opPackageName, surface,
-                    restricted, statsClient);
+                    new ClientMonitorCallbackConverter(receiver), opPackageName,
+                    restricted, statsClient, isKeyguard);
         }
 
         @Override
@@ -193,8 +196,8 @@
 
             final boolean restricted = true; // BiometricPrompt is always restricted
             mFingerprint21.scheduleAuthenticate(token, operationId, userId, cookie,
-                    new ClientMonitorCallbackConverter(sensorReceiver), opPackageName, surface,
-                    restricted, BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT);
+                    new ClientMonitorCallbackConverter(sensorReceiver), opPackageName, restricted,
+                    BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT, false /* isKeyguard */);
         }
 
         @Override // Binder call
@@ -369,8 +372,18 @@
         @Override // Binder call
         public void initializeConfiguration(int sensorId) {
             Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
-            mFingerprint21 = new Fingerprint21(getContext(), sensorId, mLockoutResetDispatcher,
-                    mGestureAvailabilityDispatcher);
+
+            if ((Build.IS_USERDEBUG || Build.IS_ENG)
+                    && getContext().getResources().getBoolean(R.bool.allow_test_udfps)
+                    && Settings.Secure.getIntForUser(getContext().getContentResolver(),
+                    Fingerprint21UdfpsMock.CONFIG_ENABLE_TEST_UDFPS, 0 /* default */,
+                    UserHandle.USER_CURRENT) != 0) {
+                mFingerprint21 = Fingerprint21UdfpsMock.newInstance(getContext(), sensorId,
+                        mLockoutResetDispatcher, mGestureAvailabilityDispatcher);
+            } else {
+                mFingerprint21 = Fingerprint21.newInstance(getContext(), sensorId,
+                        mLockoutResetDispatcher, mGestureAvailabilityDispatcher);
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 6adff0d..0c85387 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -1156,7 +1156,7 @@
         }
 
         private void enforceListenerPermission() {
-            mContext.enforceCallingPermission(Manifest.permission.TRUST_LISTENER,
+            mContext.enforceCallingOrSelfPermission(Manifest.permission.TRUST_LISTENER,
                     "register trust listener");
         }