Merge "Fill in javadocs for translation feature." into sc-dev
diff --git a/apex/media/OWNERS b/apex/media/OWNERS
index ced2fb5..73f02d3 100644
--- a/apex/media/OWNERS
+++ b/apex/media/OWNERS
@@ -1,10 +1,10 @@
-andrewlewis@google.com
-aquilescanta@google.com
-chz@google.com
+# Bug component: 1344
 hdmoon@google.com
 hkuang@google.com
 jinpark@google.com
 klhyun@google.com
 lnilsson@google.com
-marcone@google.com
 sungsoo@google.com
+
+# go/android-fwk-media-solutions for info on areas of ownership.
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 8e7fae7..d12c870 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -50,6 +50,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A class that allows the app to get the frame metrics from HardwareRendererObserver.
@@ -103,6 +104,7 @@
     private boolean mCancelled = false;
     private FrameTrackerListener mListener;
     private boolean mTracingStarted = false;
+    private Runnable mWaitForFinishTimedOut;
 
     private static class JankInfo {
         long frameVsyncId;
@@ -263,10 +265,16 @@
             if (mListener != null) {
                 mListener.onCujEvents(mSession, ACTION_SESSION_END);
             }
+            // We don't remove observer here,
+            // will remove it when all the frame metrics in this duration are called back.
+            // See onFrameMetricsAvailable for the logic of removing the observer.
+            // Waiting at most 10 seconds for all callbacks to finish.
+            mWaitForFinishTimedOut = () -> {
+                Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
+                finish(mJankInfos.size() - 1);
+            };
+            mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
         }
