Show UDFPS icon on AOD even if fp auth isn't running

* shows udfps if it's enrolled on AOD even if:
   * device requires strong auth (ie: after reboot, lockdown)
   * device is unlocked via smart lock

On click of the udfps icon in the above cases:
  * If the device is locked, the bouncer will show.
  * If the device is unlocked, the user will enter the device.

Fixes: 198315404
Test: manual
Change-Id: Id766df8f77427ae1db672507716bd38dc3462452
Merged-In: Id766df8f77427ae1db672507716bd38dc3462452
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index d8346ab..8b78732 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -70,6 +70,19 @@
             android:padding="@dimen/lock_icon_padding"
             android:layout_gravity="center"
             android:scaleType="centerCrop"/>
+
+        <!-- Fingerprint -->
+        <!-- AOD dashed fingerprint icon with moving dashes -->
+        <com.airbnb.lottie.LottieAnimationView
+            android:id="@+id/lock_udfps_aod_fp"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:padding="@dimen/lock_icon_padding"
+            android:layout_gravity="center"
+            android:scaleType="centerCrop"
+            systemui:lottie_autoPlay="false"
+            systemui:lottie_loop="true"
+            systemui:lottie_rawRes="@raw/udfps_aod_fp"/>
     </com.android.keyguard.LockIconView>
 
     <com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
index edb0569..5c34beb 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
@@ -44,7 +44,7 @@
     private int mRadius;
 
     private ImageView mLockIcon;
-    private ImageView mUnlockBgView;
+    private ImageView mBgView;
 
     private int mLockIconColor;
 
