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