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; + } +}