@@ -57,19 +57,19 @@
     public void onFinishInflate() {
         super.onFinishInflate();
         mLockIcon = findViewById(R.id.lock_icon);
-        mUnlockBgView = findViewById(R.id.lock_icon_bg);
+        mBgView = findViewById(R.id.lock_icon_bg);
     }
 
     void updateColorAndBackgroundVisibility(boolean useBackground) {
-        if (useBackground) {
+        if (useBackground && mLockIcon.getDrawable() != null) {
             mLockIconColor = Utils.getColorAttrDefaultColor(getContext(),
                     android.R.attr.textColorPrimary);
-            mUnlockBgView.setBackground(getContext().getDrawable(R.drawable.fingerprint_bg));
-            mUnlockBgView.setVisibility(View.VISIBLE);
+            mBgView.setBackground(getContext().getDrawable(R.drawable.fingerprint_bg));
+            mBgView.setVisibility(View.VISIBLE);
         } else {
             mLockIconColor = Utils.getColorAttrDefaultColor(getContext(),
                     R.attr.wallpaperTextColorAccent);
-            mUnlockBgView.setVisibility(View.GONE);
+            mBgView.setVisibility(View.GONE);
         }
 
         mLockIcon.setImageTintList(ColorStateList.valueOf(mLockIconColor));
@@ -77,9 +77,14 @@
 
     void setImageDrawable(Drawable drawable) {
         mLockIcon.setImageDrawable(drawable);
+        if (drawable == null) {
+            mBgView.setVisibility(View.INVISIBLE);
+        } else {
+            mBgView.setVisibility(View.VISIBLE);
+        }
     }
 
-    void setCenterLocation(@NonNull PointF center, int radius) {
+    public void setCenterLocation(@NonNull PointF center, int radius) {
         mLockIconCenter = center;
         mRadius = radius;
 
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 509ac8a..7d3e879 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -19,6 +19,8 @@
 import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
 
 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
+import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset;
 
 import android.content.Context;
 import android.content.res.Configuration;
@@ -32,6 +34,7 @@
 import android.os.Process;
 import android.os.Vibrator;
 import android.util.DisplayMetrics;
+import android.util.MathUtils;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
@@ -58,6 +61,8 @@
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
+import com.airbnb.lottie.LottieAnimationView;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -92,6 +97,8 @@
     @NonNull private final DelayableExecutor mExecutor;
     private boolean mUdfpsEnrolled;
 
+    @NonNull private LottieAnimationView mAodFp;
+
     @NonNull private final AnimatedVectorDrawable mFpToUnlockIcon;
     @NonNull private final AnimatedVectorDrawable mLockToUnlockIcon;
     @NonNull private final Drawable mLockIcon;
@@ -109,6 +116,7 @@
     private boolean mIsKeyguardShowing;
     private boolean mUserUnlockedWithBiometric;
     private Runnable mCancelDelayedUpdateVisibilityRunnable;
+    private Runnable mOnGestureDetectedRunnable;
 
     private boolean mUdfpsSupported;
     private float mHeightPixels;
@@ -118,6 +126,12 @@
     private boolean mShowUnlockIcon;
     private boolean mShowLockIcon;
 
+    // for udfps when strong auth is required or unlocked on AOD
+    private boolean mShowAODFpIcon;
+    private final int mMaxBurnInOffsetX;
+    private final int mMaxBurnInOffsetY;
+    private float mInterpolatedDarkAmount;
+
     private boolean mDownDetected;
     private boolean mDetectedLongPress;
     private final Rect mSensorTouchLocation = new Rect();
@@ -150,6 +164,12 @@
         mVibrator = vibrator;
 
         final Context context = view.getContext();
+        mAodFp = mView.findViewById(R.id.lock_udfps_aod_fp);
+        mMaxBurnInOffsetX = context.getResources()
+                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
+        mMaxBurnInOffsetY = context.getResources()
+                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
+
         mUnlockIcon = mView.getContext().getResources().getDrawable(
             R.drawable.ic_unlock,
             mView.getContext().getTheme());
@@ -173,15 +193,14 @@
 
     @Override
     protected void onViewAttached() {
-        // we check this here instead of onInit since the FingerprintManager + FaceManager may not
-        // have started up yet onInit
-        mUdfpsSupported = mAuthController.getUdfpsSensorLocation() != null;
-
+        updateIsUdfpsEnrolled();
         updateConfiguration();
         updateKeyguardShowing();
         mUserUnlockedWithBiometric = false;
+
         mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
         mIsDozing = mStatusBarStateController.isDozing();
+        mInterpolatedDarkAmount = mStatusBarStateController.getDozeAmount();
         mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
         mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
         mStatusBarState = mStatusBarStateController.getState();
@@ -189,15 +208,18 @@
         updateColors();
         mConfigurationController.addCallback(mConfigurationListener);
 
+        mAuthController.addCallback(mAuthControllerCallback);
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
         mStatusBarStateController.addCallback(mStatusBarStateListener);
         mKeyguardStateController.addCallback(mKeyguardStateCallback);
         mDownDetected = false;
+        updateBurnInOffsets();
         updateVisibility();
     }
 
     @Override
     protected void onViewDetached() {
+        mAuthController.removeCallback(mAuthControllerCallback);
         mConfigurationController.removeCallback(mConfigurationListener);
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
         mStatusBarStateController.removeCallback(mStatusBarStateListener);
@@ -227,7 +249,7 @@
             mCancelDelayedUpdateVisibilityRunnable = null;
         }
 
-        if (!mIsKeyguardShowing) {
+        if (!mIsKeyguardShowing && !mIsDozing) {
             mView.setVisibility(View.INVISIBLE);
             return;
         }
@@ -238,6 +260,7 @@
         mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen()
             && (!mUdfpsEnrolled || !mRunningFPS);
         mShowUnlockIcon = mCanDismissLockScreen && isLockScreen();
+        mShowAODFpIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS;
 
         final CharSequence prevContentDescription = mView.getContentDescription();
         if (mShowLockIcon) {
@@ -260,10 +283,22 @@
             }
             mView.setVisibility(View.VISIBLE);
             mView.setContentDescription(mUnlockedLabel);
+        } else if (mShowAODFpIcon) {
+            mView.setImageDrawable(null);
+            mView.setContentDescription(null);
+            mAodFp.setVisibility(View.VISIBLE);
+            mAodFp.setContentDescription(mCanDismissLockScreen ? mUnlockedLabel : mLockedLabel);
+            mView.setVisibility(View.VISIBLE);
         } else {
             mView.setVisibility(View.INVISIBLE);
             mView.setContentDescription(null);
         }
+
+        if (!mShowAODFpIcon) {
+            mAodFp.setVisibility(View.INVISIBLE);
+            mAodFp.setContentDescription(null);
+        }
+
         if (!Objects.equals(prevContentDescription, mView.getContentDescription())
                 && mView.getContentDescription() != null) {
             mView.announceForAccessibility(mView.getContentDescription());
@@ -340,10 +375,12 @@
 
     @Override
     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("mUdfpsSupported: " + mUdfpsSupported);
         pw.println("mUdfpsEnrolled: " + mUdfpsEnrolled);
         pw.println("mIsKeyguardShowing: " + mIsKeyguardShowing);
         pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
         pw.println(" mShowLockIcon: " + mShowLockIcon);
+        pw.println(" mShowAODFpIcon: " + mShowAODFpIcon);
         pw.println("  mIsDozing: " + mIsDozing);
         pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
         pw.println("  mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
@@ -351,17 +388,57 @@
         pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
         pw.println("  mStatusBarState: " + StatusBarState.toShortString(mStatusBarState));
         pw.println("  mQsExpanded: " + mQsExpanded);
+        pw.println("  mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
 
         if (mView != null) {
             mView.dump(fd, pw, args);
         }
     }
 
+    /** Every minute, update the aod icon's burn in offset */
+    public void dozeTimeTick() {
+        updateBurnInOffsets();
+    }
+
+    private void updateBurnInOffsets() {
+        float offsetX = MathUtils.lerp(0f,
+                getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
+                        - mMaxBurnInOffsetX, mInterpolatedDarkAmount);
+        float offsetY = MathUtils.lerp(0f,
+                getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
+                        - mMaxBurnInOffsetY, mInterpolatedDarkAmount);
+        float progress = MathUtils.lerp(0f, getBurnInProgressOffset(), mInterpolatedDarkAmount);
+
+        mAodFp.setTranslationX(offsetX);
+        mAodFp.setTranslationY(offsetY);
+        mAodFp.setProgress(progress);
+        mAodFp.setAlpha(255 * mInterpolatedDarkAmount);
+    }
+
+    private void updateIsUdfpsEnrolled() {
+        boolean wasUdfpsSupported = mUdfpsSupported;
+        boolean wasUdfpsEnrolled = mUdfpsEnrolled;
+
+        mUdfpsSupported = mAuthController.getUdfpsSensorLocation() != null;
+        mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled();
+        if (wasUdfpsSupported != mUdfpsSupported || wasUdfpsEnrolled != mUdfpsEnrolled) {
+            updateVisibility();
+        }
+    }
+
     private StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
                 @Override
+                public void onDozeAmountChanged(float linear, float eased) {
+                    mInterpolatedDarkAmount = eased;
+                    updateBurnInOffsets();
+                }
+
+                @Override
                 public void onDozingChanged(boolean isDozing) {
                     mIsDozing = isDozing;
+                    updateBurnInOffsets();
+                    updateIsUdfpsEnrolled();
                     updateVisibility();
                 }
 
@@ -435,7 +512,7 @@
                     mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
                         KeyguardUpdateMonitor.getCurrentUser());
             }
-            mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled();
+            updateIsUdfpsEnrolled();
             updateVisibility();
         }
 
@@ -481,8 +558,7 @@
 
                     // intercept all following touches until we see MotionEvent.ACTION_CANCEL UP or
                     // MotionEvent.ACTION_UP (see #onTouchEvent)
-                    mDownDetected = true;
-                    if (mVibrator != null) {
+                    if (mVibrator != null && !mDownDetected) {
                         mVibrator.vibrate(
                                 Process.myUid(),
                                 getContext().getOpPackageName(),
@@ -490,6 +566,8 @@
                                 "lockIcon-onDown",
                                 VIBRATION_SONIFICATION_ATTRIBUTES);
                     }
+
+                    mDownDetected = true;
                     return true;
                 }
 
@@ -539,6 +617,9 @@
                     // pre-emptively set to true to hide view
                     mIsBouncerShowing = true;
                     updateVisibility();
+                    if (mOnGestureDetectedRunnable != null) {
+                        mOnGestureDetectedRunnable.run();
+                    }
                     mKeyguardViewController.showBouncer(/* scrim */ true);
                 }
             });
