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