diff --git a/res/drawable/ic_enrollment_fingerprint.xml b/res/drawable/ic_enrollment_fingerprint.xml
new file mode 100644
index 00000000000..e5c4c6bcf5a
--- /dev/null
+++ b/res/drawable/ic_enrollment_fingerprint.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/udfps_enroll_checkmark.xml b/res/drawable/udfps_enroll_checkmark.xml
new file mode 100644
index 00000000000..f8169d377f1
--- /dev/null
+++ b/res/drawable/udfps_enroll_checkmark.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/res/layout/udfps_enroll_enrolling.xml b/res/layout/udfps_enroll_enrolling.xml
index e9337538629..c97591d6d52 100644
--- a/res/layout/udfps_enroll_enrolling.xml
+++ b/res/layout/udfps_enroll_enrolling.xml
@@ -39,6 +39,7 @@
android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
index 1fbd5aa8ef6..959bc172793 100644
--- a/res/values-night/colors.xml
+++ b/res/values-night/colors.xml
@@ -57,5 +57,15 @@
@android:color/white
+
+
+
+ #7DA7F1
+ #475670
+
+ #80475670
+ #7DA7F1
+ #607DA7F1
+ #FFEE675C
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 599a145bd1d..90a308eee9f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -184,4 +184,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
index a4e6f7065cb..5d98e94f083 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -172,4 +172,13 @@
?android:attr/textColorPrimary
+
+
+ #699FF3
+ #C2D7F7
+
+ #80C2D7F7
+ #699FF3
+ #70699FF3
+ #FFEE675C
diff --git a/res/values/config.xml b/res/values/config.xml
index bbacc5c23ca..89e374eddb5 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -636,4 +636,25 @@
true
+
+
+
+ 280
+
+
+
+
+ M25.5,16.3283C28.47,14.8433 31.9167,14 35.5834,14C39.2501,14 42.6968,14.8433 45.6668,16.3283
+ M20,28.6669C22.7683,24.3402 28.7084,21.3335 35.5834,21.3335C42.4585,21.3335 48.3985,
+ 24.3402 51.1669,28.6669
+ M22.8607,47.0002C21.834,44.3235 21.834,41.5002 21.834,41.5002C21.834,
+ 34.4051 27.7374,28.6667 35.5841,28.6667C43.4308,28.6667 49.3341,34.4051 49.3341,41.5002
+ M49.3344,41.5003V42.0319C49.3344,44.7636 47.1161,47.0003 44.3661,47.0003C41.9461,
+ 47.0003 39.8744,45.2403 39.471,42.857L38.9577,
+ 39.7769C38.591,37.5953 36.7027,36.0002 34.5027,
+ 36.0002C26.5826,36.0002 29.846,49.1087 35.291,50.6487
+ M44.9713,54.6267C42.5513,56.7167 39.2879,58.0001 35.5846,58.0001C32.2296,
+ 58.0001 29.2229,56.9551 26.8945,55.195
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a97c3d56eb0..4093a00e919 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -11490,4 +11490,7 @@
=1 {Apps installed more than # month ago}
other {Apps installed more than # months ago}
}
+
+
+ Fingerprint sensor
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 20ebe255fa2..a07eeadd84b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -905,4 +905,13 @@
- false
- true
+
+
diff --git a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
index cedbec1a0ac..9a89f7a349f 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
@@ -40,6 +40,11 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
void onEnrollmentHelp(int helpMsgId, CharSequence helpString);
void onEnrollmentError(int errMsgId, CharSequence errString);
void onEnrollmentProgressChange(int steps, int remaining);
+ /**
+ * Called when a fingerprint image has been acquired.
+ * @param isAcquiredGood whether the fingerprint image was good.
+ */
+ default void onAcquired(boolean isAcquiredGood) { }
}
private int mEnrollmentSteps = -1;
@@ -100,6 +105,19 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
}
}
+ private class QueuedAcquired extends QueuedEvent {
+ private final boolean isAcquiredGood;
+
+ public QueuedAcquired(boolean isAcquiredGood) {
+ this.isAcquiredGood = isAcquiredGood;
+ }
+
+ @Override
+ public void send(Listener listener) {
+ listener.onAcquired(isAcquiredGood);
+ }
+ }
+
private final Runnable mTimeoutRunnable = new Runnable() {
@Override
public void run() {
@@ -189,6 +207,14 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
mEnrolling = false;
}
+ protected void onAcquired(boolean isAcquiredGood) {
+ if (mListener != null) {
+ mListener.onAcquired(isAcquiredGood);
+ } else {
+ mQueuedEvents.add(new QueuedAcquired(isAcquiredGood));
+ }
+ }
+
public void setListener(Listener listener) {
mListener = listener;
if (mListener != null) {
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 5b8cd680ab1..6d144abb051 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -34,11 +34,13 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
import android.os.Process;
@@ -46,15 +48,21 @@ import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
+import android.util.DisplayUtils;
+import android.util.FeatureFlagUtils;
import android.util.Log;
+import android.view.Display;
+import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
+import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -157,6 +165,9 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
private boolean mCanAssumeUdfps;
private boolean mCanAssumeSfps;
@Nullable private ProgressBar mProgressBar;
+ @Nullable private UdfpsEnrollHelper mUdfpsEnrollHelper;
+ // TODO(b/260617060): Do not hard-code mScaleFactor, referring to AuthController.
+ private float mScaleFactor = 1.0f;
private ObjectAnimator mProgressAnim;
private TextView mErrorText;
private Interpolator mFastOutSlowInInterpolator;
@@ -245,7 +256,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
listenOrientationEvent();
if (mCanAssumeUdfps) {
- switch(getApplicationContext().getDisplay().getRotation()) {
+ int rotation = getApplicationContext().getDisplay().getRotation();
+ switch (rotation) {
case Surface.ROTATION_90:
final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate(
R.layout.udfps_enroll_enrolling, null, false);
@@ -260,8 +272,12 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
layoutContainer.setPaddingRelative((int) getResources().getDimension(
R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
? 0 : (int) getResources().getDimension(
- R.dimen.rotation_90_enroll_padding_end), 0);
+ R.dimen.rotation_90_enroll_padding_end), 0);
layoutContainer.setLayoutParams(lp);
+ if (FeatureFlagUtils.isEnabled(getApplicationContext(),
+ FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
+ layout.addView(addUdfpsEnrollView(props.get(0)));
+ }
setContentView(layout, lp);
break;
@@ -269,7 +285,31 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
case Surface.ROTATION_180:
case Surface.ROTATION_270:
default:
- setContentView(R.layout.udfps_enroll_enrolling);
+ final GlifLayout defaultLayout = (GlifLayout) getLayoutInflater().inflate(
+ R.layout.udfps_enroll_enrolling, null, false);
+ if (FeatureFlagUtils.isEnabled(getApplicationContext(),
+ FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
+ if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+ // In the portrait mode, set layout_container's height 0, so it's
+ // always shown at the bottom of the screen.
+ // Add udfps enroll view into layout_container instead of
+ // udfps_enroll_enrolling, so that when the content is too long to
+ // make udfps_enroll_enrolling larger than the screen, udfps enroll
+ // view could still be set to right position by setting bottom margin to
+ // its parent view (layout_container) because it's always at the
+ // bottom of the screen.
+ final FrameLayout portraitLayoutContainer = defaultLayout.findViewById(
+ R.id.layout_container);
+ final ViewGroup.LayoutParams containerLp =
+ portraitLayoutContainer.getLayoutParams();
+ containerLp.height = 0;
+ portraitLayoutContainer.addView(addUdfpsEnrollView(props.get(0)));
+ } else if (rotation == Surface.ROTATION_270) {
+ defaultLayout.addView(addUdfpsEnrollView(props.get(0)));
+ }
+ }
+
+ setContentView(defaultLayout);
break;
}
setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
@@ -766,6 +806,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
mErrorText.removeCallbacks(mTouchAgainRunnable);
}
showError(helpString);
+
+ if (mUdfpsEnrollHelper != null) mUdfpsEnrollHelper.onEnrollmentHelp();
}
}
@@ -813,6 +855,13 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
}
}
+ @Override
+ public void onAcquired(boolean isAcquiredGood) {
+ if (mUdfpsEnrollHelper != null) {
+ mUdfpsEnrollHelper.onAcquired(isAcquiredGood);
+ }
+ }
+
private void updateProgress(boolean animate) {
if (mSidecar == null || !mSidecar.isEnrolling()) {
Log.d(TAG, "Enrollment not started yet");
@@ -826,6 +875,12 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
if (mProgressBar != null && mProgressBar.getProgress() < progress) {
clearError();
}
+
+ if (mUdfpsEnrollHelper != null) {
+ mUdfpsEnrollHelper.onEnrollmentProgress(mSidecar.getEnrollmentSteps(),
+ mSidecar.getEnrollmentRemaining());
+ }
+
if (animate) {
animateProgress(progress);
} else {
@@ -1097,6 +1152,50 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
}
}
+ private UdfpsEnrollView addUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps) {
+ UdfpsEnrollView udfpsEnrollView = (UdfpsEnrollView) getLayoutInflater().inflate(
+ R.layout.udfps_enroll_view, null, false);
+
+ DisplayInfo displayInfo = new DisplayInfo();
+ getDisplay().getDisplayInfo(displayInfo);
+ final Display.Mode maxDisplayMode =
+ DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes);
+ final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio(
+ maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(),
+ displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight());
+ if (scaleFactor == Float.POSITIVE_INFINITY) {
+ mScaleFactor = 1f;
+ } else {
+ mScaleFactor = scaleFactor;
+ }
+
+ Rect udfpsBounds = udfpsProps.getLocation().getRect();
+ udfpsBounds.scale(mScaleFactor);
+
+ final Rect overlayBounds = new Rect(
+ 0, /* left */
+ displayInfo.getNaturalHeight() / 2, /* top */
+ displayInfo.getNaturalWidth(), /* right */
+ displayInfo.getNaturalHeight() /* botom */);
+
+ // TODO(b/260617060): Extract this logic into a 3rd party library for both Settings and
+ // SysUI to depend on.
+ UdfpsOverlayParams params = new UdfpsOverlayParams(
+ udfpsBounds,
+ overlayBounds,
+ displayInfo.getNaturalWidth(),
+ displayInfo.getNaturalHeight(),
+ mScaleFactor,
+ displayInfo.rotation,
+ udfpsProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+
+ udfpsEnrollView.setOverlayParams(params);
+ mUdfpsEnrollHelper = new UdfpsEnrollHelper(getApplicationContext(), mFingerprintManager);
+ udfpsEnrollView.setEnrollHelper(mUdfpsEnrollHelper);
+
+ return udfpsEnrollView;
+ }
+
public static class IconTouchDialog extends InstrumentedDialogFragment {
@Override
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
index b3b99753cf7..3adfc28b113 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
@@ -108,6 +108,11 @@ public class FingerprintEnrollSidecar extends BiometricEnrollSidecar {
FingerprintEnrollSidecar.super.onEnrollmentProgress(remaining);
}
+ @Override
+ public void onAcquired(boolean isAcquiredGood) {
+ FingerprintEnrollSidecar.super.onAcquired(isAcquiredGood);
+ }
+
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
FingerprintEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString);
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
index 66ed0856828..25689374da8 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
@@ -91,6 +91,11 @@ public class FingerprintUpdater {
BiometricsSafetySource.onBiometricsChanged(mContext); // biometrics data changed
}
}
+
+ @Override
+ public void onAcquired(boolean isAcquiredGood) {
+ mCallback.onAcquired(isAcquiredGood);
+ }
}
/**
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollDrawable.java
new file mode 100644
index 00000000000..480b17de8ae
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollDrawable.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint;
+
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.util.AttributeSet;
+import android.util.PathParser;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+/**
+ * UDFPS fingerprint drawable that is shown when enrolling
+ */
+public class UdfpsEnrollDrawable extends Drawable {
+ private static final String TAG = "UdfpsAnimationEnroll";
+
+ private static final long TARGET_ANIM_DURATION_LONG = 800L;
+ private static final long TARGET_ANIM_DURATION_SHORT = 600L;
+ // 1 + SCALE_MAX is the maximum that the moving target will animate to
+ private static final float SCALE_MAX = 0.25f;
+ private static final float DEFAULT_STROKE_WIDTH = 3f;
+
+ @NonNull
+ private final Drawable mMovingTargetFpIcon;
+ @NonNull
+ private final Paint mSensorOutlinePaint;
+ @NonNull
+ private final Paint mBlueFill;
+ @NonNull
+ private final ShapeDrawable mFingerprintDrawable;
+
+ private int mAlpha;
+ private boolean mSkipDraw = false;
+
+ @Nullable
+ private RectF mSensorRect;
+ @Nullable
+ private UdfpsEnrollHelper mEnrollHelper;
+
+ // Moving target animator set
+ @Nullable
+ AnimatorSet mTargetAnimatorSet;
+ // Moving target location
+ float mCurrentX;
+ float mCurrentY;
+ // Moving target size
+ float mCurrentScale = 1.f;
+
+ @NonNull
+ private final Animator.AnimatorListener mTargetAnimListener;
+
+ private boolean mShouldShowTipHint = false;
+ private boolean mShouldShowEdgeHint = false;
+
+ private int mEnrollIcon;
+ private int mMovingTargetFill;
+
+ UdfpsEnrollDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
+ mFingerprintDrawable = defaultFactory(context);
+
+ loadResources(context, attrs);
+ mSensorOutlinePaint = new Paint(0 /* flags */);
+ mSensorOutlinePaint.setAntiAlias(true);
+ mSensorOutlinePaint.setColor(mMovingTargetFill);
+ mSensorOutlinePaint.setStyle(Paint.Style.FILL);
+
+ mBlueFill = new Paint(0 /* flags */);
+ mBlueFill.setAntiAlias(true);
+ mBlueFill.setColor(mMovingTargetFill);
+ mBlueFill.setStyle(Paint.Style.FILL);
+
+ mMovingTargetFpIcon = context.getResources()
+ .getDrawable(R.drawable.ic_enrollment_fingerprint, null);
+ mMovingTargetFpIcon.setTint(mEnrollIcon);
+ mMovingTargetFpIcon.mutate();
+
+ mFingerprintDrawable.setTint(mEnrollIcon);
+
+ setAlpha(255);
+ mTargetAnimListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ updateTipHintVisibility();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ };
+ }
+
+ /** The [sensorRect] coordinates for the sensor area. */
+ void onSensorRectUpdated(@NonNull RectF sensorRect) {
+ int margin = ((int) sensorRect.height()) / 8;
+ Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin,
+ (int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin);
+ updateFingerprintIconBounds(bounds);
+ mSensorRect = sensorRect;
+ }
+
+ void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
+ mEnrollHelper = helper;
+ }
+
+ void setShouldSkipDraw(boolean skipDraw) {
+ if (mSkipDraw == skipDraw) {
+ return;
+ }
+ mSkipDraw = skipDraw;
+ invalidateSelf();
+ }
+
+ void updateFingerprintIconBounds(@NonNull Rect bounds) {
+ mFingerprintDrawable.setBounds(bounds);
+ invalidateSelf();
+ mMovingTargetFpIcon.setBounds(bounds);
+ invalidateSelf();
+ }
+
+ void onEnrollmentProgress(int remaining, int totalSteps) {
+ if (mEnrollHelper == null) {
+ return;
+ }
+
+ if (!mEnrollHelper.isCenterEnrollmentStage()) {
+ if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
+ mTargetAnimatorSet.end();
+ }
+
+ final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
+ if (mCurrentX != point.x || mCurrentY != point.y) {
+ final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
+ x.addUpdateListener(animation -> {
+ mCurrentX = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ });
+
+ final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
+ y.addUpdateListener(animation -> {
+ mCurrentY = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ });
+
+ final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
+ final long duration = isMovingToCenter
+ ? TARGET_ANIM_DURATION_SHORT
+ : TARGET_ANIM_DURATION_LONG;
+
+ final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
+ scale.setDuration(duration);
+ scale.addUpdateListener(animation -> {
+ // Grow then shrink
+ mCurrentScale = 1
+ + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
+ invalidateSelf();
+ });
+
+ mTargetAnimatorSet = new AnimatorSet();
+
+ mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+ mTargetAnimatorSet.setDuration(duration);
+ mTargetAnimatorSet.addListener(mTargetAnimListener);
+ mTargetAnimatorSet.playTogether(x, y, scale);
+ mTargetAnimatorSet.start();
+ } else {
+ updateTipHintVisibility();
+ }
+ } else {
+ updateTipHintVisibility();
+ }
+
+ updateEdgeHintVisibility();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ if (mSkipDraw) {
+ return;
+ }
+
+ // Draw moving target
+ if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
+ canvas.save();
+ canvas.translate(mCurrentX, mCurrentY);
+
+ if (mSensorRect != null) {
+ canvas.scale(mCurrentScale, mCurrentScale,
+ mSensorRect.centerX(), mSensorRect.centerY());
+ canvas.drawOval(mSensorRect, mBlueFill);
+ }
+
+ mMovingTargetFpIcon.draw(canvas);
+ canvas.restore();
+ } else {
+ if (mSensorRect != null) {
+ canvas.drawOval(mSensorRect, mSensorOutlinePaint);
+ }
+ mFingerprintDrawable.draw(canvas);
+ mFingerprintDrawable.setAlpha(getAlpha());
+ mSensorOutlinePaint.setAlpha(getAlpha());
+ }
+
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ mFingerprintDrawable.setAlpha(alpha);
+ mSensorOutlinePaint.setAlpha(alpha);
+ mBlueFill.setAlpha(alpha);
+ mMovingTargetFpIcon.setAlpha(alpha);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ }
+
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ private void updateTipHintVisibility() {
+ final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
+ // With the new update, we will git rid of most of this code, and instead
+ // we will change the fingerprint icon.
+ if (mShouldShowTipHint == shouldShow) {
+ return;
+ }
+ mShouldShowTipHint = shouldShow;
+ }
+
+ private void updateEdgeHintVisibility() {
+ final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
+ if (mShouldShowEdgeHint == shouldShow) {
+ return;
+ }
+ mShouldShowEdgeHint = shouldShow;
+ }
+
+ private ShapeDrawable defaultFactory(Context context) {
+ String fpPath = context.getResources().getString(R.string.config_udfpsIcon);
+ ShapeDrawable drawable = new ShapeDrawable(
+ new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)
+ );
+ drawable.mutate();
+ drawable.getPaint().setStyle(Paint.Style.STROKE);
+ drawable.getPaint().setStrokeCap(Paint.Cap.ROUND);
+ drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH);
+ return drawable;
+ }
+
+ private void loadResources(Context context, @Nullable AttributeSet attrs) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs,
+ R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
+ R.style.BiometricsEnrollStyle);
+ mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0);
+ mMovingTargetFill = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
+ ta.recycle();
+ }
+
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
new file mode 100644
index 00000000000..f42b8eca2fc
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.PointF;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.accessibility.AccessibilityManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helps keep track of enrollment state and animates the progress bar accordingly.
+ */
+public class UdfpsEnrollHelper {
+ private static final String TAG = "UdfpsEnrollHelper";
+
+ private static final String SCALE_OVERRIDE =
+ "com.android.systemui.biometrics.UdfpsEnrollHelper.scale";
+ private static final float SCALE = 0.5f;
+
+ private static final String NEW_COORDS_OVERRIDE =
+ "com.android.systemui.biometrics.UdfpsNewCoords";
+
+ interface Listener {
+ void onEnrollmentProgress(int remaining, int totalSteps);
+
+ void onEnrollmentHelp(int remaining, int totalSteps);
+
+ void onAcquired(boolean animateIfLastStepGood);
+ }
+
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final FingerprintManager mFingerprintManager;
+ private final boolean mAccessibilityEnabled;
+ @NonNull
+ private final List mGuidedEnrollmentPoints;
+
+ private int mTotalSteps = -1;
+ private int mRemainingSteps = -1;
+
+ // Note that this is actually not equal to "mTotalSteps - mRemainingSteps", because the
+ // interface makes no promises about monotonically increasing by one each time.
+ private int mLocationsEnrolled = 0;
+
+ private int mCenterTouchCount = 0;
+
+ @Nullable
+ UdfpsEnrollHelper.Listener mListener;
+
+ public UdfpsEnrollHelper(@NonNull Context context,
+ @NonNull FingerprintManager fingerprintManager) {
+
+ mContext = context;
+ mFingerprintManager = fingerprintManager;
+
+ final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
+ mAccessibilityEnabled = am.isEnabled();
+
+ mGuidedEnrollmentPoints = new ArrayList<>();
+
+ // Number of pixels per mm
+ float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
+ context.getResources().getDisplayMetrics());
+ boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ NEW_COORDS_OVERRIDE, 0,
+ UserHandle.USER_CURRENT) != 0;
+ if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) {
+ Log.v(TAG, "Using new coordinates");
+ mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, -1.02f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, 1.02f * px));
+ mGuidedEnrollmentPoints.add(new PointF(0.29f * px, 0.00f * px));
+ mGuidedEnrollmentPoints.add(new PointF(2.17f * px, -2.35f * px));
+ mGuidedEnrollmentPoints.add(new PointF(1.07f * px, -3.96f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, -4.31f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, -3.29f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, -1.23f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, 1.23f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, 3.29f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, 4.31f * px));
+ mGuidedEnrollmentPoints.add(new PointF(1.07f * px, 3.96f * px));
+ mGuidedEnrollmentPoints.add(new PointF(2.17f * px, 2.35f * px));
+ mGuidedEnrollmentPoints.add(new PointF(2.58f * px, 0.00f * px));
+ } else {
+ Log.v(TAG, "Using old coordinates");
+ mGuidedEnrollmentPoints.add(new PointF(2.00f * px, 0.00f * px));
+ mGuidedEnrollmentPoints.add(new PointF(0.87f * px, -2.70f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, -1.31f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, 1.31f * px));
+ mGuidedEnrollmentPoints.add(new PointF(0.88f * px, 2.70f * px));
+ mGuidedEnrollmentPoints.add(new PointF(3.94f * px, -1.06f * px));
+ mGuidedEnrollmentPoints.add(new PointF(2.90f * px, -4.14f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-0.52f * px, -5.95f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-3.33f * px, -3.33f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-3.99f * px, -0.35f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-3.62f * px, 2.54f * px));
+ mGuidedEnrollmentPoints.add(new PointF(-1.49f * px, 5.57f * px));
+ mGuidedEnrollmentPoints.add(new PointF(2.29f * px, 4.92f * px));
+ mGuidedEnrollmentPoints.add(new PointF(3.82f * px, 1.78f * px));
+ }
+ }
+
+ void onEnrollmentProgress(int totalSteps, int remaining) {
+ if (mTotalSteps == -1) {
+ mTotalSteps = totalSteps;
+ }
+
+ if (remaining != mRemainingSteps) {
+ mLocationsEnrolled++;
+ if (isCenterEnrollmentStage()) {
+ mCenterTouchCount++;
+ }
+ }
+
+ mRemainingSteps = remaining;
+
+ if (mListener != null && mTotalSteps != -1) {
+ mListener.onEnrollmentProgress(remaining, mTotalSteps);
+ }
+ }
+
+ void onEnrollmentHelp() {
+ if (mListener != null && mTotalSteps != -1) {
+ mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps);
+ }
+ }
+
+ void onAcquired(boolean isAcquiredGood) {
+ if (mListener != null && mTotalSteps != -1) {
+ mListener.onAcquired(isAcquiredGood && animateIfLastStep());
+ }
+ }
+
+ void setListener(UdfpsEnrollHelper.Listener listener) {
+ mListener = listener;
+
+ // Only notify during setListener if enrollment is already in progress, so the progress
+ // bar can be updated. If enrollment has not started yet, the progress bar will be empty
+ // anyway.
+ if (mListener != null && mTotalSteps != -1) {
+ mListener.onEnrollmentProgress(mRemainingSteps, mTotalSteps);
+ }
+ }
+
+ boolean isCenterEnrollmentStage() {
+ if (mTotalSteps == -1 || mRemainingSteps == -1) {
+ return true;
+ }
+ return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
+ }
+
+ boolean isTipEnrollmentStage() {
+ if (mTotalSteps == -1 || mRemainingSteps == -1) {
+ return false;
+ }
+ final int progressSteps = mTotalSteps - mRemainingSteps;
+ return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
+ && progressSteps < getStageThresholdSteps(mTotalSteps, 2);
+ }
+
+ boolean isEdgeEnrollmentStage() {
+ if (mTotalSteps == -1 || mRemainingSteps == -1) {
+ return false;
+ }
+ return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
+ }
+
+ @NonNull
+ PointF getNextGuidedEnrollmentPoint() {
+ if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
+ return new PointF(0f, 0f);
+ }
+
+ float scale = SCALE;
+ if (Build.IS_ENG || Build.IS_USERDEBUG) {
+ scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
+ SCALE_OVERRIDE, SCALE,
+ UserHandle.USER_CURRENT);
+ }
+ final int index = mLocationsEnrolled - mCenterTouchCount;
+ final PointF originalPoint = mGuidedEnrollmentPoints
+ .get(index % mGuidedEnrollmentPoints.size());
+ return new PointF(originalPoint.x * scale, originalPoint.y * scale);
+ }
+
+ boolean animateIfLastStep() {
+ if (mListener == null) {
+ Log.e(TAG, "animateIfLastStep, null listener");
+ return false;
+ }
+
+ return mRemainingSteps <= 2 && mRemainingSteps >= 0;
+ }
+
+ private int getStageThresholdSteps(int totalSteps, int stageIndex) {
+ return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
+ }
+
+ private boolean isGuidedEnrollmentStage() {
+ if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
+ return false;
+ }
+ final int progressSteps = mTotalSteps - mRemainingSteps;
+ return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
+ && progressSteps < getStageThresholdSteps(mTotalSteps, 1);
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
new file mode 100644
index 00000000000..52f30f5b79c
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.os.Process;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+/**
+ * UDFPS enrollment progress bar.
+ */
+public class UdfpsEnrollProgressBarDrawable extends Drawable {
+ private static final String TAG = "UdfpsProgressBar";
+
+ private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L;
+ private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L;
+ private static final long FILL_COLOR_ANIMATION_DURATION_MS = 350L;
+ private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
+ private static final float STROKE_WIDTH_DP = 12f;
+ private static final Interpolator DEACCEL = new DecelerateInterpolator();
+
+ private static final VibrationEffect VIBRATE_EFFECT_ERROR =
+ VibrationEffect.createWaveform(new long[]{0, 5, 55, 60}, -1);
+ private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
+
+ private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
+
+ private static final VibrationEffect SUCCESS_VIBRATION_EFFECT =
+ VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+
+ private final float mStrokeWidthPx;
+ @ColorInt
+ private final int mProgressColor;
+ @ColorInt
+ private final int mHelpColor;
+ @ColorInt
+ private final int mOnFirstBucketFailedColor;
+ @NonNull
+ private final Drawable mCheckmarkDrawable;
+ @NonNull
+ private final Interpolator mCheckmarkInterpolator;
+ @NonNull
+ private final Paint mBackgroundPaint;
+ @NonNull
+ private final Paint mFillPaint;
+ @NonNull
+ private final Vibrator mVibrator;
+ @NonNull
+ private final boolean mIsAccessibilityEnabled;
+ @NonNull
+ private final Context mContext;
+
+ private boolean mAfterFirstTouch;
+
+ private int mRemainingSteps = 0;
+ private int mTotalSteps = 0;
+ private float mProgress = 0f;
+ @Nullable
+ private ValueAnimator mProgressAnimator;
+ @NonNull
+ private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
+
+ private boolean mShowingHelp = false;
+ @Nullable
+ private ValueAnimator mFillColorAnimator;
+ @NonNull
+ private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
+
+ @Nullable
+ private ValueAnimator mBackgroundColorAnimator;
+ @NonNull
+ private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdateListener;
+
+ private boolean mComplete = false;
+ private float mCheckmarkScale = 0f;
+ @Nullable
+ private ValueAnimator mCheckmarkAnimator;
+ @NonNull
+ private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener;
+
+ private int mMovingTargetFill;
+ private int mMovingTargetFillError;
+ private int mEnrollProgress;
+ private int mEnrollProgressHelp;
+ private int mEnrollProgressHelpWithTalkback;
+
+ public UdfpsEnrollProgressBarDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
+ mContext = context;
+
+ loadResources(context, attrs);
+ float density = context.getResources().getDisplayMetrics().densityDpi;
+ mStrokeWidthPx = STROKE_WIDTH_DP * (density / DisplayMetrics.DENSITY_DEFAULT);
+ mProgressColor = mEnrollProgress;
+ final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
+ mIsAccessibilityEnabled = am.isTouchExplorationEnabled();
+ mOnFirstBucketFailedColor = mMovingTargetFillError;
+ if (!mIsAccessibilityEnabled) {
+ mHelpColor = mEnrollProgressHelp;
+ } else {
+ mHelpColor = mEnrollProgressHelpWithTalkback;
+ }
+ mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
+ mCheckmarkDrawable.mutate();
+ mCheckmarkInterpolator = new OvershootInterpolator();
+
+ mBackgroundPaint = new Paint();
+ mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
+ mBackgroundPaint.setColor(mMovingTargetFill);
+ mBackgroundPaint.setAntiAlias(true);
+ mBackgroundPaint.setStyle(Paint.Style.STROKE);
+ mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ // Progress fill should *not* use the extracted system color.
+ mFillPaint = new Paint();
+ mFillPaint.setStrokeWidth(mStrokeWidthPx);
+ mFillPaint.setColor(mProgressColor);
+ mFillPaint.setAntiAlias(true);
+ mFillPaint.setStyle(Paint.Style.STROKE);
+ mFillPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mVibrator = mContext.getSystemService(Vibrator.class);
+
+ mProgressUpdateListener = animation -> {
+ mProgress = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ };
+
+ mFillColorUpdateListener = animation -> {
+ mFillPaint.setColor((int) animation.getAnimatedValue());
+ invalidateSelf();
+ };
+
+ mCheckmarkUpdateListener = animation -> {
+ mCheckmarkScale = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ };
+
+ mBackgroundColorUpdateListener = animation -> {
+ mBackgroundPaint.setColor((int) animation.getAnimatedValue());
+ invalidateSelf();
+ };
+ }
+
+ void onEnrollmentProgress(int remaining, int totalSteps) {
+ mAfterFirstTouch = true;
+ updateState(remaining, totalSteps, false /* showingHelp */);
+ }
+
+ void onEnrollmentHelp(int remaining, int totalSteps) {
+ updateState(remaining, totalSteps, true /* showingHelp */);
+ }
+
+ void onLastStepAcquired() {
+ updateState(0, mTotalSteps, false /* showingHelp */);
+ }
+
+ private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) {
+ updateProgress(remainingSteps, totalSteps, showingHelp);
+ updateFillColor(showingHelp);
+ }
+
+ private void updateProgress(int remainingSteps, int totalSteps, boolean showingHelp) {
+ if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
+ return;
+ }
+
+ if (mShowingHelp) {
+ if (mVibrator != null && mIsAccessibilityEnabled) {
+ mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
+ VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::onEnrollmentHelp",
+ FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
+ }
+ } else {
+ // If the first touch is an error, remainingSteps will be -1 and the callback
+ // doesn't come from onEnrollmentHelp. If we are in the accessibility flow,
+ // we still would like to vibrate.
+ if (mVibrator != null) {
+ if (remainingSteps == -1 && mIsAccessibilityEnabled) {
+ mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
+ VIBRATE_EFFECT_ERROR,
+ getClass().getSimpleName() + "::onFirstTouchError",
+ FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
+ } else if (remainingSteps != -1 && !mIsAccessibilityEnabled) {
+ mVibrator.vibrate(Process.myUid(),
+ mContext.getOpPackageName(),
+ SUCCESS_VIBRATION_EFFECT,
+ getClass().getSimpleName() + "::OnEnrollmentProgress",
+ HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
+ }
+ }
+ }
+
+ mShowingHelp = showingHelp;
+ mRemainingSteps = remainingSteps;
+ mTotalSteps = totalSteps;
+
+ final int progressSteps = Math.max(0, totalSteps - remainingSteps);
+
+ // If needed, add 1 to progress and total steps to account for initial touch.
+ final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps;
+ final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps;
+
+ final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal);
+
+ if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
+ mProgressAnimator.cancel();
+ }
+
+ mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress);
+ mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS);
+ mProgressAnimator.addUpdateListener(mProgressUpdateListener);
+ mProgressAnimator.start();
+
+ if (remainingSteps == 0) {
+ startCompletionAnimation();
+ } else if (remainingSteps > 0) {
+ rollBackCompletionAnimation();
+ }
+ }
+
+ private void animateBackgroundColor() {
+ if (mBackgroundColorAnimator != null && mBackgroundColorAnimator.isRunning()) {
+ mBackgroundColorAnimator.end();
+ }
+ mBackgroundColorAnimator = ValueAnimator.ofArgb(mBackgroundPaint.getColor(),
+ mOnFirstBucketFailedColor);
+ mBackgroundColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+ mBackgroundColorAnimator.setRepeatCount(1);
+ mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
+ mBackgroundColorAnimator.setInterpolator(DEACCEL);
+ mBackgroundColorAnimator.addUpdateListener(mBackgroundColorUpdateListener);
+ mBackgroundColorAnimator.start();
+ }
+
+ private void updateFillColor(boolean showingHelp) {
+ if (!mAfterFirstTouch && showingHelp) {
+ // If we are on the first touch, animate the background color
+ // instead of the progress color.
+ animateBackgroundColor();
+ return;
+ }
+
+ if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
+ mFillColorAnimator.end();
+ }
+
+ @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor;
+ mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor);
+ mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+ mFillColorAnimator.setRepeatCount(1);
+ mFillColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
+ mFillColorAnimator.setInterpolator(DEACCEL);
+ mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
+ mFillColorAnimator.start();
+ }
+
+ private void startCompletionAnimation() {
+ if (mComplete) {
+ return;
+ }
+ mComplete = true;
+
+ if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
+ mCheckmarkAnimator.cancel();
+ }
+
+ mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f);
+ mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS);
+ mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS);
+ mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator);
+ mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
+ mCheckmarkAnimator.start();
+ }
+
+ private void rollBackCompletionAnimation() {
+ if (!mComplete) {
+ return;
+ }
+ mComplete = false;
+
+ // Adjust duration based on how much of the completion animation has played.
+ final float animatedFraction = mCheckmarkAnimator != null
+ ? mCheckmarkAnimator.getAnimatedFraction()
+ : 0f;
+ final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction);
+
+ if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
+ mCheckmarkAnimator.cancel();
+ }
+
+ mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f);
+ mCheckmarkAnimator.setDuration(durationMs);
+ mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
+ mCheckmarkAnimator.start();
+ }
+
+ private void loadResources(Context context, @Nullable AttributeSet attrs) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs,
+ R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
+ R.style.BiometricsEnrollStyle);
+ mMovingTargetFill = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
+ mMovingTargetFillError = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsMovingTargetFillError, 0);
+ mEnrollProgress = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsEnrollProgress, 0);
+ mEnrollProgressHelp = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelp, 0);
+ mEnrollProgressHelpWithTalkback = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0);
+ ta.recycle();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ canvas.save();
+
+ // Progress starts from the top, instead of the right
+ canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
+
+ final float halfPaddingPx = mStrokeWidthPx / 2f;
+
+ if (mProgress < 1f) {
+ // Draw the background color of the progress circle.
+ canvas.drawArc(
+ halfPaddingPx,
+ halfPaddingPx,
+ getBounds().right - halfPaddingPx,
+ getBounds().bottom - halfPaddingPx,
+ 0f /* startAngle */,
+ 360f /* sweepAngle */,
+ false /* useCenter */,
+ mBackgroundPaint);
+ }
+
+ if (mProgress > 0f) {
+ // Draw the filled portion of the progress circle.
+ canvas.drawArc(
+ halfPaddingPx,
+ halfPaddingPx,
+ getBounds().right - halfPaddingPx,
+ getBounds().bottom - halfPaddingPx,
+ 0f /* startAngle */,
+ 360f * mProgress /* sweepAngle */,
+ false /* useCenter */,
+ mFillPaint);
+ }
+
+ canvas.restore();
+
+ if (mCheckmarkScale > 0f) {
+ final float offsetScale = (float) Math.sqrt(2) / 2f;
+ final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale;
+ final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale;
+ final float centerX = getBounds().centerX() + centerXOffset;
+ final float centerY = getBounds().centerY() + centerYOffset;
+
+ final float boundsXOffset =
+ mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale;
+ final float boundsYOffset =
+ mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale;
+
+ final int left = Math.round(centerX - boundsXOffset);
+ final int top = Math.round(centerY - boundsYOffset);
+ final int right = Math.round(centerX + boundsXOffset);
+ final int bottom = Math.round(centerY + boundsYOffset);
+ mCheckmarkDrawable.setBounds(left, top, right, bottom);
+ mCheckmarkDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {}
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ }
+
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java
new file mode 100644
index 00000000000..3e3fd0e436d
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.RotationUtils;
+import android.view.Gravity;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+/**
+ * View corresponding with udfps_enroll_view.xml
+ */
+public class UdfpsEnrollView extends FrameLayout implements UdfpsEnrollHelper.Listener {
+ @NonNull
+ private final UdfpsEnrollDrawable mFingerprintDrawable;
+ @NonNull
+ private final UdfpsEnrollProgressBarDrawable mFingerprintProgressDrawable;
+ @NonNull
+ private final Handler mHandler;
+
+ @NonNull
+ private ImageView mFingerprintProgressView;
+
+ private int mProgressBarRadius;
+
+ // sensorRect may be bigger than the sensor. True sensor dimensions are defined in
+ // overlayParams.sensorBounds
+ private Rect mSensorRect;
+ private UdfpsOverlayParams mOverlayParams;
+
+ public UdfpsEnrollView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ mFingerprintDrawable = new UdfpsEnrollDrawable(mContext, attrs);
+ mFingerprintProgressDrawable = new UdfpsEnrollProgressBarDrawable(context, attrs);
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ ImageView fingerprintView = findViewById(R.id.udfps_enroll_animation_fp_view);
+ fingerprintView.setImageDrawable(mFingerprintDrawable);
+ mFingerprintProgressView = findViewById(R.id.udfps_enroll_animation_fp_progress_view);
+ mFingerprintProgressView.setImageDrawable(mFingerprintProgressDrawable);
+ }
+
+ // Implements UdfpsEnrollHelper.Listener
+ @Override
+ public void onEnrollmentProgress(int remaining, int totalSteps) {
+ mHandler.post(() -> {
+ mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
+ mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
+ });
+ }
+
+ @Override
+ public void onEnrollmentHelp(int remaining, int totalSteps) {
+ mHandler.post(() -> mFingerprintProgressDrawable.onEnrollmentHelp(remaining, totalSteps));
+ }
+
+ @Override
+ public void onAcquired(boolean animateIfLastStepGood) {
+ mHandler.post(() -> {
+ if (animateIfLastStepGood) mFingerprintProgressDrawable.onLastStepAcquired();
+ });
+ }
+
+ void setOverlayParams(UdfpsOverlayParams params) {
+ mOverlayParams = params;
+
+ post(() -> {
+ mProgressBarRadius =
+ (int) (mOverlayParams.getScaleFactor() * getContext().getResources().getInteger(
+ R.integer.config_udfpsEnrollProgressBar));
+ mSensorRect = mOverlayParams.getSensorBounds();
+
+ onSensorRectUpdated();
+ });
+ }
+
+ void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
+ mFingerprintDrawable.setEnrollHelper(enrollHelper);
+ enrollHelper.setListener(this);
+ }
+
+ private void onSensorRectUpdated() {
+ updateDimensions();
+ updateAccessibilityViewLocation();
+
+ // Updates sensor rect in relation to the overlay view
+ mSensorRect.set(getPaddingX(), getPaddingY(),
+ (mOverlayParams.getSensorBounds().width() + getPaddingX()),
+ (mOverlayParams.getSensorBounds().height() + getPaddingY()));
+ mFingerprintDrawable.onSensorRectUpdated(new RectF(mSensorRect));
+ }
+
+ private void updateDimensions() {
+ // Original sensorBounds assume portrait mode.
+ Rect rotatedBounds = mOverlayParams.getSensorBounds();
+ int rotation = mOverlayParams.getRotation();
+ if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
+ RotationUtils.rotateBounds(
+ rotatedBounds,
+ mOverlayParams.getNaturalDisplayWidth(),
+ mOverlayParams.getNaturalDisplayHeight(),
+ rotation
+ );
+ }
+
+ // Use parent view's and rotatedBound's absolute coordinates to decide the margins of
+ // UdfpsEnrollView, so that its center keeps consistent with sensor rect's.
+ ViewGroup parentView = (ViewGroup) getParent();
+ int[] coords = parentView.getLocationOnScreen();
+ int parentLeft = coords[0];
+ int parentTop = coords[1];
+ int parentRight = parentLeft + parentView.getWidth();
+ int parentBottom = parentTop + parentView.getHeight();
+ MarginLayoutParams marginLayoutParams = (MarginLayoutParams) getLayoutParams();
+ FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams();
+
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ params.gravity = Gravity.RIGHT | Gravity.TOP;
+ marginLayoutParams.rightMargin = parentRight - rotatedBounds.right - getPaddingX();
+ marginLayoutParams.topMargin = rotatedBounds.top - parentTop - getPaddingY();
+ break;
+ case Surface.ROTATION_90:
+ params.gravity = Gravity.RIGHT | Gravity.BOTTOM;
+ marginLayoutParams.rightMargin = parentRight - rotatedBounds.right - getPaddingX();
+ marginLayoutParams.bottomMargin =
+ parentBottom - rotatedBounds.bottom - getPaddingY();
+ break;
+ case Surface.ROTATION_270:
+ params.gravity = Gravity.LEFT | Gravity.BOTTOM;
+ marginLayoutParams.leftMargin = rotatedBounds.left - parentLeft - getPaddingX();
+ marginLayoutParams.bottomMargin =
+ parentBottom - rotatedBounds.bottom - getPaddingY();
+ break;
+ }
+
+ params.height = rotatedBounds.height() + 2 * getPaddingX();
+ params.width = rotatedBounds.width() + 2 * getPaddingY();
+ setLayoutParams(params);
+ }
+
+ private void updateAccessibilityViewLocation() {
+ View fingerprintAccessibilityView = findViewById(R.id.udfps_enroll_accessibility_view);
+ ViewGroup.LayoutParams params = fingerprintAccessibilityView.getLayoutParams();
+ params.width = mOverlayParams.getSensorBounds().width();
+ params.height = mOverlayParams.getSensorBounds().height();
+ fingerprintAccessibilityView.setLayoutParams(params);
+ fingerprintAccessibilityView.requestLayout();
+ }
+
+ private void onFingerDown() {
+ if (mOverlayParams.isOptical()) {
+ mFingerprintDrawable.setShouldSkipDraw(true);
+ mFingerprintDrawable.invalidateSelf();
+ }
+ }
+
+ private void onFingerUp() {
+ if (mOverlayParams.isOptical()) {
+ mFingerprintDrawable.setShouldSkipDraw(false);
+ mFingerprintDrawable.invalidateSelf();
+ }
+ }
+
+ private int getPaddingX() {
+ return mProgressBarRadius;
+ }
+
+ private int getPaddingY() {
+ return mProgressBarRadius;
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsOverlayParams.java b/src/com/android/settings/biometrics/fingerprint/UdfpsOverlayParams.java
new file mode 100644
index 00000000000..9b52ad62302
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsOverlayParams.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint;
+
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Collection of parameters that define an under-display fingerprint sensor (UDFPS) overlay.
+ *
+ * [sensorBounds] coordinates of the bounding box around the sensor in natural orientation, in
+ * pixels, for the current resolution.
+ *
+ * [overlayBounds] coordinates of the UI overlay in natural orientation, in pixels, for the current
+ * resolution.
+ *
+ * [naturalDisplayWidth] width of the physical display in natural orientation, in pixels, for the
+ * current resolution.
+ *
+ * [naturalDisplayHeight] height of the physical display in natural orientation, in pixels, for the
+ * current resolution.
+ *
+ * [scaleFactor] ratio of a dimension in the current resolution to the corresponding dimension in
+ * the native resolution.
+ *
+ * [rotation] current rotation of the display.
+ */
+public final class UdfpsOverlayParams {
+ @NonNull
+ private final Rect mSensorBounds;
+ @NonNull
+ private final Rect mOverlayBounds;
+ private final int mNaturalDisplayWidth;
+ private final int mNaturalDisplayHeight;
+ private final float mScaleFactor;
+ private final int mRotation;
+ private final boolean mIsOptical;
+
+ public UdfpsOverlayParams(@NonNull Rect sensorBounds, @NonNull Rect overlayBounds,
+ int naturalDisplayWidth, int naturalDisplayHeight, float scaleFactor, int rotation,
+ boolean isOptical) {
+ mSensorBounds = sensorBounds;
+ mOverlayBounds = overlayBounds;
+ mNaturalDisplayWidth = naturalDisplayWidth;
+ mNaturalDisplayHeight = naturalDisplayHeight;
+ mScaleFactor = scaleFactor;
+ mRotation = rotation;
+ mIsOptical = isOptical;
+ }
+
+ @NonNull
+ public Rect getSensorBounds() {
+ return mSensorBounds;
+ }
+
+ @NonNull
+ public Rect getOverlayBounds() {
+ return mOverlayBounds;
+ }
+
+ public int getNaturalDisplayWidth() {
+ return mNaturalDisplayWidth;
+ }
+
+ public int getNaturalDisplayHeight() {
+ return mNaturalDisplayHeight;
+ }
+
+ public float getScaleFactor() {
+ return mScaleFactor;
+ }
+
+ public int getRotation() {
+ return mRotation;
+ }
+
+ public boolean isOptical() {
+ return mIsOptical;
+ }
+}