RESTRICT AUTOMERGE Show tip/edge hints for UDFPS enrollment

Shows visual hints during the tip and edge stages of UDFPS enrollment.

Test: Manual
Bug: 198858684
Change-Id: Ib2e08fcb7c524916a874932d1b9d558b19028ff0
Merged-In: Ifda20133f1ee90c9e1612e9066c19efbb6ffc9cd
Merged-In: Ic12b85bd6f8f1d4068952eb83bcc8a1df21f2e8e
(cherry picked from commit 1d4c7dc05bd43e6573274ca8290293ef85d8da2d)
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..14b1863 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -202,6 +202,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()) {