[BiometricsV2] Refactor EnrollingRfpsFragment
Refactor FingerprintEnrollEnrollingRfpsFragment to kotlin and add bindView() method for it. Bug: 286198028 Test: atest FingerprintEnrollmentActivityTest Test: Manually test enrollment as Rear fingerpint device Change-Id: Ie271386f305003a89d7e545c289e47bbafb14a90
This commit is contained in:
@@ -1,464 +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.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Animatable2;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.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.AnimationUtils;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
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.google.android.setupcompat.template.FooterBarMixin;
|
||||
import com.google.android.setupcompat.template.FooterButton;
|
||||
import com.google.android.setupdesign.GlifLayout;
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for rfps
|
||||
*/
|
||||
public class FingerprintEnrollEnrollingRfpsFragment extends Fragment {
|
||||
|
||||
private static final String TAG = FingerprintEnrollEnrollingRfpsFragment.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;
|
||||
|
||||
/**
|
||||
* If we don't see progress during this time, we show an error message to remind the users that
|
||||
* they need to lift the finger and touch again.
|
||||
*/
|
||||
private static final int HINT_TIMEOUT_DURATION = 2500;
|
||||
|
||||
private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
|
||||
private FingerprintEnrollProgressViewModel mProgressViewModel;
|
||||
|
||||
private Interpolator mFastOutSlowInInterpolator;
|
||||
private Interpolator mLinearOutSlowInInterpolator;
|
||||
private Interpolator mFastOutLinearInInterpolator;
|
||||
private boolean mAnimationCancelled;
|
||||
|
||||
private GlifLayout mView;
|
||||
private ProgressBar mProgressBar;
|
||||
private ObjectAnimator mProgressAnim;
|
||||
private TextView mErrorText;
|
||||
private AnimatedVectorDrawable mIconAnimationDrawable;
|
||||
private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
setEnabled(false);
|
||||
mEnrollingViewModel.setOnBackPressed();
|
||||
cancelEnrollment();
|
||||
}
|
||||
};
|
||||
|
||||
@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);
|
||||
activity.getOnBackPressedDispatcher().addCallback(mOnBackPressedCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
mOnBackPressedCallback.setEnabled(false);
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
mView = initRfpsLayout(inflater, container);
|
||||
return mView;
|
||||
}
|
||||
|
||||
private GlifLayout initRfpsLayout(LayoutInflater inflater, ViewGroup container) {
|
||||
final GlifLayout containView = (GlifLayout) inflater.inflate(
|
||||
R.layout.fingerprint_enroll_enrolling, container, false);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, containView);
|
||||
glifLayoutHelper.setDescriptionText(getString(
|
||||
R.string.security_settings_fingerprint_enroll_start_message));
|
||||
glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
|
||||
|
||||
mErrorText = containView.findViewById(R.id.error_text);
|
||||
mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
|
||||
containView.getMixin(FooterBarMixin.class).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()
|
||||
);
|
||||
|
||||
final LayerDrawable fingerprintDrawable = (LayerDrawable) mProgressBar.getBackground();
|
||||
mIconAnimationDrawable = (AnimatedVectorDrawable)
|
||||
fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
|
||||
mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
|
||||
fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
|
||||
mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
mAnimationCancelled = false;
|
||||
startIconAnimation();
|
||||
startEnrollment();
|
||||
updateProgress(false /* animate */, mProgressViewModel.getProgressLiveData().getValue());
|
||||
updateTitleAndDescription();
|
||||
}
|
||||
|
||||
private void startIconAnimation() {
|
||||
if (mIconAnimationDrawable != null) {
|
||||
mIconAnimationDrawable.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopIconAnimation() {
|
||||
mAnimationCancelled = true;
|
||||
if (mIconAnimationDrawable != null) {
|
||||
mIconAnimationDrawable.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
stopIconAnimation();
|
||||
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 onEnrollmentHelp(@NonNull EnrollmentStatusMessage helpMessage) {
|
||||
final CharSequence helpStr = helpMessage.getStr();
|
||||
if (!TextUtils.isEmpty(helpStr)) {
|
||||
mErrorText.removeCallbacks(mTouchAgainRunnable);
|
||||
showError(helpStr);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnrollmentError(@NonNull EnrollmentStatusMessage errorMessage) {
|
||||
stopIconAnimation();
|
||||
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(
|
||||
mView.getContext().getString(
|
||||
FingerprintErrorDialog.getErrorMessage(errMsgId)),
|
||||
mView.getContext().getString(
|
||||
FingerprintErrorDialog.getErrorTitle(errMsgId)),
|
||||
errMsgId
|
||||
));
|
||||
mProgressViewModel.cancelEnrollment();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnrollmentProgressChange(@NonNull EnrollmentProgress progress) {
|
||||
updateProgress(true /* animate */, progress);
|
||||
updateTitleAndDescription();
|
||||
animateFlash();
|
||||
mErrorText.removeCallbacks(mTouchAgainRunnable);
|
||||
mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION);
|
||||
}
|
||||
|
||||
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) {
|
||||
mErrorText.setText(error);
|
||||
if (mErrorText.getVisibility() == View.INVISIBLE) {
|
||||
mErrorText.setVisibility(View.VISIBLE);
|
||||
mErrorText.setTranslationY(mView.getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.fingerprint_error_text_appear_distance));
|
||||
mErrorText.setAlpha(0f);
|
||||
mErrorText.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(mLinearOutSlowInInterpolator)
|
||||
.start();
|
||||
} else {
|
||||
mErrorText.animate().cancel();
|
||||
mErrorText.setAlpha(1f);
|
||||
mErrorText.setTranslationY(0f);
|
||||
}
|
||||
if (isResumed() && mEnrollingViewModel.isAccessibilityEnabled()) {
|
||||
mEnrollingViewModel.vibrateError(getClass().getSimpleName() + "::showError");
|
||||
}
|
||||
}
|
||||
|
||||
private void clearError() {
|
||||
if (mErrorText.getVisibility() == View.VISIBLE) {
|
||||
mErrorText.animate()
|
||||
.alpha(0f)
|
||||
.translationY(getResources().getDimensionPixelSize(
|
||||
R.dimen.fingerprint_error_text_disappear_distance))
|
||||
.setDuration(100)
|
||||
.setInterpolator(mFastOutLinearInInterpolator)
|
||||
.withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE))
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private final Runnable mTouchAgainRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Use mView to getString to prevent activity is missing during rotation
|
||||
showError(mView.getContext().getString(
|
||||
R.string.security_settings_fingerprint_enroll_lift_touch_again));
|
||||
}
|
||||
};
|
||||
|
||||
private void animateFlash() {
|
||||
if (mIconBackgroundBlinksDrawable != null) {
|
||||
mIconBackgroundBlinksDrawable.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitleAndDescription() {
|
||||
final EnrollmentProgress progressLiveData =
|
||||
mProgressViewModel.getProgressLiveData().getValue();
|
||||
new GlifLayoutHelper(getActivity(), mView).setDescriptionText(mView.getContext().getString(
|
||||
progressLiveData == null || progressLiveData.getSteps() == -1
|
||||
? R.string.security_settings_fingerprint_enroll_start_message
|
||||
: R.string.security_settings_fingerprint_enroll_repeat_message));
|
||||
}
|
||||
|
||||
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) {
|
||||
startIconAnimation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) { }
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
stopIconAnimation();
|
||||
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 final Animatable2.AnimationCallback mIconAnimationCallback =
|
||||
new Animatable2.AnimationCallback() {
|
||||
@Override
|
||||
public void onAnimationEnd(Drawable d) {
|
||||
if (mAnimationCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start animation after it has ended.
|
||||
mProgressBar.post(() -> startIconAnimation());
|
||||
}
|
||||
};
|
||||
}
|
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.Animatable2
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_CANCELED
|
||||
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.AnimationUtils.loadInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
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.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for rfps
|
||||
*/
|
||||
class FingerprintEnrollEnrollingRfpsFragment : 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() = loadInterpolator(requireActivity(), android.R.interpolator.fast_out_slow_in)
|
||||
|
||||
private val linearOutSlowInInterpolator: Interpolator
|
||||
get() = loadInterpolator(requireActivity(), android.R.interpolator.linear_out_slow_in)
|
||||
|
||||
private val fastOutLinearInInterpolator: Interpolator
|
||||
get() = loadInterpolator(requireActivity(), android.R.interpolator.fast_out_linear_in)
|
||||
|
||||
private var isAnimationCancelled = false
|
||||
|
||||
private var enrollingRfpsView: GlifLayout? = null
|
||||
private val progressBar: ProgressBar
|
||||
get() = enrollingRfpsView!!.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||
|
||||
private var progressAnim: ObjectAnimator? = null
|
||||
|
||||
private val errorText: TextView
|
||||
get() = enrollingRfpsView!!.findViewById<TextView>(R.id.error_text)!!
|
||||
|
||||
private val iconAnimationDrawable: AnimatedVectorDrawable?
|
||||
get() = (progressBar.background as LayerDrawable)
|
||||
.findDrawableByLayerId(R.id.fingerprint_animation) as AnimatedVectorDrawable?
|
||||
|
||||
private val iconBackgroundBlinksDrawable: AnimatedVectorDrawable?
|
||||
get() = (progressBar.background as LayerDrawable)
|
||||
.findDrawableByLayerId(R.id.fingerprint_background) as AnimatedVectorDrawable?
|
||||
|
||||
private var iconTouchCount = 0
|
||||
|
||||
private val touchAgainRunnable =
|
||||
Runnable {
|
||||
showError(
|
||||
// Use enrollingRfpsView to getString to prevent activity is missing during rotation
|
||||
enrollingRfpsView!!.context.getString(
|
||||
R.string.security_settings_fingerprint_enroll_lift_touch_again
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
private val onBackPressedCallback: OnBackPressedCallback =
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
isEnabled = false
|
||||
enrollingViewModel.setOnBackPressed()
|
||||
cancelEnrollment()
|
||||
}
|
||||
}
|
||||
|
||||
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(onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
enrollingRfpsView = inflater.inflate(
|
||||
R.layout.fingerprint_enroll_enrolling, container, false
|
||||
) as GlifLayout
|
||||
return enrollingRfpsView!!
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
iconAnimationDrawable!!.registerAnimationCallback(iconAnimationCallback)
|
||||
|
||||
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(
|
||||
showDialogRunnable,
|
||||
ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN
|
||||
)
|
||||
}
|
||||
} else if (event.actionMasked == MotionEvent.ACTION_CANCEL
|
||||
|| event.actionMasked == MotionEvent.ACTION_UP
|
||||
) {
|
||||
progressBar.removeCallbacks(showDialogRunnable)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
requireActivity().bindFingerprintEnrollEnrollingRfpsView(
|
||||
view = enrollingRfpsView!!,
|
||||
onSkipClickListener = onSkipClickListener
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isAnimationCancelled = false
|
||||
startIconAnimation()
|
||||
startEnrollment()
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
|
||||
private fun startIconAnimation() {
|
||||
iconAnimationDrawable?.start()
|
||||
}
|
||||
|
||||
private fun stopIconAnimation() {
|
||||
isAnimationCancelled = true
|
||||
iconAnimationDrawable?.stop()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
stopIconAnimation()
|
||||
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 onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
|
||||
val helpStr: CharSequence = helpMessage.str
|
||||
if (!TextUtils.isEmpty(helpStr)) {
|
||||
errorText.removeCallbacks(touchAgainRunnable)
|
||||
showError(helpStr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
stopIconAnimation()
|
||||
removeEnrollmentObservers()
|
||||
if (enrollingViewModel.onBackPressed
|
||||
&& errorMessage.msgId == FINGERPRINT_ERROR_CANCELED
|
||||
) {
|
||||
enrollingViewModel.onCancelledDueToOnBackPressed()
|
||||
} else if (enrollingViewModel.onSkipPressed
|
||||
&& errorMessage.msgId == FINGERPRINT_ERROR_CANCELED
|
||||
) {
|
||||
enrollingViewModel.onCancelledDueToOnSkipPressed()
|
||||
} else {
|
||||
val errMsgId: Int = errorMessage.msgId
|
||||
enrollingViewModel.showErrorDialog(
|
||||
FingerprintEnrollEnrollingViewModel.ErrorDialogData(
|
||||
enrollingRfpsView!!.context.getString(
|
||||
FingerprintErrorDialog.getErrorMessage(errMsgId)
|
||||
),
|
||||
enrollingRfpsView!!.context.getString(
|
||||
FingerprintErrorDialog.getErrorTitle(errMsgId)
|
||||
),
|
||||
errMsgId
|
||||
)
|
||||
)
|
||||
progressViewModel.cancelEnrollment()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
|
||||
updateProgress(true /* animate */, progress)
|
||||
updateTitleAndDescription()
|
||||
animateFlash()
|
||||
errorText.removeCallbacks(touchAgainRunnable)
|
||||
errorText.postDelayed(touchAgainRunnable, HINT_TIMEOUT_DURATION.toLong())
|
||||
}
|
||||
|
||||
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 = 0.coerceAtLeast(progress.steps + 1 - progress.remaining)
|
||||
return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
|
||||
}
|
||||
|
||||
private fun showError(error: CharSequence) {
|
||||
errorText.text = error
|
||||
if (errorText.visibility == View.INVISIBLE) {
|
||||
errorText.visibility = View.VISIBLE
|
||||
errorText.translationY = enrollingRfpsView!!.context.resources.getDimensionPixelSize(
|
||||
R.dimen.fingerprint_error_text_appear_distance
|
||||
).toFloat()
|
||||
errorText.alpha = 0f
|
||||
errorText.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(linearOutSlowInInterpolator)
|
||||
.start()
|
||||
} else {
|
||||
errorText.animate().cancel()
|
||||
errorText.alpha = 1f
|
||||
errorText.translationY = 0f
|
||||
}
|
||||
if (isResumed && enrollingViewModel.isAccessibilityEnabled) {
|
||||
enrollingViewModel.vibrateError(javaClass.simpleName + "::showError")
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearError() {
|
||||
if (errorText.visibility == View.VISIBLE) {
|
||||
errorText.animate()
|
||||
.alpha(0f)
|
||||
.translationY(
|
||||
resources.getDimensionPixelSize(
|
||||
R.dimen.fingerprint_error_text_disappear_distance
|
||||
).toFloat()
|
||||
)
|
||||
.setDuration(100)
|
||||
.setInterpolator(fastOutLinearInInterpolator)
|
||||
.withEndAction { errorText!!.visibility = View.INVISIBLE }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateProgress(progress: Int) {
|
||||
progressAnim?.cancel()
|
||||
val anim = ObjectAnimator.ofInt(
|
||||
progressBar /* target */,
|
||||
"progress" /* propertyName */,
|
||||
progressBar.progress /* values[0] */,
|
||||
progress /* values[1] */
|
||||
)
|
||||
anim.addListener(progressAnimationListener)
|
||||
anim.interpolator = fastOutSlowInInterpolator
|
||||
anim.setDuration(ANIMATION_DURATION)
|
||||
anim.start()
|
||||
progressAnim = anim
|
||||
}
|
||||
|
||||
private fun animateFlash() {
|
||||
iconBackgroundBlinksDrawable?.start()
|
||||
}
|
||||
|
||||
private fun updateTitleAndDescription() {
|
||||
val progressLiveData: EnrollmentProgress = progressViewModel.progressLiveData.value!!
|
||||
GlifLayoutHelper(activity!!, enrollingRfpsView!!).setDescriptionText(
|
||||
enrollingRfpsView!!.context.getString(
|
||||
if (progressLiveData.steps == -1)
|
||||
R.string.security_settings_fingerprint_enroll_start_message
|
||||
else
|
||||
R.string.security_settings_fingerprint_enroll_repeat_message
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showIconTouchDialog() {
|
||||
iconTouchCount = 0
|
||||
enrollingViewModel.showIconTouchDialog()
|
||||
}
|
||||
|
||||
private val showDialogRunnable = Runnable { showIconTouchDialog() }
|
||||
|
||||
private val progressAnimationListener: Animator.AnimatorListener =
|
||||
object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
startIconAnimation()
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator) {}
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
stopIconAnimation()
|
||||
if (progressBar.progress >= PROGRESS_BAR_MAX) {
|
||||
progressBar.postDelayed(delayedFinishRunnable, ANIMATION_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {}
|
||||
}
|
||||
|
||||
// Give the user a chance to see progress completed before jumping to the next stage.
|
||||
private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
|
||||
|
||||
private val iconAnimationCallback: Animatable2.AnimationCallback =
|
||||
object : Animatable2.AnimationCallback() {
|
||||
override fun onAnimationEnd(d: Drawable) {
|
||||
if (isAnimationCancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
// Start animation after it has ended.
|
||||
progressBar.post { startIconAnimation() }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "FingerprintEnrollEnrollingRfpsFragment"
|
||||
private const val PROGRESS_BAR_MAX = 10000
|
||||
private const val 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
|
||||
|
||||
/**
|
||||
* If we don't see progress during this time, we show an error message to remind the users that
|
||||
* they need to lift the finger and touch again.
|
||||
*/
|
||||
private const val HINT_TIMEOUT_DURATION = 2500
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollEnrollingRfpsView(
|
||||
view: GlifLayout,
|
||||
onSkipClickListener: View.OnClickListener
|
||||
) {
|
||||
GlifLayoutHelper(this, view).let {
|
||||
it.setDescriptionText(
|
||||
getString(
|
||||
R.string.security_settings_fingerprint_enroll_start_message
|
||||
)
|
||||
)
|
||||
it.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||
}
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||
.progressBackgroundTintMode = PorterDuff.Mode.SRC
|
||||
|
||||
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()
|
||||
}
|
Reference in New Issue
Block a user