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:
39
res/drawable/ic_enrollment_fingerprint.xml
Normal file
39
res/drawable/ic_enrollment_fingerprint.xml
Normal 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>
|
35
res/drawable/udfps_enroll_checkmark.xml
Normal file
35
res/drawable/udfps_enroll_checkmark.xml
Normal 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>
|
@@ -39,6 +39,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
|
42
res/layout/udfps_enroll_view.xml
Normal file
42
res/layout/udfps_enroll_view.xml
Normal 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>
|
@@ -57,5 +57,15 @@
|
||||
|
||||
<!-- Icon tint color for battery usage system icon -->
|
||||
<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>
|
||||
|
||||
|
@@ -184,4 +184,14 @@
|
||||
<attr name="ic_menu_moreoverflow" format="reference" />
|
||||
<attr name="side_margin" format="reference|dimension" />
|
||||
<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>
|
||||
|
@@ -172,4 +172,13 @@
|
||||
|
||||
<!-- Icon tint color for battery usage system icon -->
|
||||
<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>
|
||||
|
@@ -636,4 +636,25 @@
|
||||
|
||||
<!-- Whether the toggle for Auto-rotate with Face Detection should be shown. -->
|
||||
<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>
|
||||
|
@@ -11490,4 +11490,7 @@
|
||||
=1 {Apps installed more than # month ago}
|
||||
other {Apps installed more than # months ago}
|
||||
}</string>
|
||||
|
||||
<!-- Accessibility label for fingerprint sensor [CHAR LIMIT=NONE] -->
|
||||
<string name="accessibility_fingerprint_label">Fingerprint sensor</string>
|
||||
</resources>
|
||||
|
@@ -905,4 +905,13 @@
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:singleLine">true</item>
|
||||
</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>
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
@@ -262,6 +274,10 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
? 0 : (int) getResources().getDimension(
|
||||
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
|
||||
|
@@ -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);
|
||||
|
@@ -91,6 +91,11 @@ public class FingerprintUpdater {
|
||||
BiometricsSafetySource.onBiometricsChanged(mContext); // biometrics data changed
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcquired(boolean isAcquiredGood) {
|
||||
mCallback.onAcquired(isAcquiredGood);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user