Refactor FingerprintEnrollEnrolling to fragment

Bug: b/260957933
Test: NA
Change-Id: I281ec7a7373f6863e2e6f8f20bbbb01fa01e009a
This commit is contained in:
Vincent Wang
2023-02-24 03:37:23 +00:00
parent 6461dc3814
commit 882b49c8fb
5 changed files with 1341 additions and 218 deletions

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/suw_lift"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/sud_layout_icon"
style="@style/SudGlifIcon"
android:layout_marginStart="48dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginTop="68dp"
android:scaleType="fitStart"
android:src="@drawable/ic_lock" />
<TextView
style="@style/SudGlifHeaderTitle"
android:id="@+id/suc_layout_title"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="138dp"
android:layout_marginStart="40dp"
android:layout_marginEnd="24dp"/>
<TextView
style="@style/SudDescription.Glif"
android:id="@+id/sud_layout_subtitle"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="206dp"
android:layout_marginStart="40dp"
android:layout_marginEnd="24dp"/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/illustration_lottie"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginTop="294dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="17dp"
android:scaleType="centerInside"
android:visibility="gone"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_speed=".85" />
<com.android.settings.biometrics2.ui.widget.UdfpsEnrollView
android:id="@+id/udfps_animation_view"
android:layout_width="218.42dp"
android:layout_height="216dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="553dp">
<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.biometrics2.ui.widget.UdfpsEnrollView>
<Button
style="@style/SudGlifButton.Secondary"
android:id="@+id/skip_btn"
android:text="@string/security_settings_fingerprint_enroll_enrolling_skip"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_marginBottom="26dp"
android:layout_marginStart="66dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>

View File

