• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.settings.biometrics.fingerprint;
18 
19 import android.animation.ValueAnimator;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.ColorFilter;
24 import android.graphics.Paint;
25 import android.graphics.drawable.Drawable;
26 import android.os.Process;
27 import android.os.VibrationAttributes;
28 import android.os.VibrationEffect;
29 import android.os.Vibrator;
30 import android.util.AttributeSet;
31 import android.util.DisplayMetrics;
32 import android.view.accessibility.AccessibilityManager;
33 import android.view.animation.DecelerateInterpolator;
34 import android.view.animation.Interpolator;
35 import android.view.animation.OvershootInterpolator;
36 
37 import androidx.annotation.ColorInt;
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.settings.R;
43 
44 /**
45  * UDFPS enrollment progress bar.
46  */
47 public class UdfpsEnrollProgressBarDrawable extends Drawable {
48     private static final String TAG = "UdfpsProgressBar";
49 
50     private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L;
51     private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L;
52     private static final long FILL_COLOR_ANIMATION_DURATION_MS = 350L;
53     private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
54     private static final float STROKE_WIDTH_DP = 12f;
55     private static final Interpolator DEACCEL = new DecelerateInterpolator();
56 
57     private static final VibrationEffect VIBRATE_EFFECT_ERROR =
58             VibrationEffect.createWaveform(new long[]{0, 5, 55, 60}, -1);
59     private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
60             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
61 
62     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
63             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
64 
65     private static final VibrationEffect SUCCESS_VIBRATION_EFFECT =
66             VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
67 
68     private final float mStrokeWidthPx;
69     @ColorInt
70     private final int mProgressColor;
71     @ColorInt
72     private final int mHelpColor;
73     @ColorInt
74     private final int mOnFirstBucketFailedColor;
75     @NonNull
76     private final Drawable mCheckmarkDrawable;
77     @NonNull
78     private final Interpolator mCheckmarkInterpolator;
79     @NonNull
80     private final Paint mBackgroundPaint;
81     @VisibleForTesting
82     @NonNull
83     final Paint mFillPaint;
84     @NonNull
85     private final Vibrator mVibrator;
86     @NonNull
87     private final boolean mIsAccessibilityEnabled;
88     @NonNull
89     private final Context mContext;
90 
91     private boolean mAfterFirstTouch;
92 
93     private int mRemainingSteps = 0;
94     private int mTotalSteps = 0;
95     private float mProgress = 0f;
96     @Nullable
97     private ValueAnimator mProgressAnimator;
98     @NonNull
99     private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
100 
101     private boolean mShowingHelp = false;
102     @Nullable
103     private ValueAnimator mFillColorAnimator;
104     @NonNull
105     private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
106 
107     @Nullable
108     private ValueAnimator mBackgroundColorAnimator;
109     @NonNull
110     private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdateListener;
111 
112     private boolean mComplete = false;
113     private float mCheckmarkScale = 0f;
114     @Nullable
115     private ValueAnimator mCheckmarkAnimator;
116     @NonNull
117     private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener;
118 
119     private int mMovingTargetFill;
120     private int mMovingTargetFillError;
121     private int mEnrollProgress;
122     private int mEnrollProgressHelp;
123     private int mEnrollProgressHelpWithTalkback;
124 
UdfpsEnrollProgressBarDrawable(@onNull Context context, @Nullable AttributeSet attrs)125     public UdfpsEnrollProgressBarDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
126         mContext = context;
127 
128         loadResources(context, attrs);
129         float density = context.getResources().getDisplayMetrics().densityDpi;
130         mStrokeWidthPx = STROKE_WIDTH_DP * (density / DisplayMetrics.DENSITY_DEFAULT);
131         mProgressColor = mEnrollProgress;
132         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
133         mIsAccessibilityEnabled = am.isTouchExplorationEnabled();
134         mOnFirstBucketFailedColor = mMovingTargetFillError;
135         if (!mIsAccessibilityEnabled) {
136             mHelpColor = mEnrollProgressHelp;
137         } else {
138             mHelpColor = mEnrollProgressHelpWithTalkback;
139         }
140         mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
141         mCheckmarkDrawable.mutate();
142         mCheckmarkInterpolator = new OvershootInterpolator();
143 
144         mBackgroundPaint = new Paint();
145         mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
146         mBackgroundPaint.setColor(mMovingTargetFill);
147         mBackgroundPaint.setAntiAlias(true);
148         mBackgroundPaint.setStyle(Paint.Style.STROKE);
149         mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
150 
151         // Progress fill should *not* use the extracted system color.
152         mFillPaint = new Paint();
153         mFillPaint.setStrokeWidth(mStrokeWidthPx);
154         mFillPaint.setColor(mProgressColor);
155         mFillPaint.setAntiAlias(true);
156         mFillPaint.setStyle(Paint.Style.STROKE);
157         mFillPaint.setStrokeCap(Paint.Cap.ROUND);
158 
159         mVibrator = mContext.getSystemService(Vibrator.class);
160 
161         mProgressUpdateListener = animation -> {
162             mProgress = (float) animation.getAnimatedValue();
163             invalidateSelf();
164         };
165 
166         mFillColorUpdateListener = animation -> {
167             mFillPaint.setColor((int) animation.getAnimatedValue());
168             invalidateSelf();
169         };
170 
171         mCheckmarkUpdateListener = animation -> {
172             mCheckmarkScale = (float) animation.getAnimatedValue();
173             invalidateSelf();
174         };
175 
176         mBackgroundColorUpdateListener = animation -> {
177             mBackgroundPaint.setColor((int) animation.getAnimatedValue());
178             invalidateSelf();
179         };
180     }
181 
onEnrollmentProgress(int remaining, int totalSteps)182     void onEnrollmentProgress(int remaining, int totalSteps) {
183         mAfterFirstTouch = true;
184         updateState(remaining, totalSteps, false /* showingHelp */);
185     }
186 
onEnrollmentHelp(int remaining, int totalSteps)187     void onEnrollmentHelp(int remaining, int totalSteps) {
188         updateState(remaining, totalSteps, true /* showingHelp */);
189     }
190 
onLastStepAcquired()191     void onLastStepAcquired() {
192         updateState(0, mTotalSteps, false /* showingHelp */);
193     }
194 
updateState(int remainingSteps, int totalSteps, boolean showingHelp)195     private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) {
196         updateProgress(remainingSteps, totalSteps, showingHelp);
197         updateFillColor(showingHelp);
198     }
199 
updateProgress(int remainingSteps, int totalSteps, boolean showingHelp)200     private void updateProgress(int remainingSteps, int totalSteps, boolean showingHelp) {
201         if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
202             return;
203         }
204 
205         mShowingHelp = showingHelp;
206         if (mShowingHelp) {
207             if (mVibrator != null && mIsAccessibilityEnabled) {
208                 mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
209                         VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::onEnrollmentHelp",
210                         FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
211             }
212         } else {
213             // If the first touch is an error, remainingSteps will be -1 and the callback
214             // doesn't come from onEnrollmentHelp. If we are in the accessibility flow,
215             // we still would like to vibrate.
216             if (mVibrator != null) {
217                 if (remainingSteps == -1 && mIsAccessibilityEnabled) {
218                     mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
219                             VIBRATE_EFFECT_ERROR,
220                             getClass().getSimpleName() + "::onFirstTouchError",
221                             FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
222                 } else if (remainingSteps != -1 && !mIsAccessibilityEnabled) {
223                     mVibrator.vibrate(Process.myUid(),
224                             mContext.getOpPackageName(),
225                             SUCCESS_VIBRATION_EFFECT,
226                             getClass().getSimpleName() + "::OnEnrollmentProgress",
227                             HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
228                 }
229             }
230         }
231 
232         mRemainingSteps = remainingSteps;
233         mTotalSteps = totalSteps;
234 
235         final int progressSteps = Math.max(0, totalSteps - remainingSteps);
236 
237         // If needed, add 1 to progress and total steps to account for initial touch.
238         final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps;
239         final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps;
240 
241         final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal);
242 
243         if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
244             mProgressAnimator.cancel();
245         }
246 
247         mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress);
248         mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS);
249         mProgressAnimator.addUpdateListener(mProgressUpdateListener);
250         mProgressAnimator.start();
251 
252         if (remainingSteps == 0) {
253             startCompletionAnimation();
254         } else if (remainingSteps > 0) {
255             rollBackCompletionAnimation();
256         }
257     }
258 
animateBackgroundColor()259     private void animateBackgroundColor() {
260         if (mBackgroundColorAnimator != null && mBackgroundColorAnimator.isRunning()) {
261             mBackgroundColorAnimator.end();
262         }
263         mBackgroundColorAnimator = ValueAnimator.ofArgb(mBackgroundPaint.getColor(),
264                 mOnFirstBucketFailedColor);
265         mBackgroundColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
266         mBackgroundColorAnimator.setRepeatCount(1);
267         mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
268         mBackgroundColorAnimator.setInterpolator(DEACCEL);
269         mBackgroundColorAnimator.addUpdateListener(mBackgroundColorUpdateListener);
270         mBackgroundColorAnimator.start();
271     }
272 
updateFillColor(boolean showingHelp)273     private void updateFillColor(boolean showingHelp) {
274         if (!mAfterFirstTouch && showingHelp) {
275             // If we are on the first touch, animate the background color
276             // instead of the progress color.
277             animateBackgroundColor();
278             return;
279         }
280 
281         if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
282             mFillColorAnimator.end();
283         }
284 
285         @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor;
286         mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor);
287         mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
288         mFillColorAnimator.setRepeatCount(1);
289         mFillColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
290         mFillColorAnimator.setInterpolator(DEACCEL);
291         mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
292         mFillColorAnimator.start();
293     }
294 
startCompletionAnimation()295     private void startCompletionAnimation() {
296         if (mComplete) {
297             return;
298         }
299         mComplete = true;
300 
301         if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
302             mCheckmarkAnimator.cancel();
303         }
304 
305         mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f);
306         mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS);
307         mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS);
308         mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator);
309         mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
310         mCheckmarkAnimator.start();
311     }
312 
rollBackCompletionAnimation()313     private void rollBackCompletionAnimation() {
314         if (!mComplete) {
315             return;
316         }
317         mComplete = false;
318 
319         // Adjust duration based on how much of the completion animation has played.
320         final float animatedFraction = mCheckmarkAnimator != null
321                 ? mCheckmarkAnimator.getAnimatedFraction()
322                 : 0f;
323         final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction);
324 
325         if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
326             mCheckmarkAnimator.cancel();
327         }
328 
329         mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f);
330         mCheckmarkAnimator.setDuration(durationMs);
331         mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
332         mCheckmarkAnimator.start();
333     }
334 
loadResources(Context context, @Nullable AttributeSet attrs)335     private void loadResources(Context context, @Nullable AttributeSet attrs) {
336         final TypedArray ta = context.obtainStyledAttributes(attrs,
337                 R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
338                 R.style.BiometricsEnrollStyle);
339         mMovingTargetFill = ta.getColor(
340                 R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
341         mMovingTargetFillError = ta.getColor(
342                 R.styleable.BiometricsEnrollView_biometricsMovingTargetFillError, 0);
343         mEnrollProgress = ta.getColor(
344                 R.styleable.BiometricsEnrollView_biometricsEnrollProgress, 0);
345         mEnrollProgressHelp = ta.getColor(
346                 R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelp, 0);
347         mEnrollProgressHelpWithTalkback = ta.getColor(
348                 R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0);
349         ta.recycle();
350     }
351 
352     @Override
draw(@onNull Canvas canvas)353     public void draw(@NonNull Canvas canvas) {
354         canvas.save();
355 
356         // Progress starts from the top, instead of the right
357         canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
358 
359         final float halfPaddingPx = mStrokeWidthPx / 2f;
360 
361         if (mProgress < 1f) {
362             // Draw the background color of the progress circle.
363             canvas.drawArc(
364                     halfPaddingPx,
365                     halfPaddingPx,
366                     getBounds().right - halfPaddingPx,
367                     getBounds().bottom - halfPaddingPx,
368                     0f /* startAngle */,
369                     360f /* sweepAngle */,
370                     false /* useCenter */,
371                     mBackgroundPaint);
372         }
373 
374         if (mProgress > 0f) {
375             // Draw the filled portion of the progress circle.
376             canvas.drawArc(
377                     halfPaddingPx,
378                     halfPaddingPx,
379                     getBounds().right - halfPaddingPx,
380                     getBounds().bottom - halfPaddingPx,
381                     0f /* startAngle */,
382                     360f * mProgress /* sweepAngle */,
383                     false /* useCenter */,
384                     mFillPaint);
385         }
386 
387         canvas.restore();
388 
389         if (mCheckmarkScale > 0f) {
390             final float offsetScale = (float) Math.sqrt(2) / 2f;
391             final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale;
392             final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale;
393             final float centerX = getBounds().centerX() + centerXOffset;
394             final float centerY = getBounds().centerY() + centerYOffset;
395 
396             final float boundsXOffset =
397                     mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale;
398             final float boundsYOffset =
399                     mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale;
400 
401             final int left = Math.round(centerX - boundsXOffset);
402             final int top = Math.round(centerY - boundsYOffset);
403             final int right = Math.round(centerX + boundsXOffset);
404             final int bottom = Math.round(centerY + boundsYOffset);
405             mCheckmarkDrawable.setBounds(left, top, right, bottom);
406             mCheckmarkDrawable.draw(canvas);
407         }
408     }
409 
410     @Override
setAlpha(int alpha)411     public void setAlpha(int alpha) {}
412 
413     @Override
setColorFilter(@ullable ColorFilter colorFilter)414     public void setColorFilter(@Nullable ColorFilter colorFilter) {
415     }
416 
417     @Override
getOpacity()418     public int getOpacity() {
419         return 0;
420     }
421 }
422