-        // We don't remove observer here,
-        // will remove it when all the frame metrics in this duration are called back.
-        // See onFrameMetricsAvailable for the logic of removing the observer.
     }
 
     /**
@@ -396,7 +404,8 @@
     }
 
     private void finish(int indexOnOrAfterEnd) {
-
+        mHandler.removeCallbacks(mWaitForFinishTimedOut);
+        mWaitForFinishTimedOut = null;
         mMetricsFinalized = true;
 
         // The tracing has been ended, remove the observer, see if need to trigger perfetto.
@@ -481,7 +490,7 @@
             }
         }
         if (DEBUG) {
-            Log.i(TAG, "FrameTracker: CUJ=" + mSession.getName()
+            Log.i(TAG, "finish: CUJ=" + mSession.getName()
                     + " (" + mBeginVsyncId + "," + mEndVsyncId + ")"
                     + " totalFrames=" + totalFramesCount
                     + " missedAppFrames=" + missedAppFramesCount
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index aabcd7f..610cd73 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -103,7 +103,7 @@
     private static final String ACTION_PREFIX = InteractionJankMonitor.class.getCanonicalName();
 
     private static final String DEFAULT_WORKER_NAME = TAG + "-Worker";
-    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5L);
+    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(2L);
     private static final String SETTINGS_ENABLED_KEY = "enabled";
     private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
     private static final String SETTINGS_THRESHOLD_MISSED_FRAMES_KEY =
diff --git a/media/OWNERS b/media/OWNERS
index abfc8bf..0aff43e 100644
--- a/media/OWNERS
+++ b/media/OWNERS
@@ -1,8 +1,7 @@
-chz@google.com
+# Bug component: 1344
 elaurent@google.com
 essick@google.com
 etalvala@google.com
-gkasten@google.com
 hdmoon@google.com
 hkuang@google.com
 hunga@google.com
@@ -13,16 +12,13 @@
 jsharkey@android.com
 klhyun@google.com
 lajos@google.com
-marcone@google.com
 nchalko@google.com
 philburk@google.com
 quxiangfang@google.com
 wonsik@google.com
 
-# LON
-andrewlewis@google.com
-aquilescanta@google.com
-olly@google.com
+# go/android-fwk-media-solutions for info on areas of ownership.
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
 
 # SEO
 sungsoo@google.com
diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS
index cf06fad..813dee3 100644
--- a/media/java/android/media/OWNERS
+++ b/media/java/android/media/OWNERS
@@ -1,9 +1,9 @@
 # Bug component: 1344
-
 fgoldfain@google.com
 elaurent@google.com
 lajos@google.com
-olly@google.com
-andrewlewis@google.com
 sungsoo@google.com
 jmtrivi@google.com
+
+# go/android-fwk-media-solutions for info on areas of ownership.
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 1cea1f6..ac7a18a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -217,6 +217,7 @@
         }
 
         void onEnrollmentHelp() {
+            Log.d(TAG, "onEnrollmentHelp");
             if (mEnrollHelper != null) {
                 mEnrollHelper.onEnrollmentHelp();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
index 194113c..2034ff3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
@@ -16,9 +16,11 @@
 
 package com.android.systemui.biometrics;
 
+import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
@@ -26,11 +28,17 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.TypedValue;
 import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.graphics.ColorUtils;
 import com.android.systemui.R;
 
 /**
@@ -39,10 +47,20 @@
 public class UdfpsEnrollDrawable extends UdfpsDrawable {
     private static final String TAG = "UdfpsAnimationEnroll";
 
-    private static final long ANIM_DURATION = 800;
+    private static final long HINT_COLOR_ANIM_DELAY_MS = 233L;
+    private static final long HINT_COLOR_ANIM_DURATION_MS = 517L;
+    private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L;
+    private static final long TARGET_ANIM_DURATION_LONG = 800L;
+    private static final long TARGET_ANIM_DURATION_SHORT = 600L;
     // 1 + SCALE_MAX is the maximum that the moving target will animate to
     private static final float SCALE_MAX = 0.25f;
 
+    private static final float HINT_PADDING_DP = 10f;
+    private static final float HINT_MAX_WIDTH_DP = 6f;
+    private static final float HINT_ANGLE = 40f;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
     @NonNull private final Drawable mMovingTargetFpIcon;
     @NonNull private final Paint mSensorOutlinePaint;
     @NonNull private final Paint mBlueFill;
@@ -51,17 +69,41 @@
     @Nullable private UdfpsEnrollHelper mEnrollHelper;
 
     // Moving target animator set
-    @Nullable AnimatorSet mAnimatorSet;
+    @Nullable AnimatorSet mTargetAnimatorSet;
     // Moving target location
     float mCurrentX;
     float mCurrentY;
     // Moving target size
     float mCurrentScale = 1.f;
 
+    @ColorInt private final int mHintColorFaded;
+    @ColorInt private final int mHintColorHighlight;
+    private final float mHintMaxWidthPx;
+    private final float mHintPaddingPx;
+
+    @NonNull private final Animator.AnimatorListener mTargetAnimListener;
+
+    private boolean mShouldShowTipHint = false;
+    @NonNull private final Paint mTipHintPaint;
+    @Nullable private AnimatorSet mTipHintAnimatorSet;
+    @Nullable private ValueAnimator mTipHintColorAnimator;
+    @Nullable private ValueAnimator mTipHintWidthAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener;
+    @NonNull private final Animator.AnimatorListener mTipHintPulseListener;
+
+    private boolean mShouldShowEdgeHint = false;
+    @NonNull private final Paint mEdgeHintPaint;
+    @Nullable private AnimatorSet mEdgeHintAnimatorSet;
+    @Nullable private ValueAnimator mEdgeHintColorAnimator;
+    @Nullable private ValueAnimator mEdgeHintWidthAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener;
+    @NonNull private final Animator.AnimatorListener mEdgeHintPulseListener;
+
     UdfpsEnrollDrawable(@NonNull Context context) {
         super(context);
 
-
         mSensorOutlinePaint = new Paint(0 /* flags */);
         mSensorOutlinePaint.setAntiAlias(true);
         mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon));
