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

@@ -0,0 +1,39 @@
<!--
Copyright (C) 2020 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:tint="@*android:color/primary_text_material_dark"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData=
"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"
android:strokeWidth="3"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="54dp"
android:height="54dp"
android:viewportWidth="54"
android:viewportHeight="54">
<path
android:pathData="M26.9999,3.9619C39.7029,3.9619 50.0369,14.2969 50.0369,26.9999C50.0369,39.7029 39.7029,50.0379 26.9999,50.0379C14.2969,50.0379 3.9629,39.7029 3.9629,26.9999C3.9629,14.2969 14.2969,3.9619 26.9999,3.9619Z"
android:fillColor="?android:colorBackground"
android:fillType="evenOdd"/>
<path
android:pathData="M27,0C12.088,0 0,12.088 0,27C0,41.912 12.088,54 27,54C41.912,54 54,41.912 54,27C54,12.088 41.912,0 27,0ZM27,3.962C39.703,3.962 50.037,14.297 50.037,27C50.037,39.703 39.703,50.038 27,50.038C14.297,50.038 3.963,39.703 3.963,27C3.963,14.297 14.297,3.962 27,3.962Z"
android:fillColor="@color/udfps_enroll_progress"
android:fillType="evenOdd"/>
<path
android:pathData="M23.0899,38.8534L10.4199,26.1824L13.2479,23.3544L23.0899,33.1974L41.2389,15.0474L44.0679,17.8754L23.0899,38.8534Z"
android:fillColor="@color/udfps_enroll_progress"
android:fillType="evenOdd"/>
</vector>

View File

@@ -39,6 +39,7 @@
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout <FrameLayout
android:id="@+id/layout_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="false" android:clipChildren="false"

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<com.android.settings.biometrics.fingerprint.UdfpsEnrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/udfps_animation_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The layout height/width are placeholders, which will be overwritten by
FingerprintSensorPropertiesInternal. -->
<View
android:id="@+id/udfps_enroll_accessibility_view"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/accessibility_fingerprint_label"/>
<ImageView
android:id="@+id/udfps_enroll_animation_fp_progress_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- Fingerprint -->
<ImageView
android:id="@+id/udfps_enroll_animation_fp_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.android.settings.biometrics.fingerprint.UdfpsEnrollView>

View File

@@ -57,5 +57,15 @@
<!-- Icon tint color for battery usage system icon --> <!-- Icon tint color for battery usage system icon -->
<color name="battery_usage_system_icon_color">@android:color/white</color> <color name="battery_usage_system_icon_color">@android:color/white</color>
<!-- UDFPS colors -->
<color name="udfps_enroll_icon">#7DA7F1</color>
<color name="udfps_moving_target_fill">#475670</color>
<!-- 50% of udfps_moving_target_fill-->
<color name="udfps_moving_target_fill_error">#80475670</color>
<color name="udfps_enroll_progress">#7DA7F1</color>
<color name="udfps_enroll_progress_help">#607DA7F1</color>
<color name="udfps_enroll_progress_help_with_talkback">#FFEE675C</color>
</resources> </resources>

View File

@@ -184,4 +184,14 @@
<attr name="ic_menu_moreoverflow" format="reference" /> <attr name="ic_menu_moreoverflow" format="reference" />
<attr name="side_margin" format="reference|dimension" /> <attr name="side_margin" format="reference|dimension" />
<attr name="wifi_signal_color" format="reference" /> <attr name="wifi_signal_color" format="reference" />
<declare-styleable name="BiometricsEnrollView">
<attr name="biometricsEnrollStyle" format="reference" />
<attr name="biometricsEnrollIcon" format="reference|color" />
<attr name="biometricsMovingTargetFill" format="reference|color" />
<attr name="biometricsMovingTargetFillError" format="reference|color" />
<attr name="biometricsEnrollProgress" format="reference|color" />
<attr name="biometricsEnrollProgressHelp" format="reference|color" />
<attr name="biometricsEnrollProgressHelpWithTalkback" format="reference|color" />
</declare-styleable>
</resources> </resources>

