1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.biometrics;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.graphics.Paint;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.graphics.drawable.Drawable;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.view.animation.AccelerateDecelerateInterpolator;
32 import android.view.animation.LinearInterpolator;
33 
34 import androidx.annotation.ColorInt;
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 
38 import com.android.systemui.R;
39 
40 /**
41  * UDFPS fingerprint drawable that is shown when enrolling
42  */
43 public class UdfpsEnrollDrawable extends UdfpsDrawable {
44     private static final String TAG = "UdfpsAnimationEnroll";
45 
46     private static final long HINT_COLOR_ANIM_DELAY_MS = 233L;
47     private static final long HINT_COLOR_ANIM_DURATION_MS = 517L;
48     private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L;
49     private static final long TARGET_ANIM_DURATION_LONG = 800L;
50     private static final long TARGET_ANIM_DURATION_SHORT = 600L;
51     // 1 + SCALE_MAX is the maximum that the moving target will animate to
52     private static final float SCALE_MAX = 0.25f;
53 
54     private static final float HINT_PADDING_DP = 10f;
55     private static final float HINT_MAX_WIDTH_DP = 6f;
56     private static final float HINT_ANGLE = 40f;
57 
58     private final Handler mHandler = new Handler(Looper.getMainLooper());
59 
60     @NonNull private final Drawable mMovingTargetFpIcon;
61     @NonNull private final Paint mSensorOutlinePaint;
62     @NonNull private final Paint mBlueFill;
63 
64     @Nullable private RectF mSensorRect;
65     @Nullable private UdfpsEnrollHelper mEnrollHelper;
66 
67     // Moving target animator set
68     @Nullable AnimatorSet mTargetAnimatorSet;
69     // Moving target location
70     float mCurrentX;
71     float mCurrentY;
72     // Moving target size
73     float mCurrentScale = 1.f;
74 
75     @ColorInt private final int mHintColorFaded;
76     @ColorInt private final int mHintColorHighlight;
77     private final float mHintMaxWidthPx;
78     private final float mHintPaddingPx;
79 
80     @NonNull private final Animator.AnimatorListener mTargetAnimListener;
81 
82     private boolean mShouldShowTipHint = false;
83     @NonNull private final Paint mTipHintPaint;
84     @Nullable private AnimatorSet mTipHintAnimatorSet;
85     @Nullable private ValueAnimator mTipHintColorAnimator;
86     @Nullable private ValueAnimator mTipHintWidthAnimator;
87     @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener;
88     @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener;
89     @NonNull private final Animator.AnimatorListener mTipHintPulseListener;
90 
91     private boolean mShouldShowEdgeHint = false;
92     @NonNull private final Paint mEdgeHintPaint;
93     @Nullable private AnimatorSet mEdgeHintAnimatorSet;
94     @Nullable private ValueAnimator mEdgeHintColorAnimator;
95     @Nullable private ValueAnimator mEdgeHintWidthAnimator;
96     @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener;
97     @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener;
98     @NonNull private final Animator.AnimatorListener mEdgeHintPulseListener;
99 
100     private boolean mShowingNewUdfpsEnroll = false;
101 
UdfpsEnrollDrawable(@onNull Context context)102     UdfpsEnrollDrawable(@NonNull Context context) {
103         super(context);
104 
105         mSensorOutlinePaint = new Paint(0 /* flags */);
106         mSensorOutlinePaint.setAntiAlias(true);
107         mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_moving_target_fill));
108         mSensorOutlinePaint.setStyle(Paint.Style.FILL);
109 
110         mBlueFill = new Paint(0 /* flags */);
111         mBlueFill.setAntiAlias(true);
112         mBlueFill.setColor(context.getColor(R.color.udfps_moving_target_fill));
113         mBlueFill.setStyle(Paint.Style.FILL);
114 
115         mMovingTargetFpIcon = context.getResources()
116                 .getDrawable(R.drawable.ic_kg_fingerprint, null);
117         mMovingTargetFpIcon.setTint(mContext.getColor(R.color.udfps_enroll_icon));
118         mMovingTargetFpIcon.mutate();
119 
120         mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon));
121 
122         mHintColorFaded = context.getColor(R.color.udfps_moving_target_fill);
123         mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress);
124         mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP);
125         mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP);
126 
127         mTargetAnimListener = new Animator.AnimatorListener() {
128             @Override
129             public void onAnimationStart(Animator animation) {}
130 
131             @Override
132             public void onAnimationEnd(Animator animation) {
133                 updateTipHintVisibility();
134             }
135 
136             @Override
137             public void onAnimationCancel(Animator animation) {}
138 
139             @Override
140             public void onAnimationRepeat(Animator animation) {}
141         };
142 
143         mTipHintPaint = new Paint(0 /* flags */);
144         mTipHintPaint.setAntiAlias(true);
145         mTipHintPaint.setColor(mHintColorFaded);
146         mTipHintPaint.setStyle(Paint.Style.STROKE);
147         mTipHintPaint.setStrokeCap(Paint.Cap.ROUND);
148         mTipHintPaint.setStrokeWidth(0f);
149         mTipHintColorUpdateListener = animation -> {
150             mTipHintPaint.setColor((int) animation.getAnimatedValue());
151             invalidateSelf();
152         };
153         mTipHintWidthUpdateListener = animation -> {
154             mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
155             invalidateSelf();
156         };
157         mTipHintPulseListener = new Animator.AnimatorListener() {
158             @Override
159             public void onAnimationStart(Animator animation) {}
160 
161             @Override
162             public void onAnimationEnd(Animator animation) {
163                 mHandler.postDelayed(() -> {
164                     mTipHintColorAnimator =
165                             ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded);
166                     mTipHintColorAnimator.setInterpolator(new LinearInterpolator());
167                     mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
168                     mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
169                     mTipHintColorAnimator.start();
170                 }, HINT_COLOR_ANIM_DELAY_MS);
171             }
172 
173             @Override
174             public void onAnimationCancel(Animator animation) {}
175 
176             @Override
177             public void onAnimationRepeat(Animator animation) {}
178         };
179 
180         mEdgeHintPaint = new Paint(0 /* flags */);
181         mEdgeHintPaint.setAntiAlias(true);
182         mEdgeHintPaint.setColor(mHintColorFaded);
183         mEdgeHintPaint.setStyle(Paint.Style.STROKE);
184         mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND);
185         mEdgeHintPaint.setStrokeWidth(0f);
186         mEdgeHintColorUpdateListener = animation -> {
187             mEdgeHintPaint.setColor((int) animation.getAnimatedValue());
188             invalidateSelf();
189         };
190         mEdgeHintWidthUpdateListener = animation -> {
191             mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
192             invalidateSelf();
193         };
194         mEdgeHintPulseListener = new Animator.AnimatorListener() {
195             @Override
196             public void onAnimationStart(Animator animation) {}
197 
198             @Override
199             public void onAnimationEnd(Animator animation) {
200                 mHandler.postDelayed(() -> {
201                     mEdgeHintColorAnimator =
202                             ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded);
203                     mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator());
204                     mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
205                     mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
206                     mEdgeHintColorAnimator.start();
207                 }, HINT_COLOR_ANIM_DELAY_MS);
208             }
209 
210             @Override
211             public void onAnimationCancel(Animator animation) {}
212 
213             @Override
214             public void onAnimationRepeat(Animator animation) {}
215         };
216         mShowingNewUdfpsEnroll = context.getResources().getBoolean(
217                 com.android.internal.R.bool.config_udfpsSupportsNewUi);
218     }
219 
setEnrollHelper(@onNull UdfpsEnrollHelper helper)220     void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
221         mEnrollHelper = helper;
222     }
223 
224     @Override
onSensorRectUpdated(@onNull RectF sensorRect)225     public void onSensorRectUpdated(@NonNull RectF sensorRect) {
226         super.onSensorRectUpdated(sensorRect);
227         mSensorRect = sensorRect;
228     }
229 
230     @Override
updateFingerprintIconBounds(@onNull Rect bounds)231     protected void updateFingerprintIconBounds(@NonNull Rect bounds) {
232         super.updateFingerprintIconBounds(bounds);
233         mMovingTargetFpIcon.setBounds(bounds);
234         invalidateSelf();
235     }
236 
onEnrollmentProgress(int remaining, int totalSteps)237     void onEnrollmentProgress(int remaining, int totalSteps) {
238         if (mEnrollHelper == null) {
239             return;
240         }
241 
242         if (!mEnrollHelper.isCenterEnrollmentStage()) {
243             if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
244                 mTargetAnimatorSet.end();
245             }
246 
247             final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
248             if (mCurrentX != point.x || mCurrentY != point.y) {
249                 final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
250                 x.addUpdateListener(animation -> {
251                     mCurrentX = (float) animation.getAnimatedValue();
252                     invalidateSelf();
253                 });
254 
255                 final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
256                 y.addUpdateListener(animation -> {
257                     mCurrentY = (float) animation.getAnimatedValue();
258                     invalidateSelf();
259                 });
260 
261                 final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
262                 final long duration = isMovingToCenter
263                         ? TARGET_ANIM_DURATION_SHORT
264                         : TARGET_ANIM_DURATION_LONG;
265 
266                 final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
267                 scale.setDuration(duration);
268                 scale.addUpdateListener(animation -> {
269                     // Grow then shrink
270                     mCurrentScale = 1
271                             + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
272                     invalidateSelf();
273                 });
274 
275                 mTargetAnimatorSet = new AnimatorSet();
276 
277                 mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
278                 mTargetAnimatorSet.setDuration(duration);
279                 mTargetAnimatorSet.addListener(mTargetAnimListener);
280                 mTargetAnimatorSet.playTogether(x, y, scale);
281                 mTargetAnimatorSet.start();
282             } else {
283                 updateTipHintVisibility();
284             }
285         } else {
286             updateTipHintVisibility();
287         }
288 
289         updateEdgeHintVisibility();
290     }
291 
updateTipHintVisibility()292     private void updateTipHintVisibility() {
293         final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
294         if (mShouldShowTipHint == shouldShow) {
295             return;
296         }
297         mShouldShowTipHint = shouldShow;
298 
299         if (mShowingNewUdfpsEnroll) {
300             return;
301         }
302 
303         if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) {
304             mTipHintWidthAnimator.cancel();
305         }
306 
307         final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
308         mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth);
309         mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
310         mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener);
311 
312         if (shouldShow) {
313             startTipHintPulseAnimation();
314         } else {
315             mTipHintWidthAnimator.start();
316         }
317 
318     }
319 
updateEdgeHintVisibility()320     private void updateEdgeHintVisibility() {
321         final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
322         if (mShouldShowEdgeHint == shouldShow) {
323             return;
324         }
325         mShouldShowEdgeHint = shouldShow;
326 
327         if (mShowingNewUdfpsEnroll) {
328             return;
329         }
330 
331         if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) {
332             mEdgeHintWidthAnimator.cancel();
333         }
334 
335         final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
336         mEdgeHintWidthAnimator =
337                 ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth);
338         mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
339         mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener);
340 
341         if (shouldShow) {
342             startEdgeHintPulseAnimation();
343         } else {
344             mEdgeHintWidthAnimator.start();
345         }
346     }
347 
startTipHintPulseAnimation()348     private void startTipHintPulseAnimation() {
349         if (mShowingNewUdfpsEnroll) {
350             return;
351         }
352 
353         mHandler.removeCallbacksAndMessages(null);
354         if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) {
355             mTipHintAnimatorSet.cancel();
356         }
357         if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) {
358             mTipHintColorAnimator.cancel();
359         }
360 
361         mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight);
362         mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
363         mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
364         mTipHintColorAnimator.addListener(mTipHintPulseListener);
365 
366         mTipHintAnimatorSet = new AnimatorSet();
367         mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
368         mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator);
369         mTipHintAnimatorSet.start();
370     }
371 
startEdgeHintPulseAnimation()372     private void startEdgeHintPulseAnimation() {
373         if (mShowingNewUdfpsEnroll) {
374             return;
375         }
376 
377         mHandler.removeCallbacksAndMessages(null);
378         if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) {
379             mEdgeHintAnimatorSet.cancel();
380         }
381         if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) {
382             mEdgeHintColorAnimator.cancel();
383         }
384 
385         mEdgeHintColorAnimator =
386                 ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight);
387         mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
388         mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
389         mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener);
390 
391         mEdgeHintAnimatorSet = new AnimatorSet();
392         mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
393         mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator);
394         mEdgeHintAnimatorSet.start();
395     }
396 
isTipHintVisible()397     private boolean isTipHintVisible() {
398         return mTipHintPaint.getStrokeWidth() > 0f;
399     }
400 
isEdgeHintVisible()401     private boolean isEdgeHintVisible() {
402         return mEdgeHintPaint.getStrokeWidth() > 0f;
403     }
404 
405     @Override
draw(@onNull Canvas canvas)406     public void draw(@NonNull Canvas canvas) {
407         if (isIlluminationShowing()) {
408             return;
409         }
410 
411         // Draw moving target
412         if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
413             canvas.save();
414             canvas.translate(mCurrentX, mCurrentY);
415 
416             if (mSensorRect != null) {
417                 canvas.scale(mCurrentScale, mCurrentScale,
418                         mSensorRect.centerX(), mSensorRect.centerY());
419                 canvas.drawOval(mSensorRect, mBlueFill);
420             }
421 
422             mMovingTargetFpIcon.draw(canvas);
423             canvas.restore();
424         } else {
425             if (mSensorRect != null) {
426                 canvas.drawOval(mSensorRect, mSensorOutlinePaint);
427             }
428             mFingerprintDrawable.draw(canvas);
429             mFingerprintDrawable.setAlpha(mAlpha);
430             mSensorOutlinePaint.setAlpha(mAlpha);
431         }
432 
433         if (mShowingNewUdfpsEnroll) {
434             return;
435         }
436 
437         // Draw the finger tip or edges hint.
438         if (isTipHintVisible() || isEdgeHintVisible()) {
439             canvas.save();
440 
441             // Make arcs start from the top, rather than the right.
442             canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
443 
444             final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f;
445             final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f;
446             final float hintXOffset = halfSensorWidth + mHintPaddingPx;
447             final float hintYOffset = halfSensorHeight + mHintPaddingPx;
448 
449             if (isTipHintVisible()) {
450                 canvas.drawArc(
451                         mSensorRect.centerX() - hintXOffset,
452                         mSensorRect.centerY() - hintYOffset,
453                         mSensorRect.centerX() + hintXOffset,
454                         mSensorRect.centerY() + hintYOffset,
455                         -HINT_ANGLE / 2f,
456                         HINT_ANGLE,
457                         false /* useCenter */,
458                         mTipHintPaint);
459             }
460 
461             if (isEdgeHintVisible()) {
462                 // Draw right edge hint.
463                 canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
464                 canvas.drawArc(
465                         mSensorRect.centerX() - hintXOffset,
466                         mSensorRect.centerY() - hintYOffset,
467                         mSensorRect.centerX() + hintXOffset,
468                         mSensorRect.centerY() + hintYOffset,
469                         -HINT_ANGLE / 2f,
470                         HINT_ANGLE,
471                         false /* useCenter */,
472                         mEdgeHintPaint);
473 
474                 // Draw left edge hint.
475                 canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY());
476                 canvas.drawArc(
477                         mSensorRect.centerX() - hintXOffset,
478                         mSensorRect.centerY() - hintYOffset,
479                         mSensorRect.centerX() + hintXOffset,
480                         mSensorRect.centerY() + hintYOffset,
481                         -HINT_ANGLE / 2f,
482                         HINT_ANGLE,
483                         false /* useCenter */,
484                         mEdgeHintPaint);
485             }
486 
487             canvas.restore();
488         }
489     }
490 
491     @Override
setAlpha(int alpha)492     public void setAlpha(int alpha) {
493         super.setAlpha(alpha);
494         mSensorOutlinePaint.setAlpha(alpha);
495         mBlueFill.setAlpha(alpha);
496         mMovingTargetFpIcon.setAlpha(alpha);
497         invalidateSelf();
498     }
499 }
500