Add a flag for moving UdfpsEnroll* from SystemUI to settings.

The added files in this CL are mostly copied from SystemUI. Enabling the flag SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS with this CL, the udfps enrollment icons and progress bar are shown in settings.

Turn this flag on via adb:
adb shell setprop sys.fflag.override.settings_show_udfps_enroll_in_settings true

There are some known issues that will be fixed in the following CLs, including:
- When the finger is down on the screen and the lighting circle on the sensor is shown, the fingerprint icon is not hidden.
- When rotating the screen, fingerprint location is not right.
- Currently the scale factor is hard coded for pixel 7 pro, we should update the scale factor based on the device, etc.

Test: manually tested on device
Bug: 260617060
Change-Id: I5aede070eb1de9eb3b5e1400d6e51a8523079852
This commit is contained in:
Hao Dong
2022-11-28 17:43:48 +00:00
parent cf9fd9941d
commit af35c7cb9d
19 changed files with 1575 additions and 3 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);

View File

@@ -91,6 +91,11 @@ public class FingerprintUpdater {
BiometricsSafetySource.onBiometricsChanged(mContext); // biometrics data changed
}
}
@Override
public void onAcquired(boolean isAcquiredGood) {
mCallback.onAcquired(isAcquiredGood);
}
}
/**

View File

@@ -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();
}
}

View File

@@ -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<PointF> 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);
}
}

View File

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

View File

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

View File

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