[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:
Milton Wu
2023-06-19 17:17:03 +08:00
parent bf296dc1bd
commit 00bafe9f64
2 changed files with 476 additions and 464 deletions

View File

@@ -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());
}
};
}

View File

@@ -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()
}