@@ -548,16 +629,18 @@
      * in a 'clickable' state
      * @return whether to intercept the touch event
      */
-    public boolean onTouchEvent(MotionEvent event) {
+    public boolean onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable) {
         if (mSensorTouchLocation.contains((int) event.getX(), (int) event.getY())
-                && mView.getVisibility() == View.VISIBLE) {
+                && (mView.getVisibility() == View.VISIBLE
+                || mAodFp.getVisibility() == View.VISIBLE)) {
+            mOnGestureDetectedRunnable = onGestureDetectedRunnable;
             mGestureDetector.onTouchEvent(event);
         }
 
         // we continue to intercept all following touches until we see MotionEvent.ACTION_CANCEL UP
         // or MotionEvent.ACTION_UP. this is to avoid passing the touch to NPV
         // after the lock icon disappears on device entry
-        if (mDownDetected && mDetectedLongPress) {
+        if (mDownDetected) {
             if (event.getAction() == MotionEvent.ACTION_CANCEL
                     || event.getAction() == MotionEvent.ACTION_UP) {
                 mDownDetected = false;
@@ -577,4 +660,12 @@
     public void setAlpha(float alpha) {
         mView.setAlpha(alpha);
     }
+
+    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
+        @Override
+        public void onAllAuthenticatorsRegistered() {
+            updateIsUdfpsEnrolled();
+            updateConfiguration();
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 53fd9a3..54f9321 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -848,6 +848,10 @@
             return;
         }
 
+        if (!mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
+            return;
+        }
+
         mAodInterruptRunnable = () -> {
             mIsAodInterruptActive = true;
             // Since the sensor that triggers the AOD interrupt doesn't provide
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index 22d7a3ff..bf2eb5c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -40,7 +40,6 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
-
 /**
  * Class that coordinates non-HBM animations during keyguard authentication.
  */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 15e0716..0a76176 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -3655,6 +3655,7 @@
     }
 
     public void dozeTimeTick() {
+        mLockIconViewController.dozeTimeTick();
         mKeyguardBottomArea.dozeTimeTick();
         mKeyguardStatusViewController.dozeTimeTick();
         if (mInterpolatedDarkAmount > 0) {
@@ -3958,10 +3959,6 @@
                     mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
                 }
 
-                if (mLockIconViewController.onTouchEvent(event)) {
-                    return true;
-                }
-
                 handled |= super.onTouch(v, event);
                 return !mDozing || mPulsing || handled;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index b5d9bd6..66a6e72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -35,6 +35,7 @@
 import android.view.ViewGroup;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.LockIconViewController;
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dock.DockManager;
@@ -88,6 +89,7 @@
     private final NotificationShadeDepthController mDepthController;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private final LockIconViewController mLockIconViewController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
     private GestureDetector mGestureDetector;
@@ -138,7 +140,8 @@
             NotificationPanelViewController notificationPanelViewController,
             SuperStatusBarViewFactory statusBarViewFactory,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            LockIconViewController lockIconViewController) {
         mInjectionInflationController = injectionInflationController;
         mCoordinator = coordinator;
         mPulseExpansionHandler = pulseExpansionHandler;
@@ -163,6 +166,7 @@
         mStatusBarViewFactory = statusBarViewFactory;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mLockIconViewController = lockIconViewController;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -235,6 +239,7 @@
                 if (!isCancel && mService.shouldIgnoreTouch()) {
                     return false;
                 }
+
                 if (isDown) {
                     setTouchActive(true);
                     mTouchCancelled = false;
@@ -245,6 +250,7 @@
                 if (mTouchCancelled || mExpandAnimationRunning) {
                     return false;
                 }
+
                 mFalsingCollector.onTouchEvent(ev);
                 mGestureDetector.onTouchEvent(ev);
                 mStatusBarKeyguardViewManager.onTouch(ev);
@@ -260,9 +266,17 @@
                 if (isDown) {
                     mNotificationStackScrollLayoutController.closeControlsIfOutsideTouch(ev);
                 }
+
                 if (mStatusBarStateController.isDozing()) {
                     mService.mDozeScrimController.extendPulse();
                 }
+                mLockIconViewController.onTouchEvent(
+                        ev,
+                        () -> mService.wakeUpIfDozing(
+                                SystemClock.uptimeMillis(),
+                                mView,
+                                "LOCK_ICON_TOUCH"));
+
                 // In case we start outside of the view bounds (below the status bar), we need to
                 // dispatch
                 // the touch manually as the view system can't accommodate for touches outside of
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 2120b0e..1a39017 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -456,11 +456,12 @@
 
     @Test
     public void aodInterrupt() throws RemoteException {
-        // GIVEN that the overlay is showing and screen is on
+        // GIVEN that the overlay is showing and screen is on and fp is running
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
                 IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         // WHEN fingerprint is requested because of AOD interrupt
         mUdfpsController.onAodInterrupt(0, 0, 2f, 3f);
         // THEN illumination begins
@@ -478,6 +479,7 @@
                 IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         when(mUdfpsView.isIlluminationRequested()).thenReturn(true);
         // WHEN it is cancelled
@@ -493,6 +495,7 @@
                 IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         when(mUdfpsView.isIlluminationRequested()).thenReturn(true);
         // WHEN it times out
@@ -511,6 +514,23 @@
         mFgExecutor.runAllReady();
 
         // WHEN aod interrupt is received
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
+        mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
+
+        // THEN no illumination because screen is off
+        verify(mUdfpsView, never()).startIllumination(any());
+    }
+
+    @Test
+    public void aodInterrupt_fingerprintNotRunning() throws RemoteException {
+        // GIVEN showing overlay
+        mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
+                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+        mScreenObserver.onScreenTurnedOn();
+        mFgExecutor.runAllReady();
+
+        // WHEN aod interrupt is received when the fingerprint service isn't running
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(false);
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
 
         // THEN no illumination because screen is off
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
new file mode 100644
index 0000000..9c3016c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.os.Vibrator;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardViewController;
+import com.android.keyguard.LockIconView;
+import com.android.keyguard.LockIconViewController;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+
+import com.airbnb.lottie.LottieAnimationView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class LockIconViewControllerTest extends SysuiTestCase {
+    private @Mock LockIconView mLockIconView;
+    private @Mock Context mContext;
+    private @Mock Resources mResources;
+    private @Mock DisplayMetrics mDisplayMetrics;
+    private @Mock StatusBarStateController mStatusBarStateController;
+    private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private @Mock KeyguardViewController mKeyguardViewController;
+    private @Mock KeyguardStateController mKeyguardStateController;
+    private @Mock FalsingManager mFalsingManager;
+    private @Mock AuthController mAuthController;
+    private @Mock DumpManager mDumpManager;
+    private @Mock AccessibilityManager mAccessibilityManager;
+    private @Mock ConfigurationController mConfigurationController;
+    private @Mock DelayableExecutor mDelayableExecutor;
+    private @Mock Vibrator mVibrator;
+    private @Mock AuthRippleController mAuthRippleController;
+    private @Mock LottieAnimationView mAodFp;
+
+    private LockIconViewController mLockIconViewController;
+
+    // Capture listeners so that they can be used to send events
+    @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
+            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+    private View.OnAttachStateChangeListener mAttachListener;
+
+    @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
+    private AuthController.Callback mAuthControllerCallback;
+
+    @Captor private ArgumentCaptor<PointF> mPointCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mLockIconView.getResources()).thenReturn(mResources);
+        when(mLockIconView.getContext()).thenReturn(mContext);
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
+        when(mLockIconView.findViewById(anyInt())).thenReturn(mAodFp);
+
+        mLockIconViewController = new LockIconViewController(
+                mLockIconView,
+                mStatusBarStateController,
+                mKeyguardUpdateMonitor,
+                mKeyguardViewController,
+                mKeyguardStateController,
+                mFalsingManager,
+                mAuthController,
+                mDumpManager,
+                mAccessibilityManager,
+                mConfigurationController,
+                mDelayableExecutor,
+                mVibrator
+        );
+    }
+
+    @Test
+    public void testUpdateFingerprintLocationOnInit() {
+        // GIVEN fp sensor location is available pre-attached
+        final PointF udfpsLocation = new PointF(50, 75);
+        final int radius = 33;
+        final FingerprintSensorPropertiesInternal fpProps =
+                new FingerprintSensorPropertiesInternal(
+                        /* sensorId */ 0,
+                        /* strength */ 0,
+                        /* max enrollments per user */ 5,
+                        /* component info */ new ArrayList<>(),
+                        /* sensorType */ 3,
+                        /* resetLockoutRequiresHwToken */ false,
+                        (int) udfpsLocation.x, (int) udfpsLocation.y, radius);
+        when(mAuthController.getUdfpsSensorLocation()).thenReturn(udfpsLocation);
+        when(mAuthController.getUdfpsProps()).thenReturn(List.of(fpProps));
+
+        // WHEN lock icon view controller is initialized and attached
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // THEN lock icon view location is updated with the same coordinates as fpProps
+        verify(mLockIconView).setCenterLocation(mPointCaptor.capture(), eq(radius));
+        assertEquals(udfpsLocation, mPointCaptor.getValue());
+    }
+
+    @Test
+    public void testUpdateFingerprintLocationOnAuthenticatorsRegistered() {
+        // GIVEN fp sensor location is not available pre-init
+        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+        when(mAuthController.getUdfpsProps()).thenReturn(null);
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // GIVEN fp sensor location is available post-atttached
+        captureAuthControllerCallback();
+        final PointF udfpsLocation = new PointF(50, 75);
+        final int radius = 33;
+        final FingerprintSensorPropertiesInternal fpProps =
+                new FingerprintSensorPropertiesInternal(
+                        /* sensorId */ 0,
+                        /* strength */ 0,
+                        /* max enrollments per user */ 5,
+                        /* component info */ new ArrayList<>(),
+                        /* sensorType */ 3,
+                        /* resetLockoutRequiresHwToken */ false,
+                        (int) udfpsLocation.x, (int) udfpsLocation.y, radius);
+        when(mAuthController.getUdfpsSensorLocation()).thenReturn(udfpsLocation);
+        when(mAuthController.getUdfpsProps()).thenReturn(List.of(fpProps));
+
+        // WHEN all authenticators are registered
+        mAuthControllerCallback.onAllAuthenticatorsRegistered();
+
+        // THEN lock icon view location is updated with the same coordinates as fpProps
+        verify(mLockIconView).setCenterLocation(mPointCaptor.capture(), eq(radius));
+        assertEquals(udfpsLocation, mPointCaptor.getValue());
+    }
+
+    private void captureAuthControllerCallback() {
+        verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
+        mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
+    }
+
+    private void captureAttachListener() {
+        verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
+        mAttachListener = mAttachCaptor.getValue();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index 6c1a3c9..9b7c78f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -27,6 +27,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.LockIconViewController;
 import com.android.systemui.R;
 import com.android.systemui.SystemUIFactory;
 import com.android.systemui.SysuiTestCase;
@@ -90,6 +91,7 @@
     @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    @Mock private LockIconViewController mLockIconViewController;
 
     @Before
     public void setUp() {
@@ -131,7 +133,8 @@
                 mNotificationPanelViewController,
                 mStatusBarViewFactory,
                 mNotificationStackScrollLayoutController,
-                mStatusBarKeyguardViewManager);
+                mStatusBarKeyguardViewManager,
+                mLockIconViewController);
         mController.setupExpandedStatusBar();
         mController.setService(mStatusBar, mNotificationShadeWindowController);
         mController.setDragDownHelper(mDragDownHelper);