@@ -22,24 +22,19 @@ import android.annotation.RawRes;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.Drawable;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.DisplayInfo;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -52,25 +47,16 @@ import androidx.transition.TransitionSet;
import com.android.settings.R;
import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.biometrics.fingerprint.UdfpsEnrollHelper;
import com.android.settings.biometrics.fingerprint.UdfpsEnrollView;
import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
import com.android.settings.biometrics2.ui.widget.UdfpsEnrollView;
import com.android.settingslib.display.DisplayDensityUtils;
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.settingslib.udfps.UdfpsUtils;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieCompositionFactory;
import com.google.android.setupcompat.template.FooterActionButton;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupdesign.GlifLayout;
import java.util.Locale;
/**
* Fragment is used to handle enrolling process for udfps
@@ -92,9 +78,6 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
private DeviceRotationViewModel mRotationViewModel;
private FingerprintEnrollProgressViewModel mProgressViewModel;
private Interpolator mFastOutSlowInInterpolator;
private Interpolator mLinearOutSlowInInterpolator;
private Interpolator mFastOutLinearInInterpolator;
private boolean mAnimationCancelled;
private LottieAnimationView mIllustrationLottie;
@@ -104,9 +87,13 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
private boolean mHaveShownUdfpsCenterLottie;
private boolean mHaveShownUdfpsGuideLottie;
private GlifLayout mView;
private RelativeLayout mView;
private ImageView mIcon;
private TextView mErrorText;
private FooterBarMixin mFooterBarMixin;
private TextView mTitleText;
private TextView mSubTitleText;
private Button mSkipBtn;
private UdfpsEnrollView mUdfpsEnrollView;
private boolean mShouldShowLottie;
private boolean mIsAccessibilityEnabled;
@@ -145,13 +132,6 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
private int mIconTouchCount;
private UdfpsUtils mUdfpsUtils;
private float mScaleFactor = 1.0f;
//TODO UdfpsEnrollHelper should not be a Fragment, we should tell enrollview & progress
// drawable enough information EnrollView & ProgressDrawable should draw themselves without
// UdfpsEnrollHelper
private UdfpsEnrollHelper mUdfpsEnrollHelper;
@Override
public void onAttach(@NonNull Context context) {
@@ -200,10 +180,6 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
super.onCreate(savedInstanceState);
mEnrollingViewModel.restoreSavedState(savedInstanceState);
mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
mUdfpsUtils = new UdfpsUtils();
mUdfpsEnrollHelper = new UdfpsEnrollHelper(getActivity(), getActivity().getSystemService(
FingerprintManager.class
));
}
@Override
@@ -216,95 +192,85 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mView = initUdfpsLayout(inflater, container);
return mView;
}
private GlifLayout initUdfpsLayout(LayoutInflater inflater, ViewGroup container) {
final GlifLayout containView = (GlifLayout) inflater.inflate(
R.layout.udfps_enroll_enrolling, container, false);
final UdfpsEnrollView udfpsEnrollView = addUdfpsEnrollView(inflater,
mEnrollingViewModel.getFirstFingerprintSensorPropertiesInternal());
final int rotation = mRotationViewModel.getLiveData().getValue();
if (rotation == Surface.ROTATION_90) {
final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
final LinearLayout layoutContainer = containView.findViewById(
R.id.layout_container);
layoutContainer.setPaddingRelative(
(int) getResources().getDimension(R.dimen.rotation_90_enroll_padding_start),
0,
isLayoutRtl ? 0 : (int) getResources().getDimension(
R.dimen.rotation_90_enroll_padding_end),
0);
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
lp.setMarginEnd((int) getResources().getDimension(
R.dimen.rotation_90_enroll_margin_end));
layoutContainer.setLayoutParams(lp);
containView.addView(udfpsEnrollView);
containView.setClipChildren(false);
containView.setClipToPadding(false);
containView.setLayoutParams(lp);
setOnHoverListener(true, containView, udfpsEnrollView);
} else if (rotation == Surface.ROTATION_270) {
containView.addView(udfpsEnrollView);
containView.setClipChildren(false);
containView.setClipToPadding(false);
setOnHoverListener(true, containView, udfpsEnrollView);
} else {
final FrameLayout portraitLayoutContainer = containView.findViewById(
R.id.layout_container);
portraitLayoutContainer.addView(udfpsEnrollView);
ViewGroup parent = ((ViewGroup) portraitLayoutContainer.getParent());
parent.setClipChildren(false);
parent.setClipToPadding(false);
setOnHoverListener(false, containView, udfpsEnrollView);
}
private RelativeLayout initUdfpsLayout(LayoutInflater inflater, ViewGroup container) {
final RelativeLayout containView = (RelativeLayout) inflater.inflate(
R.layout.udfps_enroll_enrolling_v2, container, false);
final Activity activity = getActivity();
new GlifLayoutHelper(activity, containView).setDescriptionText(
getString(R.string.security_settings_udfps_enroll_start_message));
mIcon = containView.findViewById(R.id.sud_layout_icon);
mTitleText = containView.findViewById(R.id.suc_layout_title);
mSubTitleText = containView.findViewById(R.id.sud_layout_subtitle);
mErrorText = containView.findViewById(R.id.error_text);
mSkipBtn = containView.findViewById(R.id.skip_btn);
mSkipBtn.setOnClickListener(mOnSkipClickListener);
mUdfpsEnrollView = containView.findViewById(R.id.udfps_animation_view);
mUdfpsEnrollView.setSensorProperties(
mEnrollingViewModel.getFirstFingerprintSensorPropertiesInternal());
mShouldShowLottie = shouldShowLottie();
boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
|| BiometricUtils.isLandscape(activity);
updateOrientation(containView, (isLandscape
? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
mErrorText = containView.findViewById(R.id.error_text);
mFooterBarMixin = containView.getMixin(FooterBarMixin.class);
mFooterBarMixin.setSecondaryButton(
new FooterButton.Builder(activity)
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
.setListener(mOnSkipClickListener)
.setButtonType(FooterButton.ButtonType.SKIP)
.setTheme(R.style.SudGlifButton_Secondary)
.build()
);
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
activity, android.R.interpolator.fast_out_slow_in);
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
activity, android.R.interpolator.linear_out_slow_in);
mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
activity, android.R.interpolator.fast_out_linear_in);
final int rotation = mRotationViewModel.getLiveData().getValue();
if (rotation == Surface.ROTATION_270) {
RelativeLayout.LayoutParams iconLP = new RelativeLayout.LayoutParams(-2, -2);
iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
iconLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view);
iconLP.topMargin = (int) convertDpToPixel(76.64f, activity);
iconLP.leftMargin = (int) convertDpToPixel(151.54f, activity);
mIcon.setLayoutParams(iconLP);
final LinearLayout buttonContainer = mFooterBarMixin.getButtonContainer();
View spaceView = null;
for (int i = 0; i < buttonContainer.getChildCount(); i++) {
if (!(buttonContainer.getChildAt(i) instanceof FooterActionButton)) {
spaceView = buttonContainer.getChildAt(i);
break;
}
}
if (spaceView != null) {
spaceView.setVisibility(View.GONE);
buttonContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
RelativeLayout.LayoutParams titleLP = new RelativeLayout.LayoutParams(-1, -2);
titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
titleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view);
titleLP.topMargin = (int) convertDpToPixel(138f, activity);
titleLP.leftMargin = (int) convertDpToPixel(144f, activity);
mTitleText.setLayoutParams(titleLP);
RelativeLayout.LayoutParams subtitleLP = new RelativeLayout.LayoutParams(-1, -2);
subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
subtitleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view);
subtitleLP.topMargin = (int) convertDpToPixel(198f, activity);
subtitleLP.leftMargin = (int) convertDpToPixel(144f, activity);
mSubTitleText.setLayoutParams(subtitleLP);
} else if (rotation == Surface.ROTATION_90) {
DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
RelativeLayout.LayoutParams iconLP = new RelativeLayout.LayoutParams(-2, -2);
iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
iconLP.addRule(RelativeLayout.ALIGN_PARENT_START);
iconLP.topMargin = (int) convertDpToPixel(76.64f, activity);
iconLP.leftMargin = (int) convertDpToPixel(71.99f, activity);
mIcon.setLayoutParams(iconLP);
RelativeLayout.LayoutParams titleLP = new RelativeLayout.LayoutParams(
metrics.widthPixels / 2, -2);
titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
titleLP.addRule(RelativeLayout.ALIGN_PARENT_START, R.id.udfps_animation_view);
titleLP.topMargin = (int) convertDpToPixel(138f, activity);
titleLP.leftMargin = (int) convertDpToPixel(66f, activity);
mTitleText.setLayoutParams(titleLP);
RelativeLayout.LayoutParams subtitleLP = new RelativeLayout.LayoutParams(
metrics.widthPixels / 2, -2);
subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_START);
subtitleLP.topMargin = (int) convertDpToPixel(198f, activity);
subtitleLP.leftMargin = (int) convertDpToPixel(66f, activity);
mSubTitleText.setLayoutParams(subtitleLP);
}
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
RelativeLayout.LayoutParams skipBtnLP =
(RelativeLayout.LayoutParams) mIcon.getLayoutParams();
skipBtnLP.topMargin = (int) convertDpToPixel(26f, activity);
skipBtnLP.leftMargin = (int) convertDpToPixel(54f, activity);
mSkipBtn.requestLayout();
}
return containView;
}
@@ -358,8 +324,9 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
final int progress = getProgress(enrollmentProgress);
mUdfpsEnrollHelper.onEnrollmentProgress(enrollmentProgress.getSteps(),
enrollmentProgress.getRemaining());
mUdfpsEnrollView.onEnrollmentProgress(enrollmentProgress.getRemaining(),
enrollmentProgress.getSteps());
if (animate) {
animateProgress(progress);
@@ -382,38 +349,6 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
super.onDestroy();
}
private UdfpsEnrollView addUdfpsEnrollView(LayoutInflater inflater,
FingerprintSensorPropertiesInternal udfpsProps) {
UdfpsEnrollView enrollView = (UdfpsEnrollView) inflater.inflate(R.layout.udfps_enroll_view,
null, false);
DisplayInfo displayInfo = new DisplayInfo();
getActivity().getDisplay().getDisplayInfo(displayInfo);
mScaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
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 */);
UdfpsOverlayParams params = new UdfpsOverlayParams(
udfpsBounds,
overlayBounds,
displayInfo.getNaturalWidth(),
displayInfo.getNaturalHeight(),
mScaleFactor,
displayInfo.rotation);
enrollView.setOverlayParams(params);
enrollView.setEnrollHelper(mUdfpsEnrollHelper);
return enrollView;
}
private void animateProgress(int progress) {
// UDFPS animations are owned by SystemUI
if (progress >= PROGRESS_BAR_MAX) {
@@ -423,17 +358,11 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
}
private void updateTitleAndDescription() {
final Activity activity = getActivity();
final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mView);
switch (getCurrentStage()) {
case STAGE_CENTER:
glifLayoutHelper.setHeaderText(
R.string.security_settings_fingerprint_enroll_repeat_title);
mTitleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title);
if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_udfps_enroll_start_message));
mSubTitleText.setText(R.string.security_settings_udfps_enroll_start_message);
} else if (!mHaveShownUdfpsCenterLottie && mIllustrationLottie != null) {
mHaveShownUdfpsCenterLottie = true;
// Note: Update string reference when differentiate in between udfps & sfps
@@ -445,11 +374,10 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
break;
case STAGE_GUIDED:
glifLayoutHelper.setHeaderText(
R.string.security_settings_fingerprint_enroll_repeat_title);
mTitleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title);
if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_udfps_enroll_repeat_a11y_message));
mSubTitleText.setText(
R.string.security_settings_udfps_enroll_repeat_a11y_message);
} else if (!mHaveShownUdfpsGuideLottie && mIllustrationLottie != null) {
mHaveShownUdfpsGuideLottie = true;
mIllustrationLottie.setContentDescription(
@@ -460,8 +388,7 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
}
break;
case STAGE_FINGERTIP:
glifLayoutHelper.setHeaderText(
R.string.security_settings_udfps_enroll_fingertip_title);
mTitleText.setText(R.string.security_settings_udfps_enroll_fingertip_title);
if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) {
mHaveShownUdfpsTipLottie = true;
mIllustrationLottie.setContentDescription(
@@ -471,8 +398,7 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
}
break;
case STAGE_LEFT_EDGE:
glifLayoutHelper.setHeaderText(
R.string.security_settings_udfps_enroll_left_edge_title);
mTitleText.setText(R.string.security_settings_udfps_enroll_left_edge_title);
if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) {
mHaveShownUdfpsLeftEdgeLottie = true;
mIllustrationLottie.setContentDescription(
@@ -481,17 +407,15 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
configureEnrollmentStage("", R.raw.udfps_left_edge_hint_lottie);
} else if (mIllustrationLottie == null) {
if (isStageHalfCompleted()) {
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_fingerprint_enroll_repeat_message));
mSubTitleText.setText(
R.string.security_settings_fingerprint_enroll_repeat_message);
} else {
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_udfps_enroll_edge_message));
mSubTitleText.setText(R.string.security_settings_udfps_enroll_edge_message);
}
}
break;
case STAGE_RIGHT_EDGE:
glifLayoutHelper.setHeaderText(
R.string.security_settings_udfps_enroll_right_edge_title);
mTitleText.setText(R.string.security_settings_udfps_enroll_right_edge_title);
if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) {
mHaveShownUdfpsRightEdgeLottie = true;
mIllustrationLottie.setContentDescription(
@@ -501,31 +425,22 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
} else if (mIllustrationLottie == null) {
if (isStageHalfCompleted()) {
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_fingerprint_enroll_repeat_message));
mSubTitleText.setText(
R.string.security_settings_fingerprint_enroll_repeat_message);
} else {
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_udfps_enroll_edge_message));
mSubTitleText.setText(R.string.security_settings_udfps_enroll_edge_message);
}
}
break;
case STAGE_UNKNOWN:
default:
// setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title);
// Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
// which gets announced for a11y upon entering the page. For UDFPS, we want to
// announce a different string for a11y upon entering the page.
glifLayoutHelper.setHeaderText(
R.string.security_settings_fingerprint_enroll_udfps_title);
glifLayoutHelper.setDescriptionText(getString(
R.string.security_settings_udfps_enroll_start_message));
mTitleText.setText(R.string.security_settings_fingerprint_enroll_udfps_title);
mSubTitleText.setText(R.string.security_settings_udfps_enroll_start_message);
final CharSequence description = getString(
R.string.security_settings_udfps_enroll_a11y);
mView.getHeaderTextView().setContentDescription(description);
activity.setTitle(description);
getActivity().setTitle(description);
break;
}
}
@@ -538,7 +453,7 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
return defaultDensity == currentDensity;
}
private void updateOrientation(@NonNull GlifLayout glifLayout, int orientation) {
private void updateOrientation(@NonNull RelativeLayout content, int orientation) {
switch (orientation) {
case Configuration.ORIENTATION_LANDSCAPE: {
mIllustrationLottie = null;
@@ -546,7 +461,7 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
}
case Configuration.ORIENTATION_PORTRAIT: {
if (mShouldShowLottie) {
mIllustrationLottie = glifLayout.findViewById(R.id.illustration_lottie);
mIllustrationLottie = content.findViewById(R.id.illustration_lottie);
}
break;
}
@@ -625,7 +540,7 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
}
private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
new GlifLayoutHelper(getActivity(), mView).setDescriptionText(description);
mSubTitleText.setText(description);
LottieCompositionFactory.fromRawRes(getActivity(), lottie)
.addListener((c) -> {
mIllustrationLottie.setComposition(c);
@@ -634,31 +549,6 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
});
}
private void setOnHoverListener(boolean isLandscape, GlifLayout enrollLayout,
UdfpsEnrollView udfpsEnrollView) {
if (!mIsAccessibilityEnabled) return;
final Context context = getActivity();
final View.OnHoverListener onHoverListener = (v, event) -> {
// Map the touch to portrait mode if the device is in
// landscape mode.
final Point scaledTouch =
mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0),
event, udfpsEnrollView.getOverlayParams());
final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea(
mEnrollingViewModel.isTouchExplorationEnabled(), context,
scaledTouch.x, scaledTouch.y, udfpsEnrollView.getOverlayParams());
if (theStr != null) {
v.announceForAccessibility(theStr);
}
return false;
};
enrollLayout.findManagedViewById(isLandscape ? R.id.sud_landscape_content_area
: R.id.sud_layout_content).setOnHoverListener(onHoverListener);
}
private void onEnrollmentProgressChange(@NonNull EnrollmentProgress progress) {
updateProgress(true /* animate */, progress);
@@ -678,34 +568,43 @@ public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
private void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (!TextUtils.isEmpty(helpString)) {
showError(helpString);
mUdfpsEnrollHelper.onEnrollmentHelp();
mUdfpsEnrollView.onEnrollmentHelp();
}
}
private void onAcquired(boolean isAcquiredGood) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onAcquired(isAcquiredGood);
if (mUdfpsEnrollView != null) {
mUdfpsEnrollView.onAcquired(isAcquiredGood);
}
}
private void onPointerDown(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerDown(sensorId);
if (mUdfpsEnrollView != null) {
mUdfpsEnrollView.onPointerDown(sensorId);
}
}
private void onPointerUp(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerUp(sensorId);
if (mUdfpsEnrollView != null) {
mUdfpsEnrollView.onPointerUp(sensorId);
}
}
private void showError(CharSequence error) {
mView.setHeaderText(error);
mView.getHeaderTextView().setContentDescription(error);
new GlifLayoutHelper(getActivity(), mView).setDescriptionText("");
mTitleText.setText(error);
mTitleText.setContentDescription(error);
mSubTitleText.setContentDescription("");
}
private float convertDpToPixel(float dp, Context context) {
float px = dp * getDensity(context);
return px;
}
private float getDensity(Context context) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return metrics.density;
}
private final Runnable mShowDialogRunnable = new Runnable() {
@Override

View File

@@ -0,0 +1,436 @@
/*
* Copyright (C) 2023 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.biometrics2.ui.widget;
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.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.Log;
import android.util.PathParser;
import android.util.TypedValue;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import java.util.ArrayList;
import java.util.List;
/**
* 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;
private static final float SCALE = 0.5f;
private static final String SCALE_OVERRIDE =
"com.android.systemui.biometrics.UdfpsEnrollHelper.scale";
private static final String NEW_COORDS_OVERRIDE =
"com.android.systemui.biometrics.UdfpsNewCoords";
@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;
// 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;
private int mTotalSteps = -1;
private int mRemainingSteps = -1;
private int mLocationsEnrolled = 0;
private int mCenterTouchCount = 0;
private FingerprintManager mFingerprintManager;
private boolean mAccessibilityEnabled;
private Context mContext;
private final List<PointF> mGuidedEnrollmentPoints;
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) {
}
};
mContext = context;
mFingerprintManager = context.getSystemService(FingerprintManager.class);
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
mAccessibilityEnabled = am.isEnabled();
mGuidedEnrollmentPoints = new ArrayList<>();
initEnrollPoint(context);
}
/** 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 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(final int remaining, final int totalSteps) {
if (mTotalSteps == -1) {
mTotalSteps = totalSteps;
}
if (remaining != mRemainingSteps) {
mLocationsEnrolled++;
if (isCenterEnrollmentStage()) {
mCenterTouchCount++;
}
}
mRemainingSteps = remaining;
if (!isCenterEnrollmentStage()) {
if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
mTargetAnimatorSet.end();
}
final PointF point = 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 (!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 = 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 = 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();
}
private boolean isCenterEnrollmentStage() {
if (mTotalSteps == -1 || mRemainingSteps == -1) {
return true;
}
return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
}
private int getStageThresholdSteps(int totalSteps, int stageIndex) {
return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
}
private 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);
}
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);
}
private boolean isTipEnrollmentStage() {
if (mTotalSteps == -1 || mRemainingSteps == -1) {
return false;
}
final int progressSteps = mTotalSteps - mRemainingSteps;
return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
&& progressSteps < getStageThresholdSteps(mTotalSteps, 2);
}
private boolean isEdgeEnrollmentStage() {
if (mTotalSteps == -1 || mRemainingSteps == -1) {
return false;
}
return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
}
private void initEnrollPoint(Context context) {
// 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));
}
}
}

View File

@@ -0,0 +1,424 @@
/*
* Copyright (C) 2023 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.biometrics2.ui.widget;
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.internal.annotations.VisibleForTesting;
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;
@VisibleForTesting
@NonNull
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(final int remaining, final int totalSteps) {
android.util.Log.e(TAG, "remaining =" + remaining);
android.util.Log.e(TAG, "totalSteps =" + 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,260 @@
/*
* Copyright (C) 2023 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.biometrics2.ui.widget;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.RotationUtils;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.settingslib.udfps.UdfpsUtils;
/**
* View corresponding with udfps_enroll_view.xml
*/
public class UdfpsEnrollView extends FrameLayout {
private static final String TAG = "UdfpsEnrollView";
@NonNull
private final UdfpsEnrollDrawable mFingerprintDrawable;
@NonNull
private final UdfpsEnrollProgressBarDrawable mFingerprintProgressDrawable;
@NonNull
private final Handler mHandler;
@NonNull
private ImageView mFingerprintProgressView;
private UdfpsUtils mUdfpsUtils;
private int mProgressBarRadius;
private Rect mSensorRect;
private UdfpsOverlayParams mOverlayParams;
private FingerprintSensorPropertiesInternal mSensorProperties;
private int mTotalSteps = -1;
private int mRemainingSteps = -1;
private int mLocationsEnrolled = 0;
private int mCenterTouchCount = 0;
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());
mUdfpsUtils = new UdfpsUtils();
}
@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);
}
/**
* Receive enroll progress information from FingerprintEnrollEnrollingUdfpsFragment
*/
public void onEnrollmentProgress(int remaining, int totalSteps) {
if (mTotalSteps == -1) {
mTotalSteps = totalSteps;
}
mRemainingSteps = remaining;
mHandler.post(() -> {
mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
});
}
/**
* Receive enroll help information from FingerprintEnrollEnrollingUdfpsFragment
*/
public void onEnrollmentHelp() {
mHandler.post(
() -> mFingerprintProgressDrawable.onEnrollmentHelp(mRemainingSteps, mTotalSteps));
}
/**
* Receive onAcquired from FingerprintEnrollEnrollingUdfpsFragment
*/
public void onAcquired(boolean isAcquiredGood) {
final boolean animateIfLastStepGood =
isAcquiredGood && (mRemainingSteps <= 2 && mRemainingSteps >= 0);
mHandler.post(() -> {
onFingerUp();
if (animateIfLastStepGood) mFingerprintProgressDrawable.onLastStepAcquired();
});
}
/**
* Receive onPointerDown from FingerprintEnrollEnrollingUdfpsFragment
*/
public void onPointerDown(int sensorId) {
onFingerDown();
}
/**
* Receive onPointerUp from FingerprintEnrollEnrollingUdfpsFragment
*/
public void onPointerUp(int sensorId) {
onFingerUp();
}
/**
* setup SensorProperties
*/
public void setSensorProperties(FingerprintSensorPropertiesInternal properties) {
mSensorProperties = properties;
((ViewGroup) getParent()).getViewTreeObserver().addOnDrawListener(
new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
updateOverlayParams();
}
});
}
private void onSensorRectUpdated() {
updateDimensions();
// 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.
final Rect rotatedBounds = new Rect(mOverlayParams.getSensorBounds());
int rotation = mOverlayParams.getRotation();
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
RotationUtils.rotateBounds(
rotatedBounds,
mOverlayParams.getNaturalDisplayWidth(),
mOverlayParams.getNaturalDisplayHeight(),
rotation
);
}
RelativeLayout parent = ((RelativeLayout) getParent());
final int[] coords = parent.getLocationOnScreen();
final int parentLeft = coords[0];
final int parentTop = coords[1];
final int parentRight = parentLeft + parent.getWidth();
final int parentBottom = parentTop + parent.getHeight();
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(getWidth(),
getHeight());
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.rightMargin = parentRight - rotatedBounds.right - getPaddingX();
params.topMargin = rotatedBounds.top - parentTop - getPaddingY();
} else {
if (rotation == Surface.ROTATION_90) {
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.rightMargin = parentRight - rotatedBounds.right - getPaddingX();
params.bottomMargin = parentBottom - rotatedBounds.bottom - getPaddingY();
} else {
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
params.bottomMargin = parentBottom - rotatedBounds.bottom - getPaddingY();
params.leftMargin = rotatedBounds.left - parentLeft - getPaddingX();
}
}
params.height = rotatedBounds.height() + 2 * getPaddingX();
params.width = rotatedBounds.width() + 2 * getPaddingY();
setLayoutParams(params);
}
private void onFingerDown() {
mFingerprintDrawable.setShouldSkipDraw(true);
mFingerprintDrawable.invalidateSelf();
}
private void onFingerUp() {
mFingerprintDrawable.setShouldSkipDraw(false);
mFingerprintDrawable.invalidateSelf();
}
private int getPaddingX() {
return mProgressBarRadius;
}
private int getPaddingY() {
return mProgressBarRadius;
}
private void updateOverlayParams() {
if (mSensorProperties == null) {
android.util.Log.e(TAG, "There is no sensor info!");
return;
}
DisplayInfo displayInfo = new DisplayInfo();
getDisplay().getDisplayInfo(displayInfo);
Rect udfpsBounds = mSensorProperties.getLocation().getRect();
float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
udfpsBounds.scale(scaleFactor);
final Rect overlayBounds = new Rect(
0, /* left */
displayInfo.getNaturalHeight() / 2, /* top */
displayInfo.getNaturalWidth(), /* right */
displayInfo.getNaturalHeight() /* botom */);
mOverlayParams = new UdfpsOverlayParams(
udfpsBounds,
overlayBounds,
displayInfo.getNaturalWidth(),
displayInfo.getNaturalHeight(),
scaleFactor,
displayInfo.rotation);
post(() -> {
mProgressBarRadius =
(int) (mOverlayParams.getScaleFactor() * getContext().getResources().getInteger(
R.integer.config_udfpsEnrollProgressBar));
mSensorRect = new Rect(mOverlayParams.getSensorBounds());
onSensorRectUpdated();
});
}
}