View File

@@ -172,4 +172,13 @@
<!-- Icon tint color for battery usage system icon --> <!-- Icon tint color for battery usage system icon -->
<color name="battery_usage_system_icon_color">?android:attr/textColorPrimary</color> <color name="battery_usage_system_icon_color">?android:attr/textColorPrimary</color>
<!-- UDFPS colors -->
<color name="udfps_enroll_icon">#699FF3</color>
<color name="udfps_moving_target_fill">#C2D7F7</color>
<!-- 50% of udfps_moving_target_fill-->
<color name="udfps_moving_target_fill_error">#80C2D7F7</color>
<color name="udfps_enroll_progress">#699FF3</color>
<color name="udfps_enroll_progress_help">#70699FF3</color>
<color name="udfps_enroll_progress_help_with_talkback">#FFEE675C</color>
</resources> </resources>

View File

@@ -636,4 +636,25 @@
<!-- Whether the toggle for Auto-rotate with Face Detection should be shown. --> <!-- Whether the toggle for Auto-rotate with Face Detection should be shown. -->
<bool name="config_auto_rotate_face_detection_available">true</bool> <bool name="config_auto_rotate_face_detection_available">true</bool>
<!-- The radius of the enrollment progress bar, in dp -->
<integer name="config_udfpsEnrollProgressBar" translatable="false">
280
</integer>
<!-- Default udfps icon. Same path as ic_fingerprint.xml -->
<string name="config_udfpsIcon" translatable="false">
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
</string>
</resources> </resources>

View File

@@ -11490,4 +11490,7 @@
=1 {Apps installed more than # month ago} =1 {Apps installed more than # month ago}
other {Apps installed more than # months ago} other {Apps installed more than # months ago}
}</string> }</string>
<!-- Accessibility label for fingerprint sensor [CHAR LIMIT=NONE] -->
<string name="accessibility_fingerprint_label">Fingerprint sensor</string>
</resources> </resources>

View File

@@ -905,4 +905,13 @@
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
</style> </style>
<style name="BiometricsEnrollStyle">
<item name="biometricsEnrollIcon">@color/udfps_enroll_icon</item>
<item name="biometricsMovingTargetFill">@color/udfps_moving_target_fill</item>
<item name="biometricsMovingTargetFillError">@color/udfps_moving_target_fill_error</item>
<item name="biometricsEnrollProgress">@color/udfps_enroll_progress</item>
<item name="biometricsEnrollProgressHelp">@color/udfps_enroll_progress_help</item>
<item name="biometricsEnrollProgressHelpWithTalkback">@color/udfps_enroll_progress_help_with_talkback</item>
</style>
</resources> </resources>

View File

