1 /*
2  * Copyright (C) 2022 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.settings.biometrics.fingerprint;
18 
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorSet;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.graphics.Canvas;
26 import android.graphics.ColorFilter;
27 import android.graphics.Paint;
28 import android.graphics.PointF;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.ShapeDrawable;
33 import android.graphics.drawable.shapes.PathShape;
34 import android.util.AttributeSet;
35 import android.util.PathParser;
36 import android.view.animation.AccelerateDecelerateInterpolator;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 
41 import com.android.settings.R;
42 
43 /**
44  * UDFPS fingerprint drawable that is shown when enrolling
45  */
46 public class UdfpsEnrollDrawable extends Drawable {
47     private static final String TAG = "UdfpsAnimationEnroll";
48 
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     private static final float DEFAULT_STROKE_WIDTH = 3f;
54 
55     @NonNull
56     private final Drawable mMovingTargetFpIcon;
57     @NonNull
58     private final Paint mSensorOutlinePaint;
59     @NonNull
60     private final Paint mBlueFill;
61     @NonNull
62     private final ShapeDrawable mFingerprintDrawable;
63 
64     private int mAlpha;
65     private boolean mSkipDraw = false;
66 
67     @Nullable
68     private RectF mSensorRect;
69     @Nullable
70     private UdfpsEnrollHelper mEnrollHelper;
71 
72     // Moving target animator set
73     @Nullable
74     AnimatorSet mTargetAnimatorSet;
75     // Moving target location
76     float mCurrentX;
77     float mCurrentY;
78     // Moving target size
79     float mCurrentScale = 1.f;
80 
81     @NonNull
82     private final Animator.AnimatorListener mTargetAnimListener;
83 
84     private boolean mShouldShowTipHint = false;
85     private boolean mShouldShowEdgeHint = false;
86 
87     private int mEnrollIcon;
88     private int mMovingTargetFill;
89 
UdfpsEnrollDrawable(@onNull Context context, @Nullable AttributeSet attrs)90     UdfpsEnrollDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
91         mFingerprintDrawable = defaultFactory(context);
92 
93         loadResources(context, attrs);
94         mSensorOutlinePaint = new Paint(0 /* flags */);
95         mSensorOutlinePaint.setAntiAlias(true);
96         mSensorOutlinePaint.setColor(mMovingTargetFill);
97         mSensorOutlinePaint.setStyle(Paint.Style.FILL);
98 
99         mBlueFill = new Paint(0 /* flags */);
100         mBlueFill.setAntiAlias(true);
101         mBlueFill.setColor(mMovingTargetFill);
102         mBlueFill.setStyle(Paint.Style.FILL);
103 
104         mMovingTargetFpIcon = context.getResources()
105                 .getDrawable(R.drawable.ic_enrollment_fingerprint, null);
106         mMovingTargetFpIcon.setTint(mEnrollIcon);
107         mMovingTargetFpIcon.mutate();
108 
109         mFingerprintDrawable.setTint(mEnrollIcon);
110 
111         setAlpha(255);
112         mTargetAnimListener = new Animator.AnimatorListener() {
113             @Override
114             public void onAnimationStart(Animator animation) {
115             }
116 
117             @Override
118             public void onAnimationEnd(Animator animation) {
119                 updateTipHintVisibility();
120             }
121 
122             @Override
123             public void onAnimationCancel(Animator animation) {
124             }
125 
126             @Override
127             public void onAnimationRepeat(Animator animation) {
128             }
129         };
130     }
131 
132     /** The [sensorRect] coordinates for the sensor area. */
onSensorRectUpdated(@onNull RectF sensorRect)133     void onSensorRectUpdated(@NonNull RectF sensorRect) {
134         int margin = ((int) sensorRect.height()) / 8;
135         Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin,
136                 (int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin);
137         updateFingerprintIconBounds(bounds);
138         mSensorRect = sensorRect;
139     }
140 
setEnrollHelper(@onNull UdfpsEnrollHelper helper)141     void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
142         mEnrollHelper = helper;
143     }
144 
setShouldSkipDraw(boolean skipDraw)145     void setShouldSkipDraw(boolean skipDraw) {
146         if (mSkipDraw == skipDraw) {
147             return;
148         }
149         mSkipDraw = skipDraw;
150         invalidateSelf();
151     }
152 
updateFingerprintIconBounds(@onNull Rect bounds)153     void updateFingerprintIconBounds(@NonNull Rect bounds) {
154         mFingerprintDrawable.setBounds(bounds);
155         invalidateSelf();
156         mMovingTargetFpIcon.setBounds(bounds);
157         invalidateSelf();
158     }
159 
onEnrollmentProgress(int remaining, int totalSteps)160     void onEnrollmentProgress(int remaining, int totalSteps) {
161         if (mEnrollHelper == null) {
162             return;
163         }
164 
165         if (!mEnrollHelper.isCenterEnrollmentStage()) {
166             if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
167                 mTargetAnimatorSet.end();
168             }
169 
170             final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
171             if (mCurrentX != point.x || mCurrentY != point.y) {
172                 final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
173                 x.addUpdateListener(animation -> {
174                     mCurrentX = (float) animation.getAnimatedValue();
175                     invalidateSelf();
176                 });
177 
178                 final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
179                 y.addUpdateListener(animation -> {
180                     mCurrentY = (float) animation.getAnimatedValue();
181                     invalidateSelf();
182                 });
183 
184                 final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
185                 final long duration = isMovingToCenter
186                         ? TARGET_ANIM_DURATION_SHORT
187                         : TARGET_ANIM_DURATION_LONG;
188 
189                 final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
190                 scale.setDuration(duration);
191                 scale.addUpdateListener(animation -> {
192                     // Grow then shrink
193                     mCurrentScale = 1
194                             + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
195                     invalidateSelf();
196                 });
197 
198                 mTargetAnimatorSet = new AnimatorSet();
199 
200                 mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
201                 mTargetAnimatorSet.setDuration(duration);
202                 mTargetAnimatorSet.addListener(mTargetAnimListener);
203                 mTargetAnimatorSet.playTogether(x, y, scale);
204                 mTargetAnimatorSet.start();
205             } else {
206                 updateTipHintVisibility();
207             }
208         } else {
209             updateTipHintVisibility();
210         }
211 
212         updateEdgeHintVisibility();
213     }
214 
215     @Override
draw(@onNull Canvas canvas)216     public void draw(@NonNull Canvas canvas) {
217         if (mSkipDraw) {
218             return;
219         }
220 
221         // Draw moving target
222         if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
223             canvas.save();
224             canvas.translate(mCurrentX, mCurrentY);
225 
226             if (mSensorRect != null) {
227                 canvas.scale(mCurrentScale, mCurrentScale,
228                         mSensorRect.centerX(), mSensorRect.centerY());
229                 canvas.drawOval(mSensorRect, mBlueFill);
230             }
231 
232             mMovingTargetFpIcon.draw(canvas);
233             canvas.restore();
234         } else {
235             if (mSensorRect != null) {
236                 canvas.drawOval(mSensorRect, mSensorOutlinePaint);
237             }
238             mFingerprintDrawable.draw(canvas);
239             mFingerprintDrawable.setAlpha(getAlpha());
240             mSensorOutlinePaint.setAlpha(getAlpha());
241         }
242 
243     }
244 
245     @Override
setAlpha(int alpha)246     public void setAlpha(int alpha) {
247         mAlpha = alpha;
248         mFingerprintDrawable.setAlpha(alpha);
249         mSensorOutlinePaint.setAlpha(alpha);
250         mBlueFill.setAlpha(alpha);
251         mMovingTargetFpIcon.setAlpha(alpha);
252         invalidateSelf();
253     }
254 
255     @Override
getAlpha()256     public int getAlpha() {
257         return mAlpha;
258     }
259 
260     @Override
setColorFilter(@ullable ColorFilter colorFilter)261     public void setColorFilter(@Nullable ColorFilter colorFilter) {
262     }
263 
264     @Override
getOpacity()265     public int getOpacity() {
266         return 0;
267     }
268 
updateTipHintVisibility()269     private void updateTipHintVisibility() {
270         final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
271         // With the new update, we will git rid of most of this code, and instead
272         // we will change the fingerprint icon.
273         if (mShouldShowTipHint == shouldShow) {
274             return;
275         }
276         mShouldShowTipHint = shouldShow;
277     }
278 
updateEdgeHintVisibility()279     private void updateEdgeHintVisibility() {
280         final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
281         if (mShouldShowEdgeHint == shouldShow) {
282             return;
283         }
284         mShouldShowEdgeHint = shouldShow;
285     }
286 
defaultFactory(Context context)287     private ShapeDrawable defaultFactory(Context context) {
288         String fpPath = context.getResources().getString(R.string.config_udfpsIcon);
289         ShapeDrawable drawable = new ShapeDrawable(
290                 new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)
291         );
292         drawable.mutate();
293         drawable.getPaint().setStyle(Paint.Style.STROKE);
294         drawable.getPaint().setStrokeCap(Paint.Cap.ROUND);
295         drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH);
296         return drawable;
297     }
298 
loadResources(Context context, @Nullable AttributeSet attrs)299     private void loadResources(Context context, @Nullable AttributeSet attrs) {
300         final TypedArray ta = context.obtainStyledAttributes(attrs,
301                 R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
302                 R.style.BiometricsEnrollStyle);
303         mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0);
304         mMovingTargetFill = ta.getColor(
305                 R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
306         ta.recycle();
307     }
308 
309 }
310