@@ -78,6 +120,117 @@
         mMovingTargetFpIcon.mutate();
 
         mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon));
+
+        mHintColorFaded = getHintColorFaded(context);
+        mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress);
+        mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP);
+        mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP);
+
+        mTargetAnimListener = new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                updateTipHintVisibility();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        };
+
+        mTipHintPaint = new Paint(0 /* flags */);
+        mTipHintPaint.setAntiAlias(true);
+        mTipHintPaint.setColor(mHintColorFaded);
+        mTipHintPaint.setStyle(Paint.Style.STROKE);
+        mTipHintPaint.setStrokeCap(Paint.Cap.ROUND);
+        mTipHintPaint.setStrokeWidth(0f);
+        mTipHintColorUpdateListener = animation -> {
+            mTipHintPaint.setColor((int) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mTipHintWidthUpdateListener = animation -> {
+            mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mTipHintPulseListener = new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mHandler.postDelayed(() -> {
+                    mTipHintColorAnimator =
+                            ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded);
+                    mTipHintColorAnimator.setInterpolator(new LinearInterpolator());
+                    mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
+                    mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
+                    mTipHintColorAnimator.start();
+                }, HINT_COLOR_ANIM_DELAY_MS);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        };
+
+        mEdgeHintPaint = new Paint(0 /* flags */);
+        mEdgeHintPaint.setAntiAlias(true);
+        mEdgeHintPaint.setColor(mHintColorFaded);
+        mEdgeHintPaint.setStyle(Paint.Style.STROKE);
+        mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND);
+        mEdgeHintPaint.setStrokeWidth(0f);
+        mEdgeHintColorUpdateListener = animation -> {
+            mEdgeHintPaint.setColor((int) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mEdgeHintWidthUpdateListener = animation -> {
+            mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mEdgeHintPulseListener = new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mHandler.postDelayed(() -> {
+                    mEdgeHintColorAnimator =
+                            ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded);
+                    mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator());
+                    mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
+                    mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
+                    mEdgeHintColorAnimator.start();
+                }, HINT_COLOR_ANIM_DELAY_MS);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        };
+    }
+
+    @ColorInt
+    private static int getHintColorFaded(@NonNull Context context) {
+        final TypedValue tv = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
+        final int alpha = (int) (tv.getFloat() * 255f);
+
+        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
+        final TypedArray ta = context.obtainStyledAttributes(attrs);
+        try {
+            @ColorInt final int color = ta.getColor(0, context.getColor(R.color.white_disabled));
+            return ColorUtils.setAlphaComponent(color, alpha);
+        } finally {
+            ta.recycle();
+        }
     }
 
     void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
@@ -98,9 +251,13 @@
     }
 
     void onEnrollmentProgress(int remaining, int totalSteps) {
-        if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
-            if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
-                mAnimatorSet.end();
+        if (mEnrollHelper == null) {
+            return;
+        }
+
+        if (!mEnrollHelper.isCenterEnrollmentStage()) {
+            if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
+                mTargetAnimatorSet.end();
             }
 
             final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
@@ -117,8 +274,13 @@
                     invalidateSelf();
                 });
 
+                final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
+                final long duration = isMovingToCenter
+                        ? TARGET_ANIM_DURATION_SHORT
+                        : TARGET_ANIM_DURATION_LONG;
+
                 final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
-                scale.setDuration(ANIM_DURATION);
+                scale.setDuration(duration);
                 scale.addUpdateListener(animation -> {
                     // Grow then shrink
                     mCurrentScale = 1
@@ -126,14 +288,117 @@
                     invalidateSelf();
                 });
 
-                mAnimatorSet = new AnimatorSet();
+                mTargetAnimatorSet = new AnimatorSet();
 
