Merge "[BiometricsV2] Refactor EnrollingSfpsFragment"
This commit is contained in:
committed by
Android (Google) Code Review
commit
dfe6fac149
@@ -1,599 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.view;
|
|
||||||
|
|
||||||
import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.annotation.RawRes;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.ColorStateList;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.PorterDuffColorFilter;
|
|
||||||
import android.hardware.fingerprint.FingerprintManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
||||||
import android.view.animation.AnimationUtils;
|
|
||||||
import android.view.animation.Interpolator;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
|
|
||||||
import androidx.activity.OnBackPressedCallback;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import androidx.lifecycle.Observer;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import com.android.settings.R;
|
|
||||||
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog;
|
|
||||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
|
|
||||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
|
|
||||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
|
|
||||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
|
|
||||||
|
|
||||||
import com.airbnb.lottie.LottieAnimationView;
|
|
||||||
import com.airbnb.lottie.LottieCompositionFactory;
|
|
||||||
import com.airbnb.lottie.LottieProperty;
|
|
||||||
import com.airbnb.lottie.model.KeyPath;
|
|
||||||
import com.google.android.setupcompat.template.FooterBarMixin;
|
|
||||||
import com.google.android.setupcompat.template.FooterButton;
|
|
||||||
import com.google.android.setupdesign.GlifLayout;
|
|
||||||
import com.google.android.setupdesign.template.DescriptionMixin;
|
|
||||||
import com.google.android.setupdesign.template.HeaderMixin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment is used to handle enrolling process for sfps
|
|
||||||
*/
|
|
||||||
public class FingerprintEnrollEnrollingSfpsFragment extends Fragment {
|
|
||||||
|
|
||||||
private static final String TAG = FingerprintEnrollEnrollingSfpsFragment.class.getSimpleName();
|
|
||||||
private static final boolean DEBUG = false;
|
|
||||||
|
|
||||||
private static final int PROGRESS_BAR_MAX = 10000;
|
|
||||||
private static final long ANIMATION_DURATION = 250L;
|
|
||||||
private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
|
|
||||||
private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
|
|
||||||
|
|
||||||
private static final int STAGE_UNKNOWN = -1;
|
|
||||||
private static final int SFPS_STAGE_NO_ANIMATION = 0;
|
|
||||||
private static final int SFPS_STAGE_CENTER = 1;
|
|
||||||
private static final int SFPS_STAGE_FINGERTIP = 2;
|
|
||||||
private static final int SFPS_STAGE_LEFT_EDGE = 3;
|
|
||||||
private static final int SFPS_STAGE_RIGHT_EDGE = 4;
|
|
||||||
|
|
||||||
private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
|
|
||||||
private FingerprintEnrollProgressViewModel mProgressViewModel;
|
|
||||||
|
|
||||||
private Interpolator mFastOutSlowInInterpolator;
|
|
||||||
|
|
||||||
private GlifLayout mView;
|
|
||||||
private ProgressBar mProgressBar;
|
|
||||||
private ObjectAnimator mProgressAnim;
|
|
||||||
|
|
||||||
private LottieAnimationView mIllustrationLottie;
|
|
||||||
|
|
||||||
private boolean mHaveShownSfpsNoAnimationLottie;
|
|
||||||
private boolean mHaveShownSfpsCenterLottie;
|
|
||||||
private boolean mHaveShownSfpsTipLottie;
|
|
||||||
private boolean mHaveShownSfpsLeftEdgeLottie;
|
|
||||||
private boolean mHaveShownSfpsRightEdgeLottie;
|
|
||||||
private ObjectAnimator mHelpAnimation;
|
|
||||||
private int mIconTouchCount;
|
|
||||||
|
|
||||||
private final View.OnClickListener mOnSkipClickListener = v -> {
|
|
||||||
mEnrollingViewModel.setOnSkipPressed();
|
|
||||||
cancelEnrollment();
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Observer<EnrollmentProgress> mProgressObserver = progress -> {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "mProgressObserver(" + progress + ")");
|
|
||||||
}
|
|
||||||
if (progress != null && progress.getSteps() >= 0) {
|
|
||||||
onEnrollmentProgressChange(progress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Observer<EnrollmentStatusMessage> mHelpMessageObserver = helpMessage -> {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "mHelpMessageObserver(" + helpMessage + ")");
|
|
||||||
}
|
|
||||||
if (helpMessage != null) {
|
|
||||||
onEnrollmentHelp(helpMessage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Observer<EnrollmentStatusMessage> mErrorMessageObserver = errorMessage -> {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "mErrorMessageObserver(" + errorMessage + ")");
|
|
||||||
}
|
|
||||||
if (errorMessage != null) {
|
|
||||||
onEnrollmentError(errorMessage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(@NonNull Context context) {
|
|
||||||
final FragmentActivity activity = getActivity();
|
|
||||||
final ViewModelProvider provider = new ViewModelProvider(activity);
|
|
||||||
mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
|
|
||||||
mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
|
|
||||||
super.onAttach(context);
|
|
||||||
requireActivity().getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) {
|
|
||||||
@Override
|
|
||||||
public void handleOnBackPressed() {
|
|
||||||
setEnabled(false);
|
|
||||||
mEnrollingViewModel.setOnBackPressed();
|
|
||||||
cancelEnrollment();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
mView = initSfpsLayout(inflater, container);
|
|
||||||
maybeHideSfpsText(getActivity().getResources().getConfiguration());
|
|
||||||
return mView;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GlifLayout initSfpsLayout(LayoutInflater inflater, ViewGroup container) {
|
|
||||||
final GlifLayout containView = (GlifLayout) inflater.inflate(R.layout.sfps_enroll_enrolling,
|
|
||||||
container, false);
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
|
|
||||||
new GlifLayoutHelper(activity, containView).setDescriptionText(
|
|
||||||
getString(R.string.security_settings_fingerprint_enroll_start_message));
|
|
||||||
|
|
||||||
// setHelpAnimation()
|
|
||||||
final float translationX = 40;
|
|
||||||
final int duration = 550;
|
|
||||||
final RelativeLayout progressLottieLayout = containView.findViewById(R.id.progress_lottie);
|
|
||||||
mHelpAnimation = ObjectAnimator.ofFloat(progressLottieLayout,
|
|
||||||
"translationX" /* propertyName */,
|
|
||||||
0, translationX, -1 * translationX, translationX, 0f);
|
|
||||||
mHelpAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
|
|
||||||
mHelpAnimation.setDuration(duration);
|
|
||||||
mHelpAnimation.setAutoCancel(false);
|
|
||||||
|
|
||||||
mIllustrationLottie = containView.findViewById(R.id.illustration_lottie);
|
|
||||||
|
|
||||||
mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
|
|
||||||
final FooterBarMixin footerBarMixin = containView.getMixin(FooterBarMixin.class);
|
|
||||||
footerBarMixin.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);
|
|
||||||
|
|
||||||
mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
|
|
||||||
mProgressBar.setOnTouchListener((v, event) -> {
|
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
|
||||||
mIconTouchCount++;
|
|
||||||
if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
|
|
||||||
showIconTouchDialog();
|
|
||||||
} else {
|
|
||||||
mProgressBar.postDelayed(mShowDialogRunnable,
|
|
||||||
ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
|
|
||||||
}
|
|
||||||
} else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|
|
||||||
|| event.getActionMasked() == MotionEvent.ACTION_UP) {
|
|
||||||
mProgressBar.removeCallbacks(mShowDialogRunnable);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return containView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
startEnrollment();
|
|
||||||
updateProgress(false /* animate */, mProgressViewModel.getProgressLiveData().getValue());
|
|
||||||
final EnrollmentStatusMessage msg = mProgressViewModel.getHelpMessageLiveData().getValue();
|
|
||||||
if (msg != null) {
|
|
||||||
onEnrollmentHelp(msg);
|
|
||||||
} else {
|
|
||||||
clearError();
|
|
||||||
updateTitleAndDescription();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
removeEnrollmentObservers();
|
|
||||||
if (!getActivity().isChangingConfigurations() && mProgressViewModel.isEnrolling()) {
|
|
||||||
mProgressViewModel.cancelEnrollment();
|
|
||||||
}
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeEnrollmentObservers() {
|
|
||||||
preRemoveEnrollmentObservers();
|
|
||||||
mProgressViewModel.getErrorMessageLiveData().removeObserver(mErrorMessageObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void preRemoveEnrollmentObservers() {
|
|
||||||
mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
|
|
||||||
mProgressViewModel.getHelpMessageLiveData().removeObserver(mHelpMessageObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelEnrollment() {
|
|
||||||
preRemoveEnrollmentObservers();
|
|
||||||
mProgressViewModel.cancelEnrollment();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startEnrollment() {
|
|
||||||
final boolean startResult = mProgressViewModel.startEnrollment(ENROLL_ENROLL);
|
|
||||||
if (!startResult) {
|
|
||||||
Log.e(TAG, "startEnrollment(), failed");
|
|
||||||
}
|
|
||||||
mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
|
|
||||||
mProgressViewModel.getHelpMessageLiveData().observe(this, mHelpMessageObserver);
|
|
||||||
mProgressViewModel.getErrorMessageLiveData().observe(this, mErrorMessageObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
|
|
||||||
new GlifLayoutHelper(getActivity(), mView).setDescriptionText(description);
|
|
||||||
LottieCompositionFactory.fromRawRes(getActivity(), lottie)
|
|
||||||
.addListener((c) -> {
|
|
||||||
mIllustrationLottie.setComposition(c);
|
|
||||||
mIllustrationLottie.setVisibility(View.VISIBLE);
|
|
||||||
mIllustrationLottie.playAnimation();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getCurrentSfpsStage() {
|
|
||||||
EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
|
|
||||||
|
|
||||||
if (progressLiveData == null) {
|
|
||||||
return STAGE_UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
|
|
||||||
if (progressSteps < getStageThresholdSteps(0)) {
|
|
||||||
return SFPS_STAGE_NO_ANIMATION;
|
|
||||||
} else if (progressSteps < getStageThresholdSteps(1)) {
|
|
||||||
return SFPS_STAGE_CENTER;
|
|
||||||
} else if (progressSteps < getStageThresholdSteps(2)) {
|
|
||||||
return SFPS_STAGE_FINGERTIP;
|
|
||||||
} else if (progressSteps < getStageThresholdSteps(3)) {
|
|
||||||
return SFPS_STAGE_LEFT_EDGE;
|
|
||||||
} else {
|
|
||||||
return SFPS_STAGE_RIGHT_EDGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onEnrollmentHelp(@NonNull EnrollmentStatusMessage helpMessage) {
|
|
||||||
final CharSequence helpStr = helpMessage.getStr();
|
|
||||||
if (!TextUtils.isEmpty(helpStr)) {
|
|
||||||
showError(helpStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onEnrollmentError(@NonNull EnrollmentStatusMessage errorMessage) {
|
|
||||||
removeEnrollmentObservers();
|
|
||||||
|
|
||||||
if (mEnrollingViewModel.getOnBackPressed()
|
|
||||||
&& errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
|
|
||||||
mEnrollingViewModel.onCancelledDueToOnBackPressed();
|
|
||||||
} else if (mEnrollingViewModel.getOnSkipPressed()
|
|
||||||
&& errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
|
|
||||||
mEnrollingViewModel.onCancelledDueToOnSkipPressed();
|
|
||||||
} else {
|
|
||||||
final int errMsgId = errorMessage.getMsgId();
|
|
||||||
mEnrollingViewModel.showErrorDialog(
|
|
||||||
new FingerprintEnrollEnrollingViewModel.ErrorDialogData(
|
|
||||||
getString(FingerprintErrorDialog.getErrorMessage(errMsgId)),
|
|
||||||
getString(FingerprintErrorDialog.getErrorTitle(errMsgId)),
|
|
||||||
errMsgId
|
|
||||||
));
|
|
||||||
mProgressViewModel.cancelEnrollment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void announceEnrollmentProgress(CharSequence announcement) {
|
|
||||||
mEnrollingViewModel.sendAccessibilityEvent(announcement);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onEnrollmentProgressChange(@NonNull EnrollmentProgress progress) {
|
|
||||||
updateProgress(true /* animate */, progress);
|
|
||||||
if (mEnrollingViewModel.isAccessibilityEnabled()) {
|
|
||||||
final int percent = (int) (((float) (progress.getSteps() - progress.getRemaining())
|
|
||||||
/ (float) progress.getSteps()) * 100);
|
|
||||||
|
|
||||||
CharSequence announcement = getString(
|
|
||||||
R.string.security_settings_sfps_enroll_progress_a11y_message, percent);
|
|
||||||
announceEnrollmentProgress(announcement);
|
|
||||||
|
|
||||||
mIllustrationLottie.setContentDescription(
|
|
||||||
getString(R.string.security_settings_sfps_animation_a11y_label, percent)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
updateTitleAndDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProgress(boolean animate, @NonNull EnrollmentProgress enrollmentProgress) {
|
|
||||||
if (!mProgressViewModel.isEnrolling()) {
|
|
||||||
Log.d(TAG, "Enrollment not started yet");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int progress = getProgress(enrollmentProgress);
|
|
||||||
// Only clear the error when progress has been made.
|
|
||||||
// TODO (b/234772728) Add tests.
|
|
||||||
if (mProgressBar != null && mProgressBar.getProgress() < progress) {
|
|
||||||
clearError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
animateProgress(progress);
|
|
||||||
} else {
|
|
||||||
if (mProgressBar != null) {
|
|
||||||
mProgressBar.setProgress(progress);
|
|
||||||
}
|
|
||||||
if (progress >= PROGRESS_BAR_MAX) {
|
|
||||||
mDelayedFinishRunnable.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getProgress(@NonNull EnrollmentProgress progress) {
|
|
||||||
if (progress.getSteps() == -1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
int displayProgress = Math.max(0, progress.getSteps() + 1 - progress.getRemaining());
|
|
||||||
return PROGRESS_BAR_MAX * displayProgress / (progress.getSteps() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showError(CharSequence error) {
|
|
||||||
mView.setHeaderText(error);
|
|
||||||
mView.getHeaderTextView().setContentDescription(error);
|
|
||||||
new GlifLayoutHelper(getActivity(), mView).setDescriptionText("");
|
|
||||||
if (isResumed() && !mHelpAnimation.isRunning()) {
|
|
||||||
mHelpAnimation.start();
|
|
||||||
}
|
|
||||||
applySfpsErrorDynamicColors(true);
|
|
||||||
if (isResumed() && mEnrollingViewModel.isAccessibilityEnabled()) {
|
|
||||||
mEnrollingViewModel.vibrateError(getClass().getSimpleName() + "::showError");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearError() {
|
|
||||||
applySfpsErrorDynamicColors(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateProgress(int progress) {
|
|
||||||
if (mProgressAnim != null) {
|
|
||||||
mProgressAnim.cancel();
|
|
||||||
}
|
|
||||||
ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress",
|
|
||||||
mProgressBar.getProgress(), progress);
|
|
||||||
anim.addListener(mProgressAnimationListener);
|
|
||||||
anim.setInterpolator(mFastOutSlowInInterpolator);
|
|
||||||
anim.setDuration(ANIMATION_DURATION);
|
|
||||||
anim.start();
|
|
||||||
mProgressAnim = anim;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies dynamic colors corresponding to showing or clearing errors on the progress bar
|
|
||||||
* and finger lottie for SFPS
|
|
||||||
*/
|
|
||||||
private void applySfpsErrorDynamicColors(boolean isError) {
|
|
||||||
applyProgressBarDynamicColor(isError);
|
|
||||||
applyLottieDynamicColor(isError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyProgressBarDynamicColor(boolean isError) {
|
|
||||||
final Context context = getActivity().getApplicationContext();
|
|
||||||
int error_color = context.getColor(R.color.sfps_enrollment_progress_bar_error_color);
|
|
||||||
int progress_bar_fill_color = context.getColor(
|
|
||||||
R.color.sfps_enrollment_progress_bar_fill_color);
|
|
||||||
ColorStateList fillColor = ColorStateList.valueOf(
|
|
||||||
isError ? error_color : progress_bar_fill_color);
|
|
||||||
mProgressBar.setProgressTintList(fillColor);
|
|
||||||
mProgressBar.setProgressTintMode(PorterDuff.Mode.SRC);
|
|
||||||
mProgressBar.invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyLottieDynamicColor(boolean isError) {
|
|
||||||
final Context context = getActivity().getApplicationContext();
|
|
||||||
int error_color = context.getColor(R.color.sfps_enrollment_fp_error_color);
|
|
||||||
int fp_captured_color = context.getColor(R.color.sfps_enrollment_fp_captured_color);
|
|
||||||
int color = isError ? error_color : fp_captured_color;
|
|
||||||
mIllustrationLottie.addValueCallback(
|
|
||||||
new KeyPath(".blue100", "**"),
|
|
||||||
LottieProperty.COLOR_FILTER,
|
|
||||||
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
|
||||||
);
|
|
||||||
mIllustrationLottie.invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getStageThresholdSteps(int index) {
|
|
||||||
final EnrollmentProgress progressLiveData =
|
|
||||||
mProgressViewModel.getProgressLiveData().getValue();
|
|
||||||
|
|
||||||
if (progressLiveData == null || progressLiveData.getSteps() == -1) {
|
|
||||||
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return Math.round(progressLiveData.getSteps()
|
|
||||||
* mEnrollingViewModel.getEnrollStageThreshold(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTitleAndDescription() {
|
|
||||||
final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(), mView);
|
|
||||||
if (mEnrollingViewModel.isAccessibilityEnabled()) {
|
|
||||||
mEnrollingViewModel.clearTalkback();
|
|
||||||
glifLayoutHelper.getGlifLayout().getDescriptionTextView().setAccessibilityLiveRegion(
|
|
||||||
View.ACCESSIBILITY_LIVE_REGION_POLITE);
|
|
||||||
}
|
|
||||||
final int stage = getCurrentSfpsStage();
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "updateTitleAndDescription, stage:" + stage
|
|
||||||
+ ", noAnimation:" + mHaveShownSfpsNoAnimationLottie
|
|
||||||
+ ", center:" + mHaveShownSfpsCenterLottie
|
|
||||||
+ ", tip:" + mHaveShownSfpsTipLottie
|
|
||||||
+ ", leftEdge:" + mHaveShownSfpsLeftEdgeLottie
|
|
||||||
+ ", rightEdge:" + mHaveShownSfpsRightEdgeLottie);
|
|
||||||
}
|
|
||||||
switch (stage) {
|
|
||||||
case SFPS_STAGE_NO_ANIMATION:
|
|
||||||
glifLayoutHelper.setHeaderText(
|
|
||||||
R.string.security_settings_fingerprint_enroll_repeat_title);
|
|
||||||
if (!mHaveShownSfpsNoAnimationLottie) {
|
|
||||||
mHaveShownSfpsNoAnimationLottie = true;
|
|
||||||
mIllustrationLottie.setContentDescription(
|
|
||||||
getString(
|
|
||||||
R.string.security_settings_sfps_animation_a11y_label,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
configureEnrollmentStage(
|
|
||||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
|
||||||
R.raw.sfps_lottie_no_animation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SFPS_STAGE_CENTER:
|
|
||||||
glifLayoutHelper.setHeaderText(
|
|
||||||
R.string.security_settings_sfps_enroll_finger_center_title);
|
|
||||||
if (!mHaveShownSfpsCenterLottie) {
|
|
||||||
mHaveShownSfpsCenterLottie = true;
|
|
||||||
configureEnrollmentStage(
|
|
||||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
|
||||||
R.raw.sfps_lottie_pad_center
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SFPS_STAGE_FINGERTIP:
|
|
||||||
glifLayoutHelper.setHeaderText(
|
|
||||||
R.string.security_settings_sfps_enroll_fingertip_title);
|
|
||||||
if (!mHaveShownSfpsTipLottie) {
|
|
||||||
mHaveShownSfpsTipLottie = true;
|
|
||||||
configureEnrollmentStage("", R.raw.sfps_lottie_tip);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SFPS_STAGE_LEFT_EDGE:
|
|
||||||
glifLayoutHelper.setHeaderText(
|
|
||||||
R.string.security_settings_sfps_enroll_left_edge_title);
|
|
||||||
if (!mHaveShownSfpsLeftEdgeLottie) {
|
|
||||||
mHaveShownSfpsLeftEdgeLottie = true;
|
|
||||||
configureEnrollmentStage("", R.raw.sfps_lottie_left_edge);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SFPS_STAGE_RIGHT_EDGE:
|
|
||||||
glifLayoutHelper.setHeaderText(
|
|
||||||
R.string.security_settings_sfps_enroll_right_edge_title);
|
|
||||||
if (!mHaveShownSfpsRightEdgeLottie) {
|
|
||||||
mHaveShownSfpsRightEdgeLottie = true;
|
|
||||||
configureEnrollmentStage("", R.raw.sfps_lottie_right_edge);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case STAGE_UNKNOWN:
|
|
||||||
default:
|
|
||||||
// Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
|
|
||||||
// which gets announced for a11y upon entering the page. For SFPS, we want to
|
|
||||||
// announce a different string for a11y upon entering the page.
|
|
||||||
glifLayoutHelper.setHeaderText(
|
|
||||||
R.string.security_settings_sfps_enroll_find_sensor_title);
|
|
||||||
glifLayoutHelper.setDescriptionText(getString(
|
|
||||||
R.string.security_settings_sfps_enroll_start_message));
|
|
||||||
final CharSequence description = getString(
|
|
||||||
R.string.security_settings_sfps_enroll_find_sensor_message);
|
|
||||||
glifLayoutHelper.getGlifLayout().getHeaderTextView().setContentDescription(
|
|
||||||
description);
|
|
||||||
glifLayoutHelper.getActivity().setTitle(description);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showIconTouchDialog() {
|
|
||||||
mIconTouchCount = 0;
|
|
||||||
mEnrollingViewModel.showIconTouchDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Runnable mShowDialogRunnable = () -> showIconTouchDialog();
|
|
||||||
|
|
||||||
private final Animator.AnimatorListener mProgressAnimationListener =
|
|
||||||
new Animator.AnimatorListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animation) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animator animation) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) {
|
|
||||||
mProgressBar.postDelayed(mDelayedFinishRunnable, ANIMATION_DURATION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animation) { }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Give the user a chance to see progress completed before jumping to the next stage.
|
|
||||||
private final Runnable mDelayedFinishRunnable = () -> mEnrollingViewModel.onEnrollingDone();
|
|
||||||
|
|
||||||
private void maybeHideSfpsText(@NonNull Configuration newConfig) {
|
|
||||||
final HeaderMixin headerMixin = ((GlifLayout) mView).getMixin(HeaderMixin.class);
|
|
||||||
final DescriptionMixin descriptionMixin = ((GlifLayout) mView).getMixin(
|
|
||||||
DescriptionMixin.class);
|
|
||||||
final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
|
|
||||||
|
|
||||||
if (isLandscape) {
|
|
||||||
headerMixin.setAutoTextSizeEnabled(true);
|
|
||||||
headerMixin.getTextView().setMinLines(0);
|
|
||||||
headerMixin.getTextView().setMaxLines(10);
|
|
||||||
descriptionMixin.getTextView().setMinLines(0);
|
|
||||||
descriptionMixin.getTextView().setMaxLines(10);
|
|
||||||
} else {
|
|
||||||
headerMixin.setAutoTextSizeEnabled(false);
|
|
||||||
headerMixin.getTextView().setLines(4);
|
|
||||||
// hide the description
|
|
||||||
descriptionMixin.getTextView().setLines(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,620 @@
|
|||||||
|
/*
|
||||||
|
* 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.view
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.annotation.RawRes
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.view.animation.Interpolator
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
|
import com.airbnb.lottie.LottieComposition
|
||||||
|
import com.airbnb.lottie.LottieCompositionFactory
|
||||||
|
import com.airbnb.lottie.LottieProperty
|
||||||
|
import com.airbnb.lottie.model.KeyPath
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
|
||||||
|
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||||
|
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||||
|
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
|
||||||
|
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||||
|
import com.google.android.setupcompat.template.FooterBarMixin
|
||||||
|
import com.google.android.setupcompat.template.FooterButton
|
||||||
|
import com.google.android.setupdesign.GlifLayout
|
||||||
|
import com.google.android.setupdesign.template.DescriptionMixin
|
||||||
|
import com.google.android.setupdesign.template.HeaderMixin
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment is used to handle enrolling process for sfps
|
||||||
|
*/
|
||||||
|
class FingerprintEnrollEnrollingSfpsFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
|
||||||
|
private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
|
||||||
|
get() = _enrollingViewModel!!
|
||||||
|
|
||||||
|
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||||
|
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||||
|
get() = _progressViewModel!!
|
||||||
|
|
||||||
|
private val fastOutSlowInInterpolator: Interpolator
|
||||||
|
get() = AnimationUtils.loadInterpolator(activity, R.interpolator.fast_out_slow_in)
|
||||||
|
|
||||||
|
private var enrollingSfpsView: GlifLayout? = null
|
||||||
|
|
||||||
|
private val progressBar: ProgressBar
|
||||||
|
get() = enrollingSfpsView!!.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||||
|
|
||||||
|
private var progressAnim: ObjectAnimator? = null
|
||||||
|
|
||||||
|
private val progressAnimationListener: Animator.AnimatorListener =
|
||||||
|
object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(animation: Animator) {}
|
||||||
|
override fun onAnimationRepeat(animation: Animator) {}
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
if (progressBar.progress >= PROGRESS_BAR_MAX) {
|
||||||
|
progressBar.postDelayed(delayedFinishRunnable, PROGRESS_ANIMATION_DURATION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val illustrationLottie: LottieAnimationView
|
||||||
|
get() = enrollingSfpsView!!.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!
|
||||||
|
|
||||||
|
private var haveShownSfpsNoAnimationLottie = false
|
||||||
|
private var haveShownSfpsCenterLottie = false
|
||||||
|
private var haveShownSfpsTipLottie = false
|
||||||
|
private var haveShownSfpsLeftEdgeLottie = false
|
||||||
|
private var haveShownSfpsRightEdgeLottie = false
|
||||||
|
|
||||||
|
private var helpAnimation: ObjectAnimator? = null
|
||||||
|
|
||||||
|
private var iconTouchCount = 0
|
||||||
|
|
||||||
|
private val showIconTouchDialogRunnable = Runnable { showIconTouchDialog() }
|
||||||
|
|
||||||
|
// Give the user a chance to see progress completed before jumping to the next stage.
|
||||||
|
private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
|
||||||
|
|
||||||
|
private val onSkipClickListener = View.OnClickListener { _: View? ->
|
||||||
|
enrollingViewModel.setOnSkipPressed()
|
||||||
|
cancelEnrollment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val progressObserver: Observer<EnrollmentProgress> =
|
||||||
|
Observer<EnrollmentProgress> { progress: EnrollmentProgress? ->
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "progressObserver($progress)")
|
||||||
|
}
|
||||||
|
if (progress != null && progress.steps >= 0) {
|
||||||
|
onEnrollmentProgressChange(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val helpMessageObserver: Observer<EnrollmentStatusMessage> =
|
||||||
|
Observer<EnrollmentStatusMessage> { helpMessage: EnrollmentStatusMessage? ->
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "helpMessageObserver($helpMessage)")
|
||||||
|
}
|
||||||
|
helpMessage?.let { onEnrollmentHelp(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorMessageObserver: Observer<EnrollmentStatusMessage> =
|
||||||
|
Observer<EnrollmentStatusMessage> { errorMessage: EnrollmentStatusMessage? ->
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||||
|
}
|
||||||
|
errorMessage?.let { onEnrollmentError(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
ViewModelProvider(requireActivity()).let { provider ->
|
||||||
|
_enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
|
||||||
|
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||||
|
}
|
||||||
|
super.onAttach(context)
|
||||||
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
|
object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
isEnabled = false
|
||||||
|
enrollingViewModel.setOnBackPressed()
|
||||||
|
cancelEnrollment()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
enrollingSfpsView = inflater.inflate(
|
||||||
|
R.layout.sfps_enroll_enrolling,
|
||||||
|
container, false
|
||||||
|
) as GlifLayout
|
||||||
|
return enrollingSfpsView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
requireActivity().bindFingerprintEnrollEnrollingSfpsView(
|
||||||
|
view = enrollingSfpsView!!,
|
||||||
|
onSkipClickListener = onSkipClickListener
|
||||||
|
)
|
||||||
|
|
||||||
|
// setHelpAnimation()
|
||||||
|
helpAnimation = ObjectAnimator.ofFloat(
|
||||||
|
enrollingSfpsView!!.findViewById<RelativeLayout>(R.id.progress_lottie)!!,
|
||||||
|
"translationX" /* propertyName */,
|
||||||
|
0f,
|
||||||
|
HELP_ANIMATION_TRANSLATION_X,
|
||||||
|
-1 * HELP_ANIMATION_TRANSLATION_X,
|
||||||
|
HELP_ANIMATION_TRANSLATION_X,
|
||||||
|
0f
|
||||||
|
).also {
|
||||||
|
it.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
it.setDuration(HELP_ANIMATION_DURATION)
|
||||||
|
it.setAutoCancel(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.setOnTouchListener { _: View?, event: MotionEvent ->
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
|
iconTouchCount++
|
||||||
|
if (iconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
|
||||||
|
showIconTouchDialog()
|
||||||
|
} else {
|
||||||
|
progressBar.postDelayed(
|
||||||
|
showIconTouchDialogRunnable,
|
||||||
|
ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (event.actionMasked == MotionEvent.ACTION_CANCEL
|
||||||
|
|| event.actionMasked == MotionEvent.ACTION_UP
|
||||||
|
) {
|
||||||
|
progressBar.removeCallbacks(showIconTouchDialogRunnable)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
startEnrollment()
|
||||||
|
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||||
|
progressViewModel.helpMessageLiveData.value?.let {
|
||||||
|
onEnrollmentHelp(it)
|
||||||
|
} ?: run {
|
||||||
|
clearError()
|
||||||
|
updateTitleAndDescription()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
removeEnrollmentObservers()
|
||||||
|
if (!activity!!.isChangingConfigurations && progressViewModel.isEnrolling) {
|
||||||
|
progressViewModel.cancelEnrollment()
|
||||||
|
}
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeEnrollmentObservers() {
|
||||||
|
preRemoveEnrollmentObservers()
|
||||||
|
progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preRemoveEnrollmentObservers() {
|
||||||
|
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||||
|
progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelEnrollment() {
|
||||||
|
preRemoveEnrollmentObservers()
|
||||||
|
progressViewModel.cancelEnrollment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startEnrollment() {
|
||||||
|
val startResult: Boolean =
|
||||||
|
progressViewModel.startEnrollment(FingerprintManager.ENROLL_ENROLL)
|
||||||
|
if (!startResult) {
|
||||||
|
Log.e(TAG, "startEnrollment(), failed")
|
||||||
|
}
|
||||||
|
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||||
|
progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
|
||||||
|
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureEnrollmentStage(description: CharSequence, @RawRes lottie: Int) {
|
||||||
|
GlifLayoutHelper(requireActivity(), enrollingSfpsView!!).setDescriptionText(description)
|
||||||
|
LottieCompositionFactory.fromRawRes(activity, lottie)
|
||||||
|
.addListener { c: LottieComposition ->
|
||||||
|
illustrationLottie.setComposition(c)
|
||||||
|
illustrationLottie.visibility = View.VISIBLE
|
||||||
|
illustrationLottie.playAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentSfpsStage: Int
|
||||||
|
get() {
|
||||||
|
val progressLiveData: EnrollmentProgress =
|
||||||
|
progressViewModel.progressLiveData.value
|
||||||
|
?: return STAGE_UNKNOWN
|
||||||
|
val progressSteps: Int = progressLiveData.steps - progressLiveData.remaining
|
||||||
|
return if (progressSteps < getStageThresholdSteps(0)) {
|
||||||
|
SFPS_STAGE_NO_ANIMATION
|
||||||
|
} else if (progressSteps < getStageThresholdSteps(1)) {
|
||||||
|
SFPS_STAGE_CENTER
|
||||||
|
} else if (progressSteps < getStageThresholdSteps(2)) {
|
||||||
|
SFPS_STAGE_FINGERTIP
|
||||||
|
} else if (progressSteps < getStageThresholdSteps(3)) {
|
||||||
|
SFPS_STAGE_LEFT_EDGE
|
||||||
|
} else {
|
||||||
|
SFPS_STAGE_RIGHT_EDGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
|
||||||
|
val helpStr: CharSequence = helpMessage.str
|
||||||
|
if (helpStr.isNotEmpty()) {
|
||||||
|
showError(helpStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||||
|
removeEnrollmentObservers()
|
||||||
|
if (enrollingViewModel.onBackPressed
|
||||||
|
&& errorMessage.msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
|
||||||
|
) {
|
||||||
|
enrollingViewModel.onCancelledDueToOnBackPressed()
|
||||||
|
} else if (enrollingViewModel.onSkipPressed
|
||||||
|
&& errorMessage.msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
|
||||||
|
) {
|
||||||
|
enrollingViewModel.onCancelledDueToOnSkipPressed()
|
||||||
|
} else {
|
||||||
|
val errMsgId: Int = errorMessage.msgId
|
||||||
|
enrollingViewModel.showErrorDialog(
|
||||||
|
FingerprintEnrollEnrollingViewModel.ErrorDialogData(
|
||||||
|
getString(FingerprintErrorDialog.getErrorMessage(errMsgId)),
|
||||||
|
getString(FingerprintErrorDialog.getErrorTitle(errMsgId)),
|
||||||
|
errMsgId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
progressViewModel.cancelEnrollment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun announceEnrollmentProgress(announcement: CharSequence) {
|
||||||
|
enrollingViewModel.sendAccessibilityEvent(announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
|
||||||
|
updateProgress(true /* animate */, progress)
|
||||||
|
if (enrollingViewModel.isAccessibilityEnabled) {
|
||||||
|
val percent: Int =
|
||||||
|
((progress.steps - progress.remaining).toFloat() / progress.steps.toFloat() * 100).toInt()
|
||||||
|
val announcement: CharSequence = getString(
|
||||||
|
R.string.security_settings_sfps_enroll_progress_a11y_message, percent
|
||||||
|
)
|
||||||
|
announceEnrollmentProgress(announcement)
|
||||||
|
illustrationLottie.contentDescription =
|
||||||
|
getString(R.string.security_settings_sfps_animation_a11y_label, percent)
|
||||||
|
}
|
||||||
|
updateTitleAndDescription()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
|
||||||
|
if (!progressViewModel.isEnrolling) {
|
||||||
|
Log.d(TAG, "Enrollment not started yet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val progress = getProgress(enrollmentProgress)
|
||||||
|
|
||||||
|
// Only clear the error when progress has been made.
|
||||||
|
// TODO (b/234772728) Add tests.
|
||||||
|
if (progressBar.progress < progress) {
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
if (animate) {
|
||||||
|
animateProgress(progress)
|
||||||
|
} else {
|
||||||
|
progressBar.progress = progress
|
||||||
|
if (progress >= PROGRESS_BAR_MAX) {
|
||||||
|
delayedFinishRunnable.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProgress(progress: EnrollmentProgress): Int {
|
||||||
|
if (progress.steps == -1) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val displayProgress = Math.max(0, progress.steps + 1 - progress.remaining)
|
||||||
|
return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(error: CharSequence) {
|
||||||
|
enrollingSfpsView!!.let {
|
||||||
|
it.headerText = error
|
||||||
|
it.headerTextView.contentDescription = error
|
||||||
|
GlifLayoutHelper(requireActivity(), it).setDescriptionText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResumed && !helpAnimation!!.isRunning) {
|
||||||
|
helpAnimation!!.start()
|
||||||
|
}
|
||||||
|
applySfpsErrorDynamicColors(true)
|
||||||
|
if (isResumed && enrollingViewModel.isAccessibilityEnabled) {
|
||||||
|
enrollingViewModel.vibrateError(javaClass.simpleName + "::showError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearError() {
|
||||||
|
applySfpsErrorDynamicColors(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateProgress(progress: Int) {
|
||||||
|
progressAnim?.cancel()
|
||||||
|
progressAnim = ObjectAnimator.ofInt(
|
||||||
|
progressBar,
|
||||||
|
"progress",
|
||||||
|
progressBar.progress,
|
||||||
|
progress
|
||||||
|
).also {
|
||||||
|
it.addListener(progressAnimationListener)
|
||||||
|
it.interpolator = fastOutSlowInInterpolator
|
||||||
|
it.setDuration(PROGRESS_ANIMATION_DURATION)
|
||||||
|
it.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies dynamic colors corresponding to showing or clearing errors on the progress bar
|
||||||
|
* and finger lottie for SFPS
|
||||||
|
*/
|
||||||
|
private fun applySfpsErrorDynamicColors(isError: Boolean) {
|
||||||
|
progressBar.applyProgressBarDynamicColor(requireContext(), isError)
|
||||||
|
illustrationLottie.applyLottieDynamicColor(requireContext(), isError)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStageThresholdSteps(index: Int): Int {
|
||||||
|
val progressLiveData: EnrollmentProgress? =
|
||||||
|
progressViewModel.progressLiveData.value
|
||||||
|
if (progressLiveData == null || progressLiveData.steps == -1) {
|
||||||
|
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return (progressLiveData.steps
|
||||||
|
* enrollingViewModel.getEnrollStageThreshold(index)).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTitleAndDescription() {
|
||||||
|
val helper = GlifLayoutHelper(requireActivity(), enrollingSfpsView!!)
|
||||||
|
if (enrollingViewModel.isAccessibilityEnabled) {
|
||||||
|
enrollingViewModel.clearTalkback()
|
||||||
|
helper.glifLayout.descriptionTextView.accessibilityLiveRegion =
|
||||||
|
View.ACCESSIBILITY_LIVE_REGION_POLITE
|
||||||
|
}
|
||||||
|
val stage = currentSfpsStage
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG, "updateTitleAndDescription, stage:" + stage
|
||||||
|
+ ", noAnimation:" + haveShownSfpsNoAnimationLottie
|
||||||
|
+ ", center:" + haveShownSfpsCenterLottie
|
||||||
|
+ ", tip:" + haveShownSfpsTipLottie
|
||||||
|
+ ", leftEdge:" + haveShownSfpsLeftEdgeLottie
|
||||||
|
+ ", rightEdge:" + haveShownSfpsRightEdgeLottie
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (stage) {
|
||||||
|
SFPS_STAGE_NO_ANIMATION -> {
|
||||||
|
helper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||||
|
if (!haveShownSfpsNoAnimationLottie) {
|
||||||
|
haveShownSfpsNoAnimationLottie = true
|
||||||
|
illustrationLottie.contentDescription =
|
||||||
|
getString(R.string.security_settings_sfps_animation_a11y_label, 0)
|
||||||
|
configureEnrollmentStage(
|
||||||
|
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||||
|
R.raw.sfps_lottie_no_animation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SFPS_STAGE_CENTER -> {
|
||||||
|
helper.setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title)
|
||||||
|
if (!haveShownSfpsCenterLottie) {
|
||||||
|
haveShownSfpsCenterLottie = true
|
||||||
|
configureEnrollmentStage(
|
||||||
|
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||||
|
R.raw.sfps_lottie_pad_center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SFPS_STAGE_FINGERTIP -> {
|
||||||
|
helper.setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title)
|
||||||
|
if (!haveShownSfpsTipLottie) {
|
||||||
|
haveShownSfpsTipLottie = true
|
||||||
|
configureEnrollmentStage("", R.raw.sfps_lottie_tip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SFPS_STAGE_LEFT_EDGE -> {
|
||||||
|
helper.setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title)
|
||||||
|
if (!haveShownSfpsLeftEdgeLottie) {
|
||||||
|
haveShownSfpsLeftEdgeLottie = true
|
||||||
|
configureEnrollmentStage("", R.raw.sfps_lottie_left_edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SFPS_STAGE_RIGHT_EDGE -> {
|
||||||
|
helper.setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title)
|
||||||
|
if (!haveShownSfpsRightEdgeLottie) {
|
||||||
|
haveShownSfpsRightEdgeLottie = true
|
||||||
|
configureEnrollmentStage("", R.raw.sfps_lottie_right_edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STAGE_UNKNOWN -> {
|
||||||
|
// Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
|
||||||
|
// which gets announced for a11y upon entering the page. For SFPS, we want to
|
||||||
|
// announce a different string for a11y upon entering the page.
|
||||||
|
helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||||
|
helper.setDescriptionText(
|
||||||
|
getString(R.string.security_settings_sfps_enroll_start_message)
|
||||||
|
)
|
||||||
|
val description: CharSequence = getString(
|
||||||
|
R.string.security_settings_sfps_enroll_find_sensor_message
|
||||||
|
)
|
||||||
|
helper.glifLayout.headerTextView.contentDescription = description
|
||||||
|
helper.activity.title = description
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||||
|
helper.setDescriptionText(
|
||||||
|
getString(R.string.security_settings_sfps_enroll_start_message)
|
||||||
|
)
|
||||||
|
val description: CharSequence = getString(
|
||||||
|
R.string.security_settings_sfps_enroll_find_sensor_message
|
||||||
|
)
|
||||||
|
helper.glifLayout.headerTextView.contentDescription = description
|
||||||
|
helper.activity.title = description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showIconTouchDialog() {
|
||||||
|
iconTouchCount = 0
|
||||||
|
enrollingViewModel.showIconTouchDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = FingerprintEnrollEnrollingSfpsFragment::class.java.simpleName
|
||||||
|
private const val DEBUG = false
|
||||||
|
private const val PROGRESS_BAR_MAX = 10000
|
||||||
|
private const val HELP_ANIMATION_DURATION = 550L
|
||||||
|
private const val HELP_ANIMATION_TRANSLATION_X = 40f
|
||||||
|
private const val PROGRESS_ANIMATION_DURATION = 250L
|
||||||
|
private const val ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN: Long = 500
|
||||||
|
private const val ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3
|
||||||
|
private const val STAGE_UNKNOWN = -1
|
||||||
|
private const val SFPS_STAGE_NO_ANIMATION = 0
|
||||||
|
private const val SFPS_STAGE_CENTER = 1
|
||||||
|
private const val SFPS_STAGE_FINGERTIP = 2
|
||||||
|
private const val SFPS_STAGE_LEFT_EDGE = 3
|
||||||
|
private const val SFPS_STAGE_RIGHT_EDGE = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FragmentActivity.bindFingerprintEnrollEnrollingSfpsView(
|
||||||
|
view: GlifLayout,
|
||||||
|
onSkipClickListener: View.OnClickListener
|
||||||
|
) {
|
||||||
|
GlifLayoutHelper(this, view).setDescriptionText(
|
||||||
|
getString(R.string.security_settings_fingerprint_enroll_start_message)
|
||||||
|
)
|
||||||
|
|
||||||
|
view.getMixin(FooterBarMixin::class.java).secondaryButton = FooterButton.Builder(this)
|
||||||
|
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||||
|
.setListener(onSkipClickListener)
|
||||||
|
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||||
|
.setTheme(R.style.SudGlifButton_Secondary)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!.progressBackgroundTintMode =
|
||||||
|
PorterDuff.Mode.SRC
|
||||||
|
|
||||||
|
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||||
|
.applyProgressBarDynamicColor(this, false)
|
||||||
|
|
||||||
|
view.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!
|
||||||
|
.applyLottieDynamicColor(this, false)
|
||||||
|
|
||||||
|
view.maybeHideSfpsText(resources.configuration.orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ProgressBar.applyProgressBarDynamicColor(context: Context, isError: Boolean) {
|
||||||
|
progressTintList = ColorStateList.valueOf(
|
||||||
|
context.getColor(
|
||||||
|
if (isError)
|
||||||
|
R.color.sfps_enrollment_progress_bar_error_color
|
||||||
|
else
|
||||||
|
R.color.sfps_enrollment_progress_bar_fill_color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
progressTintMode = PorterDuff.Mode.SRC
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LottieAnimationView.applyLottieDynamicColor(context: Context, isError: Boolean) {
|
||||||
|
addValueCallback<ColorFilter>(
|
||||||
|
KeyPath(".blue100", "**"),
|
||||||
|
LottieProperty.COLOR_FILTER
|
||||||
|
) {
|
||||||
|
PorterDuffColorFilter(
|
||||||
|
context.getColor(
|
||||||
|
if (isError)
|
||||||
|
R.color.sfps_enrollment_fp_error_color
|
||||||
|
else
|
||||||
|
R.color.sfps_enrollment_fp_captured_color
|
||||||
|
),
|
||||||
|
PorterDuff.Mode.SRC_ATOP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GlifLayout.maybeHideSfpsText(@Configuration.Orientation orientation: Int) {
|
||||||
|
val headerMixin: HeaderMixin = getMixin(HeaderMixin::class.java)
|
||||||
|
val descriptionMixin: DescriptionMixin = getMixin(DescriptionMixin::class.java)
|
||||||
|
|
||||||
|
val isLandscape = (orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||||
|
headerMixin.setAutoTextSizeEnabled(isLandscape)
|
||||||
|
if (isLandscape) {
|
||||||
|
headerMixin.textView.minLines = 0
|
||||||
|
headerMixin.textView.maxLines = 10
|
||||||
|
descriptionMixin.textView.minLines = 0
|
||||||
|
descriptionMixin.textView.maxLines = 10
|
||||||
|
} else {
|
||||||
|
headerMixin.textView.setLines(4)
|
||||||
|
// hide the description
|
||||||
|
descriptionMixin.textView.setLines(0)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user