diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java deleted file mode 100644 index 8a4ed633222..00000000000 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java +++ /dev/null @@ -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 mProgressObserver = progress -> { - if (DEBUG) { - Log.d(TAG, "mProgressObserver(" + progress + ")"); - } - if (progress != null && progress.getSteps() >= 0) { - onEnrollmentProgressChange(progress); - } - }; - - private final Observer mHelpMessageObserver = helpMessage -> { - if (DEBUG) { - Log.d(TAG, "mHelpMessageObserver(" + helpMessage + ")"); - } - if (helpMessage != null) { - onEnrollmentHelp(helpMessage); - } - }; - - private final Observer 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()); - } - }; -} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.kt b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.kt new file mode 100644 index 00000000000..66a9c001705 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.kt @@ -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(R.id.fingerprint_progress_bar)!! + + private var progressAnim: ObjectAnimator? = null + + private val errorText: TextView + get() = enrollingRfpsView!!.findViewById(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 = + Observer { progress: EnrollmentProgress? -> + if (DEBUG) { + Log.d(TAG, "progressObserver($progress)") + } + if (progress != null && progress.steps >= 0) { + onEnrollmentProgressChange(progress) + } + } + + private val helpMessageObserver: Observer = + Observer { helpMessage: EnrollmentStatusMessage? -> + if (DEBUG) { + Log.d(TAG, "helpMessageObserver($helpMessage)") + } + helpMessage?.let { onEnrollmentHelp(it) } + } + + private val errorMessageObserver: Observer = + Observer { 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(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() +}