-                mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
-                mAnimatorSet.setDuration(ANIM_DURATION);
-                mAnimatorSet.playTogether(x, y, scale);
-                mAnimatorSet.start();
+                mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+                mTargetAnimatorSet.setDuration(duration);
+                mTargetAnimatorSet.addListener(mTargetAnimListener);
+                mTargetAnimatorSet.playTogether(x, y, scale);
+                mTargetAnimatorSet.start();
+            } else {
+                updateTipHintVisibility();
             }
+        } else {
+            updateTipHintVisibility();
         }
+
+        updateEdgeHintVisibility();
+    }
+
+    private void updateTipHintVisibility() {
+        final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
+        if (mShouldShowTipHint == shouldShow) {
+            return;
+        }
+        mShouldShowTipHint = shouldShow;
+
+        if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) {
+            mTipHintWidthAnimator.cancel();
+        }
+
+        final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
+        mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth);
+        mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener);
+
+        if (shouldShow) {
+            startTipHintPulseAnimation();
+        } else {
+            mTipHintWidthAnimator.start();
+        }
+    }
+
+    private void updateEdgeHintVisibility() {
+        final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
+        if (mShouldShowEdgeHint == shouldShow) {
+            return;
+        }
+        mShouldShowEdgeHint = shouldShow;
+
+        if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) {
+            mEdgeHintWidthAnimator.cancel();
+        }
+
+        final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
+        mEdgeHintWidthAnimator =
+                ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth);
+        mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener);
+
+        if (shouldShow) {
+            startEdgeHintPulseAnimation();
+        } else {
+            mEdgeHintWidthAnimator.start();
+        }
+    }
+
+    private void startTipHintPulseAnimation() {
+        mHandler.removeCallbacksAndMessages(null);
+        if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) {
+            mTipHintAnimatorSet.cancel();
+        }
+        if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) {
+            mTipHintColorAnimator.cancel();
+        }
+
+        mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight);
+        mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
+        mTipHintColorAnimator.addListener(mTipHintPulseListener);
+
+        mTipHintAnimatorSet = new AnimatorSet();
+        mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+        mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator);
+        mTipHintAnimatorSet.start();
+    }
+
+    private void startEdgeHintPulseAnimation() {
+        mHandler.removeCallbacksAndMessages(null);
+        if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) {
+            mEdgeHintAnimatorSet.cancel();
+        }
+        if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) {
+            mEdgeHintColorAnimator.cancel();
+        }
+
+        mEdgeHintColorAnimator =
+                ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight);
+        mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
+        mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener);
+
+        mEdgeHintAnimatorSet = new AnimatorSet();
+        mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+        mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator);
+        mEdgeHintAnimatorSet.start();
+    }
+
+    private boolean isTipHintVisible() {
+        return mTipHintPaint.getStrokeWidth() > 0f;
+    }
+
+    private boolean isEdgeHintVisible() {
+        return mEdgeHintPaint.getStrokeWidth() > 0f;
     }
 
     @Override
@@ -163,6 +428,59 @@
             mFingerprintDrawable.setAlpha(mAlpha);
             mSensorOutlinePaint.setAlpha(mAlpha);
         }