@@ -40,6 +40,11 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
void onEnrollmentHelp(int helpMsgId, CharSequence helpString); void onEnrollmentHelp(int helpMsgId, CharSequence helpString);
void onEnrollmentError(int errMsgId, CharSequence errString); void onEnrollmentError(int errMsgId, CharSequence errString);
void onEnrollmentProgressChange(int steps, int remaining); 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; 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() { private final Runnable mTimeoutRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -189,6 +207,14 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
mEnrolling = false; mEnrolling = false;
} }
protected void onAcquired(boolean isAcquiredGood) {
if (mListener != null) {
mListener.onAcquired(isAcquiredGood);
} else {
mQueuedEvents.add(new QueuedAcquired(isAcquiredGood));
}
}
public void setListener(Listener listener) { public void setListener(Listener listener) {
mListener = listener; mListener = listener;
if (mListener != null) { if (mListener != null) {

View File

@@ -34,11 +34,13 @@ import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Animatable2; import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.LayerDrawable;
import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle; import android.os.Bundle;
import android.os.Process; import android.os.Process;
@@ -46,15 +48,21 @@ import android.os.VibrationAttributes;
import android.os.VibrationEffect; import android.os.VibrationEffect;
import android.os.Vibrator; import android.os.Vibrator;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayUtils;
import android.util.FeatureFlagUtils;
import android.util.Log; import android.util.Log;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.OrientationEventListener; import android.view.OrientationEventListener;
import android.view.Surface; import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
@@ -157,6 +165,9 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
private boolean mCanAssumeUdfps; private boolean mCanAssumeUdfps;
private boolean mCanAssumeSfps; private boolean mCanAssumeSfps;
@Nullable private ProgressBar mProgressBar; @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 ObjectAnimator mProgressAnim;
private TextView mErrorText; private TextView mErrorText;
private Interpolator mFastOutSlowInInterpolator; private Interpolator mFastOutSlowInInterpolator;
@@ -245,7 +256,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
listenOrientationEvent(); listenOrientationEvent();
if (mCanAssumeUdfps) { if (mCanAssumeUdfps) {
switch(getApplicationContext().getDisplay().getRotation()) { int rotation = getApplicationContext().getDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_90: case Surface.ROTATION_90:
final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate( final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate(
R.layout.udfps_enroll_enrolling, null, false); R.layout.udfps_enroll_enrolling, null, false);
@@ -260,8 +272,12 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
layoutContainer.setPaddingRelative((int) getResources().getDimension( layoutContainer.setPaddingRelative((int) getResources().getDimension(
R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
? 0 : (int) getResources().getDimension( ? 0 : (int) getResources().getDimension(
R.dimen.rotation_90_enroll_padding_end), 0); R.dimen.rotation_90_enroll_padding_end), 0);
layoutContainer.setLayoutParams(lp); layoutContainer.setLayoutParams(lp);
if (FeatureFlagUtils.isEnabled(getApplicationContext(),
FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
layout.addView(addUdfpsEnrollView(props.get(0)));
}
setContentView(layout, lp); setContentView(layout, lp);
break; break;
@@ -269,7 +285,31 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
case Surface.ROTATION_180: case Surface.ROTATION_180:
case Surface.ROTATION_270: case Surface.ROTATION_270:
default: 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; break;
} }
setDescriptionText(R.string.security_settings_udfps_enroll_start_message); setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
@@ -766,6 +806,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
mErrorText.removeCallbacks(mTouchAgainRunnable); mErrorText.removeCallbacks(mTouchAgainRunnable);
} }
showError(helpString); 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) { private void updateProgress(boolean animate) {
if (mSidecar == null || !mSidecar.isEnrolling()) { if (mSidecar == null || !mSidecar.isEnrolling()) {
Log.d(TAG, "Enrollment not started yet"); Log.d(TAG, "Enrollment not started yet");
@@ -826,6 +875,12 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
if (mProgressBar != null && mProgressBar.getProgress() < progress) { if (mProgressBar != null && mProgressBar.getProgress() < progress) {
clearError(); clearError();
} }
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onEnrollmentProgress(mSidecar.getEnrollmentSteps(),
mSidecar.getEnrollmentRemaining());
}
if (animate) { if (animate) {
animateProgress(progress); animateProgress(progress);
} else { } 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 { public static class IconTouchDialog extends InstrumentedDialogFragment {
@Override @Override

View File

@@ -108,6 +108,11 @@ public class FingerprintEnrollSidecar extends BiometricEnrollSidecar {
FingerprintEnrollSidecar.super.onEnrollmentProgress(remaining); FingerprintEnrollSidecar.super.onEnrollmentProgress(remaining);
} }
@Override
public void onAcquired(boolean isAcquiredGood) {
FingerprintEnrollSidecar.super.onAcquired(isAcquiredGood);
}
@Override @Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
FingerprintEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString); FingerprintEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString);

View File

@@ -91,6 +91,11 @@ public class FingerprintUpdater {
BiometricsSafetySource.onBiometricsChanged(mContext); // biometrics data changed 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;
}
}