1 /*
2  * Copyright (C) 2023 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.biometrics2.ui.widget;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.ColorFilter;
26 import android.graphics.Paint;
27 import android.graphics.PointF;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.graphics.drawable.Drawable;
31 import android.graphics.drawable.ShapeDrawable;
32 import android.graphics.drawable.shapes.PathShape;
33 import android.hardware.fingerprint.FingerprintManager;
34 import android.os.Build;
35 import android.os.UserHandle;
36 import android.provider.Settings;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.PathParser;
40 import android.util.TypedValue;
41 import android.view.accessibility.AccessibilityManager;
42 import android.view.animation.AccelerateDecelerateInterpolator;
43 
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 
47 import com.android.settings.R;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * UDFPS fingerprint drawable that is shown when enrolling
54  */
55 public class UdfpsEnrollDrawable extends Drawable {
56     private static final String TAG = "UdfpsAnimationEnroll";
57 
58     private static final long TARGET_ANIM_DURATION_LONG = 800L;
59     private static final long TARGET_ANIM_DURATION_SHORT = 600L;
60     // 1 + SCALE_MAX is the maximum that the moving target will animate to
61     private static final float SCALE_MAX = 0.25f;
62     private static final float DEFAULT_STROKE_WIDTH = 3f;
63     private static final float SCALE = 0.5f;
64     private static final String SCALE_OVERRIDE =
65             "com.android.systemui.biometrics.UdfpsEnrollHelper.scale";
66     private static final String NEW_COORDS_OVERRIDE =
67             "com.android.systemui.biometrics.UdfpsNewCoords";
68 
69     @NonNull
70     private final Drawable mMovingTargetFpIcon;
71     @NonNull
72     private final Paint mSensorOutlinePaint;
73     @NonNull
74     private final Paint mBlueFill;
75     @NonNull
76     private final ShapeDrawable mFingerprintDrawable;
77     private int mAlpha;
78     private boolean mSkipDraw = false;
79 
80     @Nullable
81     private RectF mSensorRect;
82 
83     // Moving target animator set
84     @Nullable
85     AnimatorSet mTargetAnimatorSet;
86     // Moving target location
87     float mCurrentX;
88     float mCurrentY;
89     // Moving target size
90     float mCurrentScale = 1.f;
91 
92     @NonNull
93     private final Animator.AnimatorListener mTargetAnimListener;
94 
95     private boolean mShouldShowTipHint = false;
96     private boolean mShouldShowEdgeHint = false;
97 
98     private int mEnrollIcon;
99     private int mMovingTargetFill;
100 
101     private int mTotalSteps = -1;
102     private int mRemainingSteps = -1;
103     private int mLocationsEnrolled = 0;
104     private int mCenterTouchCount = 0;
105 
106     private FingerprintManager mFingerprintManager;
107 
108     private boolean mAccessibilityEnabled;
109     private Context mContext;
110     private final List<PointF> mGuidedEnrollmentPoints;
111 
UdfpsEnrollDrawable(@onNull Context context, @Nullable AttributeSet attrs)112     UdfpsEnrollDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
113         mFingerprintDrawable = defaultFactory(context);
114 
115         loadResources(context, attrs);
116         mSensorOutlinePaint = new Paint(0 /* flags */);
117         mSensorOutlinePaint.setAntiAlias(true);
118         mSensorOutlinePaint.setColor(mMovingTargetFill);
119         mSensorOutlinePaint.setStyle(Paint.Style.FILL);
120 
121         mBlueFill = new Paint(0 /* flags */);
122         mBlueFill.setAntiAlias(true);
123         mBlueFill.setColor(mMovingTargetFill);
124         mBlueFill.setStyle(Paint.Style.FILL);
125 
126         mMovingTargetFpIcon = context.getResources()
127                 .getDrawable(R.drawable.ic_enrollment_fingerprint, null);
128         mMovingTargetFpIcon.setTint(mEnrollIcon);
129         mMovingTargetFpIcon.mutate();
130 
131         mFingerprintDrawable.setTint(mEnrollIcon);
132 
133         setAlpha(255);
134         mTargetAnimListener = new Animator.AnimatorListener() {
135             @Override
136             public void onAnimationStart(Animator animation) {
137             }
138 
139             @Override
140             public void onAnimationEnd(Animator animation) {
141                 updateTipHintVisibility();
142             }
143 
144             @Override
145             public void onAnimationCancel(Animator animation) {
146             }
147 
148             @Override
149             public void onAnimationRepeat(Animator animation) {
150             }
151         };
152         mContext = context;
153         mFingerprintManager = context.getSystemService(FingerprintManager.class);
154         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
155         mAccessibilityEnabled = am.isEnabled();
156         mGuidedEnrollmentPoints = new ArrayList<>();
157         initEnrollPoint(context);
158     }
159 
160     /** The [sensorRect] coordinates for the sensor area. */
onSensorRectUpdated(@onNull RectF sensorRect)161     void onSensorRectUpdated(@NonNull RectF sensorRect) {
162         int margin = ((int) sensorRect.height()) / 8;
163         Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin,
164                 (int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin);
165         updateFingerprintIconBounds(bounds);
166         mSensorRect = sensorRect;
167     }
168 
setShouldSkipDraw(boolean skipDraw)169     void setShouldSkipDraw(boolean skipDraw) {
170         if (mSkipDraw == skipDraw) {
171             return;
172         }
173         mSkipDraw = skipDraw;
174         invalidateSelf();
175     }
176 
updateFingerprintIconBounds(@onNull Rect bounds)177     void updateFingerprintIconBounds(@NonNull Rect bounds) {
178         mFingerprintDrawable.setBounds(bounds);
179         invalidateSelf();
180         mMovingTargetFpIcon.setBounds(bounds);
181         invalidateSelf();
182     }
183 
onEnrollmentProgress(final int remaining, final int totalSteps)184     void onEnrollmentProgress(final int remaining, final int totalSteps) {
185         if (mTotalSteps == -1) {
186             mTotalSteps = totalSteps;
187         }
188 
189         if (remaining != mRemainingSteps) {
190             mLocationsEnrolled++;
191             if (isCenterEnrollmentStage()) {
192                 mCenterTouchCount++;
193             }
194         }
195         mRemainingSteps = remaining;
196 
197         if (!isCenterEnrollmentStage()) {
198             if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
199                 mTargetAnimatorSet.end();
200             }
201 
202             final PointF point = getNextGuidedEnrollmentPoint();
203             if (mCurrentX != point.x || mCurrentY != point.y) {
204                 final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
205                 x.addUpdateListener(animation -> {
206                     mCurrentX = (float) animation.getAnimatedValue();
207                     invalidateSelf();
208                 });
209 
210                 final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
211                 y.addUpdateListener(animation -> {
212                     mCurrentY = (float) animation.getAnimatedValue();
213                     invalidateSelf();
214                 });
215 
216                 final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
217                 final long duration = isMovingToCenter
218                         ? TARGET_ANIM_DURATION_SHORT
219                         : TARGET_ANIM_DURATION_LONG;
220 
221                 final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
222                 scale.setDuration(duration);
223                 scale.addUpdateListener(animation -> {
224                     // Grow then shrink
225                     mCurrentScale = 1
226                             + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
227                     invalidateSelf();
228                 });
229 
230                 mTargetAnimatorSet = new AnimatorSet();
231 
232                 mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
233                 mTargetAnimatorSet.setDuration(duration);
234                 mTargetAnimatorSet.addListener(mTargetAnimListener);
235                 mTargetAnimatorSet.playTogether(x, y, scale);
236                 mTargetAnimatorSet.start();
237             } else {
238                 updateTipHintVisibility();
239             }
240         } else {
241             updateTipHintVisibility();
242         }
243 
244         updateEdgeHintVisibility();
245     }
246 
247     @Override
draw(@onNull Canvas canvas)248     public void draw(@NonNull Canvas canvas) {
249         if (mSkipDraw) {
250             return;
251         }
252 
253         // Draw moving target
254         if (!isCenterEnrollmentStage()) {
255             canvas.save();
256             canvas.translate(mCurrentX, mCurrentY);
257 
258             if (mSensorRect != null) {
259                 canvas.scale(mCurrentScale, mCurrentScale,
260                         mSensorRect.centerX(), mSensorRect.centerY());
261                 canvas.drawOval(mSensorRect, mBlueFill);
262             }
263 
264             mMovingTargetFpIcon.draw(canvas);
265             canvas.restore();
266         } else {
267             if (mSensorRect != null) {
268                 canvas.drawOval(mSensorRect, mSensorOutlinePaint);
269             }
270             mFingerprintDrawable.draw(canvas);
271             mFingerprintDrawable.setAlpha(getAlpha());
272             mSensorOutlinePaint.setAlpha(getAlpha());
273         }
274 
275     }
276 
277     @Override
setAlpha(int alpha)278     public void setAlpha(int alpha) {
279         mAlpha = alpha;
280         mFingerprintDrawable.setAlpha(alpha);
281         mSensorOutlinePaint.setAlpha(alpha);
282         mBlueFill.setAlpha(alpha);
283         mMovingTargetFpIcon.setAlpha(alpha);
284         invalidateSelf();
285     }
286 
287     @Override
getAlpha()288     public int getAlpha() {
289         return mAlpha;
290     }
291 
292     @Override
setColorFilter(@ullable ColorFilter colorFilter)293     public void setColorFilter(@Nullable ColorFilter colorFilter) {
294     }
295 
296     @Override
getOpacity()297     public int getOpacity() {
298         return 0;
299     }
300 
updateTipHintVisibility()301     private void updateTipHintVisibility() {
302         final boolean shouldShow = isTipEnrollmentStage();
303         // With the new update, we will git rid of most of this code, and instead
304         // we will change the fingerprint icon.
305         if (mShouldShowTipHint == shouldShow) {
306             return;
307         }
308         mShouldShowTipHint = shouldShow;
309     }
310 
updateEdgeHintVisibility()311     private void updateEdgeHintVisibility() {
312         final boolean shouldShow = isEdgeEnrollmentStage();
313         if (mShouldShowEdgeHint == shouldShow) {
314             return;
315         }
316         mShouldShowEdgeHint = shouldShow;
317     }
318 
defaultFactory(Context context)319     private ShapeDrawable defaultFactory(Context context) {
320         String fpPath = context.getResources().getString(R.string.config_udfpsIcon);
321         ShapeDrawable drawable = new ShapeDrawable(
322                 new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)
323         );
324         drawable.mutate();
325         drawable.getPaint().setStyle(Paint.Style.STROKE);
326         drawable.getPaint().setStrokeCap(Paint.Cap.ROUND);
327         drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH);
328         return drawable;
329     }
330 
loadResources(Context context, @Nullable AttributeSet attrs)331     private void loadResources(Context context, @Nullable AttributeSet attrs) {
332         final TypedArray ta = context.obtainStyledAttributes(attrs,
333                 R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
334                 R.style.BiometricsEnrollStyle);
335         mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0);
336         mMovingTargetFill = ta.getColor(
337                 R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
338         ta.recycle();
339     }
340 
isCenterEnrollmentStage()341     private boolean isCenterEnrollmentStage() {
342         if (mTotalSteps == -1 || mRemainingSteps == -1) {
343             return true;
344         }
345         return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
346     }
347 
getStageThresholdSteps(int totalSteps, int stageIndex)348     private int getStageThresholdSteps(int totalSteps, int stageIndex) {
349         return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
350     }
351 
getNextGuidedEnrollmentPoint()352     private PointF getNextGuidedEnrollmentPoint() {
353         if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
354             return new PointF(0f, 0f);
355         }
356 
357         float scale = SCALE;
358         if (Build.IS_ENG || Build.IS_USERDEBUG) {
359             scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
360                     SCALE_OVERRIDE, SCALE,
361                     UserHandle.USER_CURRENT);
362         }
363         final int index = mLocationsEnrolled - mCenterTouchCount;
364         final PointF originalPoint = mGuidedEnrollmentPoints
365                 .get(index % mGuidedEnrollmentPoints.size());
366         return new PointF(originalPoint.x * scale, originalPoint.y * scale);
367     }
368 
isGuidedEnrollmentStage()369     private boolean isGuidedEnrollmentStage() {
370         if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
371             return false;
372         }
373         final int progressSteps = mTotalSteps - mRemainingSteps;
374         return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
375                 && progressSteps < getStageThresholdSteps(mTotalSteps, 1);
376     }
377 
isTipEnrollmentStage()378     private boolean isTipEnrollmentStage() {
379         if (mTotalSteps == -1 || mRemainingSteps == -1) {
380             return false;
381         }
382         final int progressSteps = mTotalSteps - mRemainingSteps;
383         return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
384                 && progressSteps < getStageThresholdSteps(mTotalSteps, 2);
385     }
386 
isEdgeEnrollmentStage()387     private boolean isEdgeEnrollmentStage() {
388         if (mTotalSteps == -1 || mRemainingSteps == -1) {
389             return false;
390         }
391         return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
392     }
393 
initEnrollPoint(Context context)394     private void initEnrollPoint(Context context) {
395         // Number of pixels per mm
396         float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
397                 context.getResources().getDisplayMetrics());
398         boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(),
399                 NEW_COORDS_OVERRIDE, 0,
400                 UserHandle.USER_CURRENT) != 0;
401         if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) {
402             Log.v(TAG, "Using new coordinates");
403             mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, -1.02f * px));
404             mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, 1.02f * px));
405             mGuidedEnrollmentPoints.add(new PointF(0.29f * px, 0.00f * px));
406             mGuidedEnrollmentPoints.add(new PointF(2.17f * px, -2.35f * px));
407             mGuidedEnrollmentPoints.add(new PointF(1.07f * px, -3.96f * px));
408             mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, -4.31f * px));
409             mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, -3.29f * px));
410             mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, -1.23f * px));
411             mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, 1.23f * px));
412             mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, 3.29f * px));
413             mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, 4.31f * px));
414             mGuidedEnrollmentPoints.add(new PointF(1.07f * px, 3.96f * px));
415             mGuidedEnrollmentPoints.add(new PointF(2.17f * px, 2.35f * px));
416             mGuidedEnrollmentPoints.add(new PointF(2.58f * px, 0.00f * px));
417         } else {
418             Log.v(TAG, "Using old coordinates");
419             mGuidedEnrollmentPoints.add(new PointF(2.00f * px, 0.00f * px));
420             mGuidedEnrollmentPoints.add(new PointF(0.87f * px, -2.70f * px));
421             mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, -1.31f * px));
422             mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, 1.31f * px));
423             mGuidedEnrollmentPoints.add(new PointF(0.88f * px, 2.70f * px));
424             mGuidedEnrollmentPoints.add(new PointF(3.94f * px, -1.06f * px));
425             mGuidedEnrollmentPoints.add(new PointF(2.90f * px, -4.14f * px));
426             mGuidedEnrollmentPoints.add(new PointF(-0.52f * px, -5.95f * px));
427             mGuidedEnrollmentPoints.add(new PointF(-3.33f * px, -3.33f * px));
428             mGuidedEnrollmentPoints.add(new PointF(-3.99f * px, -0.35f * px));
429             mGuidedEnrollmentPoints.add(new PointF(-3.62f * px, 2.54f * px));
430             mGuidedEnrollmentPoints.add(new PointF(-1.49f * px, 5.57f * px));
431             mGuidedEnrollmentPoints.add(new PointF(2.29f * px, 4.92f * px));
432             mGuidedEnrollmentPoints.add(new PointF(3.82f * px, 1.78f * px));
433         }
434     }
435 
436 }
437