+
+        // Draw the finger tip or edges hint.
+        if (isTipHintVisible() || isEdgeHintVisible()) {
+            canvas.save();
+
+            // Make arcs start from the top, rather than the right.
+            canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
+
+            final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f;
+            final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f;
+            final float hintXOffset = halfSensorWidth + mHintPaddingPx;
+            final float hintYOffset = halfSensorHeight + mHintPaddingPx;
+
+            if (isTipHintVisible()) {
+                canvas.drawArc(
+                        mSensorRect.centerX() - hintXOffset,
+                        mSensorRect.centerY() - hintYOffset,
+                        mSensorRect.centerX() + hintXOffset,
+                        mSensorRect.centerY() + hintYOffset,
+                        -HINT_ANGLE / 2f,
+                        HINT_ANGLE,
+                        false /* useCenter */,
+                        mTipHintPaint);
+            }
+
+            if (isEdgeHintVisible()) {
+                // Draw right edge hint.
+                canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
+                canvas.drawArc(
+                        mSensorRect.centerX() - hintXOffset,
+                        mSensorRect.centerY() - hintYOffset,
+                        mSensorRect.centerX() + hintXOffset,
+                        mSensorRect.centerY() + hintYOffset,
+                        -HINT_ANGLE / 2f,
+                        HINT_ANGLE,
+                        false /* useCenter */,
+                        mEdgeHintPaint);
+
+                // Draw left edge hint.
+                canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY());
+                canvas.drawArc(
+                        mSensorRect.centerX() - hintXOffset,
+                        mSensorRect.centerY() - hintYOffset,
+                        mSensorRect.centerX() + hintXOffset,
+                        mSensorRect.centerY() + hintYOffset,
+                        -HINT_ANGLE / 2f,
+                        HINT_ANGLE,
+                        false /* useCenter */,
+                        mEdgeHintPaint);
+            }
+
+            canvas.restore();
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
index bce013d..8ac6df7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -56,6 +56,7 @@
 
     interface Listener {
         void onEnrollmentProgress(int remaining, int totalSteps);
+        void onEnrollmentHelp(int remaining, int totalSteps);
         void onLastStepAcquired();
     }
 
@@ -173,7 +174,9 @@
     }
 
     void onEnrollmentHelp() {
-
+        if (mListener != null) {
+            mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps);
+        }
     }
 
     void setListener(Listener listener) {
@@ -202,6 +205,21 @@
         return progressSteps >= STAGE_THRESHOLDS[0] && progressSteps < STAGE_THRESHOLDS[1];
     }
 
+    boolean isTipEnrollmentStage() {
+        if (mTotalSteps == -1 || mRemainingSteps == -1) {
+            return false;
+        }
+        final int progressSteps = mTotalSteps - mRemainingSteps;
+        return progressSteps >= STAGE_THRESHOLDS[1] && progressSteps < STAGE_THRESHOLDS[2];
+    }
+
+    boolean isEdgeEnrollmentStage() {
+        if (mTotalSteps == -1 || mRemainingSteps == -1) {
+            return false;
+        }
+        return mTotalSteps - mRemainingSteps >= STAGE_THRESHOLDS[2];
+    }
+
     @NonNull
     PointF getNextGuidedEnrollmentPoint() {
         if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
index 5e58e08..6f02c64 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
@@ -83,6 +83,11 @@
         });
     }
 
+    void onEnrollmentHelp(int remaining, int totalSteps) {
+        mHandler.post(
+                () -> mFingerprintProgressDrawable.setEnrollmentProgress(remaining, totalSteps));
+    }
+
     void onLastStepAcquired() {
         mHandler.post(() -> {
             mFingerprintProgressDrawable.onLastStepAcquired();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 9ae24db..6cdd1c8b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -33,16 +33,21 @@
     @NonNull private final UdfpsEnrollHelper mEnrollHelper;
     @NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
             new UdfpsEnrollHelper.Listener() {
-        @Override
-        public void onEnrollmentProgress(int remaining, int totalSteps) {
-            mView.onEnrollmentProgress(remaining, totalSteps);
-        }
+                @Override
+                public void onEnrollmentProgress(int remaining, int totalSteps) {
+                    mView.onEnrollmentProgress(remaining, totalSteps);
+                }
 
-        @Override
-        public void onLastStepAcquired() {
-            mView.onLastStepAcquired();
-        }
-    };
+                @Override
+                public void onEnrollmentHelp(int remaining, int totalSteps) {
+                    mView.onEnrollmentHelp(remaining, totalSteps);
+                }
+
+                @Override
+                public void onLastStepAcquired() {
+                    mView.onLastStepAcquired();
+                }
+            };
 
     protected UdfpsEnrollViewController(
             @NonNull UdfpsEnrollView view,