Attach FingerprintEnrollEnrollingRfps fragment

Support enrolling RFPS on biomerics v2.

Bug: 260957939
Test: atest FingerprintEnrollFindSensorViewModelTest
      FingerprintEnrollProgressViewModelTest
      FingerprintEnrollmentViewModelTest
      FingerprintEnrollmentActivityTest
Change-Id: Ic04b934592415d03f1b119383bffd40bd5eef2bd
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java b/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java
index 870e1bb..8f604e2 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java
@@ -123,7 +123,10 @@
         dialog.show(fragmentManager, FingerprintErrorDialog.class.getName());
     }
 
-    private static int getErrorMessage(int errMsgId) {
+    /**
+     * Gets dialog message as error id inside {@link FingerprintManager}
+     */
+    public static int getErrorMessage(int errMsgId) {
         switch (errMsgId) {
             case FingerprintManager.FINGERPRINT_ERROR_TIMEOUT:
                 // This message happens when the underlying crypto layer decides to revoke
@@ -137,7 +140,10 @@
         }
     }
 
-    private static int getErrorTitle(int errMsgId) {
+    /**
+     * Gets dialog title as error id inside {@link FingerprintManager}
+     */
+    public static int getErrorTitle(int errMsgId) {
         switch (errMsgId) {
             case FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
                 return R.string
diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
index 8f432e6..6a9892a 100644
--- a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
+++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
@@ -98,8 +98,11 @@
         return resources.getInteger(R.integer.suw_max_fingerprints_enrollable);
     }
 
+    /**
+     * Gets the first FingerprintSensorPropertiesInternal from FingerprintManager
+     */
     @Nullable
-    private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
+    public FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
         final List<FingerprintSensorPropertiesInternal> props = mSensorPropertiesCache;
         if (props == null) {
             // Handle this case if it really happens
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
index 7bf9d53..19971a7 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
@@ -113,13 +113,14 @@
                         new FingerprintUpdater(application), userId);
             }
         } else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) {
+            final Integer userId = extras.get(USER_ID_KEY);
             final FingerprintRepository fingerprint = provider.getFingerprintRepository(
                     application);
             final AccessibilityRepository accessibility = provider.getAccessibilityRepository(
                     application);
             final VibratorRepository vibrator = provider.getVibratorRepository(application);
             if (fingerprint != null && accessibility != null && vibrator != null) {
-                return (T) new FingerprintEnrollEnrollingViewModel(application, fingerprint,
+                return (T) new FingerprintEnrollEnrollingViewModel(application, userId, fingerprint,
                         accessibility, vibrator);
             }
         }
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingErrorDialog.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingErrorDialog.java
new file mode 100644
index 0000000..ad34e74
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingErrorDialog.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2021 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 com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.ErrorDialogData;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ERROR_DIALOG_ACTION_RESTART;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * Fingerprint error dialog, will be shown when an error occurs during fingerprint enrollment.
+ */
+public class FingerprintEnrollEnrollingErrorDialog extends InstrumentedDialogFragment {
+
+    private FingerprintEnrollEnrollingViewModel mViewModel;
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        final ErrorDialogData data = mViewModel.getErrorDialogLiveData().getValue();
+        final int errMsgId = data.getErrMsgId();
+        final boolean canAssumeUdfps = mViewModel.canAssumeUdfps();
+        final boolean wasTimeout = errMsgId == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT;
+
+        builder.setTitle(data.getErrTitle())
+                .setMessage(data.getErrMsg())
+                .setCancelable(false);
+        if (wasTimeout && canAssumeUdfps) {
+            builder.setPositiveButton(
+                    R.string.security_settings_fingerprint_enroll_dialog_try_again,
+                    (dialog, which) -> {
+                            dialog.dismiss();
+                            mViewModel.onErrorDialogAction(FINGERPRINT_ERROR_DIALOG_ACTION_RESTART);
+                    });
+            builder.setNegativeButton(
+                    R.string.security_settings_fingerprint_enroll_dialog_ok,
+                    (dialog, which) -> {
+                            dialog.dismiss();
+                            mViewModel.onErrorDialogAction(
+                                    FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT);
+                    });
+        } else {
+            builder.setPositiveButton(
+                    R.string.security_settings_fingerprint_enroll_dialog_ok,
+                    (dialog, which) -> {
+                        dialog.dismiss();
+                        mViewModel.onErrorDialogAction(wasTimeout
+                                ? FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT
+                                : FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH);
+                    });
+        }
+        final AlertDialog dialog = builder.create();
+        dialog.setCanceledOnTouchOutside(false);
+        return dialog;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_FINGERPINT_ERROR;
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        mViewModel = new ViewModelProvider(getActivity()).get(
+                FingerprintEnrollEnrollingViewModel.class);
+        super.onAttach(context);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
index 30b66a2..30823ea 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
@@ -16,15 +16,19 @@
 
 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.content.res.Configuration;
 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.os.Bundle;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -35,18 +39,22 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
+import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModelProvider;
+import androidx.transition.Transition;
+import androidx.transition.TransitionSet;
 
 import com.android.settings.R;
-import com.android.settings.biometrics.BiometricUtils;
+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.DeviceRotationViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
-import com.android.settingslib.display.DisplayDensityUtils;
 
 import com.airbnb.lottie.LottieAnimationView;
 import com.google.android.setupcompat.template.FooterBarMixin;
@@ -59,9 +67,16 @@
 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 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;
@@ -73,8 +88,9 @@
     private Interpolator mFastOutLinearInInterpolator;
     private boolean mAnimationCancelled;
 
-    private View mView;
+    private GlifLayout mView;
     private ProgressBar mProgressBar;
+    private ObjectAnimator mProgressAnim;
     private TextView mErrorText;
     private FooterBarMixin mFooterBarMixin;
     private AnimatedVectorDrawable mIconAnimationDrawable;
@@ -90,8 +106,45 @@
     private boolean mHaveShownSfpsLeftEdgeLottie;
     private boolean mHaveShownSfpsRightEdgeLottie;
 
-    private final View.OnClickListener mOnSkipClickListener =
-            (v) -> mEnrollingViewModel.onSkipButtonClick();
+    private final View.OnClickListener mOnSkipClickListener = v -> {
+        mProgressViewModel.cancelEnrollment();
+        mEnrollingViewModel.onSkipButtonClick();
+    };
+
+    private final Observer<EnrollmentProgress> mProgressObserver = progress -> {
+        if (DEBUG) {
+            Log.d(TAG, "mProgressObserver(" + progress + ")");
+        }
+        if (progress != null) {
+            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 Observer<Boolean> mAcquireObserver = isAcquiredGood -> {
+        // TODO
+    };
+    private final Observer<Integer> mPointerDownObserver = sensorId -> {
+        // TODO
+    };
+    private final Observer<Integer> mPointerUpObserver = sensorId -> {
+        // TODO
+    };
 
     private int mIconTouchCount;
 
@@ -103,39 +156,74 @@
         mRotationViewModel = provider.get(DeviceRotationViewModel.class);
         mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
         super.onAttach(context);
+        final TransitionSet transitionSet = (TransitionSet) getSharedElementEnterTransition();
+        if (transitionSet != null) {
+            transitionSet.addListener(new Transition.TransitionListener() {
+                @Override
+                public void onTransitionStart(@NonNull Transition transition) {
+                }
+
+                @Override
+                public void onTransitionEnd(@NonNull Transition transition) {
+                    transition.removeListener(this);
+                    mAnimationCancelled = false;
+                    startIconAnimation();
+                }
+
+                @Override
+                public void onTransitionCancel(@NonNull Transition transition) {
+                }
+
+                @Override
+                public void onTransitionPause(@NonNull Transition transition) {
+                }
+
+                @Override
+                public void onTransitionResume(@NonNull Transition transition) {
+                }
+            });
+        }
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mEnrollingViewModel.restoreSavedState(savedInstanceState);
         mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
     }
 
     @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        mEnrollingViewModel.onSaveInstanceState(outState);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         mView = initRfpsLayout(inflater, container);
         return mView;
     }
 
-    private View initRfpsLayout(LayoutInflater inflater, ViewGroup container) {
-        final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false);
+    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,
-                (GlifLayout) containView);
-        glifLayoutHelper.setDescriptionText(
-                R.string.security_settings_fingerprint_enroll_start_message);
+        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);
 
-        mShouldShowLottie = shouldShowLottie();
-        boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
-                || BiometricUtils.isLandscape(activity);
-        updateOrientation((isLandscape
-                ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+//        mShouldShowLottie = shouldShowLottie(); // TODO move this call into updateOrientation()?
+//        boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+//                || BiometricUtils.isLandscape(activity);
+//        updateOrientation(containView, (isLandscape
+//                ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
 
         mErrorText = containView.findViewById(R.id.error_text);
         mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
-        mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin = containView.getMixin(FooterBarMixin.class);
         mFooterBarMixin.setSecondaryButton(
                 new FooterButton.Builder(activity)
                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
@@ -145,15 +233,12 @@
                         .build()
         );
 
-        final LayerDrawable fingerprintDrawable = mProgressBar != null
-                ? (LayerDrawable) mProgressBar.getBackground() : null;
-        if (fingerprintDrawable != null) {
-            mIconAnimationDrawable = (AnimatedVectorDrawable)
-                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
-            mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
-                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
-            mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
-        }
+        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);
@@ -162,58 +247,34 @@
         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
                 activity, android.R.interpolator.fast_out_linear_in);
 
-        if (mProgressBar != null) {
-            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);
+        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);
                 }
-                return true;
-            });
-        }
+            } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                    || event.getActionMasked() == MotionEvent.ACTION_UP) {
+                mProgressBar.removeCallbacks(mShowDialogRunnable);
+            }
+            return true;
+        });
 
         return containView;
     }
 
-    private void updateOrientation(int orientation) {
-        switch (orientation) {
-            case Configuration.ORIENTATION_LANDSCAPE: {
-                mIllustrationLottie = null;
-                break;
-            }
-            case Configuration.ORIENTATION_PORTRAIT: {
-                if (mShouldShowLottie) {
-                    mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
-                }
-                break;
-            }
-            default:
-                Log.e(TAG, "Error unhandled configuration change");
-                break;
-        }
-    }
-
-    private void updateTitleAndDescription() {
-        final Activity activity = getActivity();
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
-                (GlifLayout) mView);
-
-        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
-        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
-            glifLayoutHelper.setDescriptionText(
-                    R.string.security_settings_fingerprint_enroll_start_message);
-        } else {
-            glifLayoutHelper.setDescriptionText(
-                    R.string.security_settings_fingerprint_enroll_repeat_message);
+    @Override
+    public void onStart() {
+        super.onStart();
+        startEnrollment();
+        updateProgress(false /* animate */, mProgressViewModel.getProgressLiveData().getValue());
+        updateTitleAndDescription();
+        if (true /* TODO check mRestoring */) {
+            startIconAnimation();
         }
     }
 
@@ -230,25 +291,243 @@
         }
     }
 
+    private void onCancelEnrollment(@IdRes int errorMsgId) {
+        // showErrorDialog() will cause onWindowFocusChanged(false), set mIsCanceled to false
+        // before showErrorDialog() to prevent that another error dialog is triggered again.
+// TODO       mIsCanceled = true;
+// TODO       mIsOrientationChanged = false;
+        mEnrollingViewModel.showErrorDialog(new FingerprintEnrollEnrollingViewModel.ErrorDialogData(
+                mView.getContext().getString(FingerprintErrorDialog.getErrorMessage(errorMsgId)),
+                mView.getContext().getString(FingerprintErrorDialog.getErrorTitle(errorMsgId)),
+                errorMsgId
+        ));
+        cancelEnrollment();
+        stopIconAnimation();
+    }
+
+    @Override
+    public void onStop() {
+        stopIconAnimation();
+        removeEnrollmentObserver();
+        if (!getActivity().isChangingConfigurations()) {
+            mProgressViewModel.cancelEnrollment();
+        }
+        super.onStop();
+    }
+
+    private void removeEnrollmentObserver() {
+        mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
+        mProgressViewModel.getHelpMessageLiveData().removeObserver(mHelpMessageObserver);
+        mProgressViewModel.getErrorMessageLiveData().removeObserver(mErrorMessageObserver);
+        mProgressViewModel.getAcquireLiveData().removeObserver(mAcquireObserver);
+        mProgressViewModel.getPointerDownLiveData().removeObserver(mPointerDownObserver);
+        mProgressViewModel.getPointerUpLiveData().removeObserver(mPointerUpObserver);
+    }
+
+    private void cancelEnrollment() {
+        removeEnrollmentObserver();
+        mProgressViewModel.cancelEnrollment();
+    }
+
+    private void startEnrollment() {
+        mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
+        mProgressViewModel.getHelpMessageLiveData().observe(this, mHelpMessageObserver);
+        mProgressViewModel.getErrorMessageLiveData().observe(this, mErrorMessageObserver);
+        mProgressViewModel.getAcquireLiveData().observe(this, mAcquireObserver);
+        mProgressViewModel.getPointerDownLiveData().observe(this, mPointerDownObserver);
+        mProgressViewModel.getPointerUpLiveData().observe(this, mPointerUpObserver);
+        mProgressViewModel.startEnrollment(ENROLL_ENROLL);
+    }
+
+    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) {
+        onCancelEnrollment(errorMessage.getMsgId());
+    }
+
+    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();
+        }
+    }
+
+
+    @Override
+    public void onDestroy() {
+        // TODO stopListenOrientationEvent();
+        super.onDestroy();
+    }
+
+    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(250);
+        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 updateOrientation(@NonNull GlifLayout glifLayout, int orientation) {
+//        switch (orientation) {
+//            case Configuration.ORIENTATION_LANDSCAPE: {
+//                mIllustrationLottie = null;
+//                break;
+//            }
+//            case Configuration.ORIENTATION_PORTRAIT: {
+//                if (shouldShowLottie()) {
+//                    mIllustrationLottie = glifLayout.findViewById(R.id.illustration_lottie);
+//                }
+//                break;
+//            }
+//            default:
+//                Log.e(TAG, "Error unhandled configuration change");
+//                break;
+//        }
+//    }
+
+    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;
         //TODO EnrollingActivity should observe live data and add dialog fragment
         mEnrollingViewModel.onIconTouchDialogShow();
     }
 
-    private boolean shouldShowLottie() {
-        DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
-        int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
-        final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
-                [currentDensityIndex];
-        final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
-        return defaultDensity == currentDensity;
-    }
+    private final Runnable mShowDialogRunnable = () -> showIconTouchDialog();
 
-    private final Runnable mShowDialogRunnable = new Runnable() {
+    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, 250L);
+                    }
+                }
+
+                @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 = new Runnable() {
         @Override
         public void run() {
-            showIconTouchDialog();
+            mEnrollingViewModel.onSkipButtonClick();
+            /* TODO launchFinish(); */
         }
     };
 
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
index ddeb465..57b8665 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
@@ -16,15 +16,20 @@
 
 package com.android.settings.biometrics2.ui.view;
 
+import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
 import android.annotation.RawRes;
 import android.app.Activity;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Animatable2;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -39,11 +44,14 @@
 import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModelProvider;
+import androidx.transition.Transition;
+import androidx.transition.TransitionSet;
 
 import com.android.settings.R;
-import com.android.settings.biometrics.BiometricUtils;
 import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
@@ -51,6 +59,8 @@
 
 import com.airbnb.lottie.LottieAnimationView;
 import com.airbnb.lottie.LottieCompositionFactory;
+import com.airbnb.lottie.LottieProperty;
+import com.airbnb.lottie.model.KeyPath;
 import com.google.android.setupcompat.template.FooterBarMixin;
 import com.google.android.setupcompat.template.FooterButton;
 import com.google.android.setupdesign.GlifLayout;
@@ -64,9 +74,9 @@
 
     private static final String TAG = FingerprintEnrollEnrollingSfpsFragment.class.getSimpleName();
 
+    private static final int PROGRESS_BAR_MAX = 10000;
     private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
     private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
-    private static final int HINT_TIMEOUT_DURATION = 2500;
 
     private static final int STAGE_UNKNOWN = -1;
     private static final int SFPS_STAGE_NO_ANIMATION = 0;
@@ -84,8 +94,9 @@
     private Interpolator mFastOutLinearInInterpolator;
     private boolean mAnimationCancelled;
 
-    private View mView;
+    private GlifLayout mView;
     private ProgressBar mProgressBar;
+    private ObjectAnimator mProgressAnim;
     private TextView mErrorText;
     private FooterBarMixin mFooterBarMixin;
     private AnimatedVectorDrawable mIconAnimationDrawable;
@@ -103,6 +114,24 @@
 
     private final View.OnClickListener mOnSkipClickListener =
             (v) -> mEnrollingViewModel.onSkipButtonClick();
+    private final Observer<EnrollmentProgress> mProgressObserver = progress -> {
+        // TODO
+    };
+    private final Observer<EnrollmentStatusMessage> mHelpMessageObserver = helpMessage -> {
+        // TODO
+    };
+    private final Observer<EnrollmentStatusMessage> mErrorMessageObserver = errorMessage -> {
+        // TODO
+    };
+    private final Observer<Boolean> mAcquireObserver = isAcquiredGood -> {
+        // TODO
+    };
+    private final Observer<Integer> mPointerDownObserver = sensorId -> {
+        // TODO
+    };
+    private final Observer<Integer> mPointerUpObserver = sensorId -> {
+        // TODO
+    };
 
     private int mIconTouchCount;
 
@@ -114,41 +143,73 @@
         mRotationViewModel = provider.get(DeviceRotationViewModel.class);
         mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
         super.onAttach(context);
+        final TransitionSet transitionSet = (TransitionSet) getSharedElementEnterTransition();
+        if (transitionSet != null) {
+            transitionSet.addListener(new Transition.TransitionListener() {
+                @Override
+                public void onTransitionStart(@NonNull Transition transition) {
+
+                }
+
+                @Override
+                public void onTransitionEnd(@NonNull Transition transition) {
+                    transition.removeListener(this);
+                    mAnimationCancelled = false;
+                    startIconAnimation();
+                }
+
+                @Override
+                public void onTransitionCancel(@NonNull Transition transition) {
+
+                }
+
+                @Override
+                public void onTransitionPause(@NonNull Transition transition) {
+
+                }
+
+                @Override
+                public void onTransitionResume(@NonNull Transition transition) {
+
+                }
+            });
+        }
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mEnrollingViewModel.restoreSavedState(savedInstanceState);
         mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
     }
 
     @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        mEnrollingViewModel.onSaveInstanceState(outState);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         mView = initSfpsLayout(inflater, container);
-        final Configuration config = getActivity().getResources().getConfiguration();
-        maybeHideSfpsText(config);
         return mView;
     }
 
-    private View initSfpsLayout(LayoutInflater inflater, ViewGroup container) {
-        final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false);
+    private GlifLayout initSfpsLayout(LayoutInflater inflater, ViewGroup container) {
+        final GlifLayout containView = (GlifLayout) inflater.inflate(R.layout.sfps_enroll_enrolling,
+                container, false);
         final Activity activity = getActivity();
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
-                (GlifLayout) containView);
-        glifLayoutHelper.setDescriptionText(
-                R.string.security_settings_fingerprint_enroll_start_message);
-        updateTitleAndDescription();
 
-        mShouldShowLottie = shouldShowLottie();
-        boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
-                || BiometricUtils.isLandscape(activity);
-        updateOrientation((isLandscape
-                ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+        new GlifLayoutHelper(activity, containView).setDescriptionText(
+                getString(R.string.security_settings_fingerprint_enroll_start_message));
+
+        mShouldShowLottie = shouldShowLottie(); // Move shouldShowLottie into updateOrientation()?
+        mIllustrationLottie = containView.findViewById(R.id.illustration_lottie);
 
         mErrorText = containView.findViewById(R.id.error_text);
         mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
-        mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin = containView.getMixin(FooterBarMixin.class);
         mFooterBarMixin.setSecondaryButton(
                 new FooterButton.Builder(activity)
                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
@@ -158,16 +219,6 @@
                         .build()
         );
 
-        final LayerDrawable fingerprintDrawable = mProgressBar != null
-                ? (LayerDrawable) mProgressBar.getBackground() : null;
-        if (fingerprintDrawable != null) {
-            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(
@@ -175,37 +226,161 @@
         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
                 activity, android.R.interpolator.fast_out_linear_in);
 
-        if (mProgressBar != null) {
-            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);
+        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);
                 }
-                return true;
-            });
-        }
+            } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                    || event.getActionMasked() == MotionEvent.ACTION_UP) {
+                mProgressBar.removeCallbacks(mShowDialogRunnable);
+            }
+            return true;
+        });
 
+        maybeHideSfpsText(activity.getResources().getConfiguration());
         return containView;
     }
 
-    private void updateTitleAndDescription() {
+    @Override
+    public void onStart() {
+        super.onStart();
+        startEnrollment();
+        updateProgress(false /* animate */);
+        updateTitleAndDescription(new GlifLayoutHelper(getActivity(), mView));
+        if (true /* TODO mRestoring */) {
+            startIconAnimation();
+        }
+    }
 
-        final Activity activity = getActivity();
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
-                (GlifLayout) mView);
+    @Override
+    public void onStop() {
+        stopIconAnimation();
+        mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
+        mProgressViewModel.getHelpMessageLiveData().removeObserver(mHelpMessageObserver);
+        mProgressViewModel.getErrorMessageLiveData().removeObserver(mErrorMessageObserver);
+        mProgressViewModel.getAcquireLiveData().removeObserver(mAcquireObserver);
+        mProgressViewModel.getPointerDownLiveData().removeObserver(mPointerDownObserver);
+        mProgressViewModel.getPointerUpLiveData().removeObserver(mPointerUpObserver);
+        if (!getActivity().isChangingConfigurations()) {
+            mProgressViewModel.cancelEnrollment();
+        }
+        super.onStop();
+    }
 
+    private void startEnrollment() {
+        mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
+        mProgressViewModel.getHelpMessageLiveData().observe(this, mHelpMessageObserver);
+        mProgressViewModel.getErrorMessageLiveData().observe(this, mErrorMessageObserver);
+        mProgressViewModel.getAcquireLiveData().observe(this, mAcquireObserver);
+        mProgressViewModel.getPointerDownLiveData().observe(this, mPointerDownObserver);
+        mProgressViewModel.getPointerUpLiveData().observe(this, mPointerUpObserver);
+        mProgressViewModel.startEnrollment(ENROLL_ENROLL);
+    }
+
+    private void updateProgress(boolean animate) {
+        if (!mProgressViewModel.isEnrolling()) {
+            Log.d(TAG, "Enrollment not started yet");
+            return;
+        }
+
+        final EnrollmentProgress enrollmentProgress =
+                mProgressViewModel.getProgressLiveData().getValue();
+        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 clearError() {
+        applySfpsErrorDynamicColors(false);
+    }
+
+    /**
+     * Applies dynamic colors corresponding to showing or clearing errors on the progress bar
+     * and finger lottie for SFPS
+     */
+    private void applySfpsErrorDynamicColors(boolean isError) {
+        applyProgressBarDynamicColor(isError);
+        if (mIllustrationLottie != null) {
+            applyLottieDynamicColor(isError);
+        }
+    }
+
+    private void applyProgressBarDynamicColor(boolean isError) {
+        final Context context = getActivity().getApplicationContext();
+        int error_color = context.getColor(R.color.sfps_enrollment_progress_bar_error_color);
+        int progress_bar_fill_color = context.getColor(
+                R.color.sfps_enrollment_progress_bar_fill_color);
+        ColorStateList fillColor = ColorStateList.valueOf(
+                isError ? error_color : progress_bar_fill_color);
+        mProgressBar.setProgressTintList(fillColor);
+        mProgressBar.setProgressTintMode(PorterDuff.Mode.SRC);
+        mProgressBar.invalidate();
+    }
+
+    private void applyLottieDynamicColor(boolean isError) {
+        final Context context = getActivity().getApplicationContext();
+        int error_color = context.getColor(R.color.sfps_enrollment_fp_error_color);
+        int fp_captured_color = context.getColor(R.color.sfps_enrollment_fp_captured_color);
+        int color = isError ? error_color : fp_captured_color;
+        mIllustrationLottie.addValueCallback(
+                new KeyPath(".blue100", "**"),
+                LottieProperty.COLOR_FILTER,
+                frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
+        );
+        mIllustrationLottie.invalidate();
+    }
+
+    @Override
+    public void onDestroy() {
+        // TODO stopListenOrientationEvent();
+        super.onDestroy();
+    }
+
+    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(250);
+        anim.start();
+        mProgressAnim = anim;
+    }
+
+    private void updateTitleAndDescription(@NonNull GlifLayoutHelper glifLayoutHelper) {
         if (mIsAccessibilityEnabled) {
             mEnrollingViewModel.clearTalkback();
-            ((GlifLayout) mView).getDescriptionTextView().setAccessibilityLiveRegion(
+            glifLayoutHelper.getGlifLayout().getDescriptionTextView().setAccessibilityLiveRegion(
                     View.ACCESSIBILITY_LIVE_REGION_POLITE);
         }
         switch (getCurrentSfpsStage()) {
@@ -273,12 +448,13 @@
                 // announce a different string for a11y upon entering the page.
                 glifLayoutHelper.setHeaderText(
                         R.string.security_settings_sfps_enroll_find_sensor_title);
-                glifLayoutHelper.setDescriptionText(
-                        R.string.security_settings_sfps_enroll_start_message);
+                glifLayoutHelper.setDescriptionText(getString(
+                        R.string.security_settings_sfps_enroll_start_message));
                 final CharSequence description = getString(
                         R.string.security_settings_sfps_enroll_find_sensor_message);
-                ((GlifLayout) mView).getHeaderTextView().setContentDescription(description);
-                activity.setTitle(description);
+                glifLayoutHelper.getGlifLayout().getHeaderTextView().setContentDescription(
+                        description);
+                glifLayoutHelper.getActivity().setTitle(description);
                 break;
 
         }
@@ -327,8 +503,8 @@
     }
 
     private int getStageThresholdSteps(int index) {
-
-        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+        final EnrollmentProgress progressLiveData =
+                mProgressViewModel.getProgressLiveData().getValue();
 
         if (progressLiveData == null || progressLiveData.getSteps() == -1) {
             Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
@@ -338,7 +514,7 @@
                 * mEnrollingViewModel.getEnrollStageThreshold(index));
     }
 
-    private void updateOrientation(int orientation) {
+    private void updateOrientation() {
         mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
     }
 
@@ -372,9 +548,7 @@
     }
 
     private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(),
-                (GlifLayout) mView);
-        glifLayoutHelper.setDescriptionText(description);
+        new GlifLayoutHelper(getActivity(), mView).setDescriptionText(description);
         LottieCompositionFactory.fromRawRes(getActivity(), lottie)
                 .addListener((c) -> {
                     mIllustrationLottie.setComposition(c);
@@ -390,6 +564,38 @@
         }
     };
 
+    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, 250L);
+                    }
+                }
+
+                @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 = new Runnable() {
+        @Override
+        public void run() {
+            /* TODO launchFinish(); */
+        }
+    };
+
     private final Animatable2.AnimationCallback mIconAnimationCallback =
             new Animatable2.AnimationCallback() {
                 @Override
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
index 89b061f..18a7c25 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
@@ -16,37 +16,39 @@
 
 package com.android.settings.biometrics2.ui.view;
 
+import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
+
 import android.annotation.RawRes;
 import android.app.Activity;
 import android.content.Context;
 import android.content.res.Configuration;
-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.os.Bundle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
-import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.LinearLayout;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModelProvider;
+import androidx.transition.Transition;
+import androidx.transition.TransitionSet;
 
 import com.android.settings.R;
 import com.android.settings.biometrics.BiometricUtils;
 import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
@@ -67,6 +69,7 @@
 
     private static final String TAG = FingerprintEnrollEnrollingUdfpsFragment.class.getSimpleName();
 
+    private static final int PROGRESS_BAR_MAX = 10000;
     private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
     private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
     private static final int HINT_TIMEOUT_DURATION = 2500;
@@ -94,8 +97,7 @@
     private boolean mHaveShownUdfpsCenterLottie;
     private boolean mHaveShownUdfpsGuideLottie;
 
-    private View mView;
-    private ProgressBar mProgressBar;
+    private GlifLayout mView;
     private TextView mErrorText;
     private FooterBarMixin mFooterBarMixin;
     private AnimatedVectorDrawable mIconAnimationDrawable;
@@ -106,6 +108,24 @@
 
     private final View.OnClickListener mOnSkipClickListener =
             (v) -> mEnrollingViewModel.onSkipButtonClick();
+    private final Observer<EnrollmentProgress> mProgressObserver = progress -> {
+        // TODO
+    };
+    private final Observer<EnrollmentStatusMessage> mHelpMessageObserver = helpMessage -> {
+        // TODO
+    };
+    private final Observer<EnrollmentStatusMessage> mErrorMessageObserver = errorMessage -> {
+        // TODO
+    };
+    private final Observer<Boolean> mAcquireObserver = isAcquiredGood -> {
+        // TODO
+    };
+    private final Observer<Integer> mPointerDownObserver = sensorId -> {
+        // TODO
+    };
+    private final Observer<Integer> mPointerUpObserver = sensorId -> {
+        // TODO
+    };
 
     private int mIconTouchCount;
 
@@ -117,61 +137,98 @@
         mRotationViewModel = provider.get(DeviceRotationViewModel.class);
         mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
         super.onAttach(context);
+        final TransitionSet transitionSet = (TransitionSet) getSharedElementEnterTransition();
+        if (transitionSet != null) {
+            transitionSet.addListener(new Transition.TransitionListener() {
+                @Override
+                public void onTransitionStart(@NonNull Transition transition) {
+
+                }
+
+                @Override
+                public void onTransitionEnd(@NonNull Transition transition) {
+                    transition.removeListener(this);
+                    startEnrollment();
+                    mAnimationCancelled = false;
+                    startIconAnimation();
+                }
+
+                @Override
+                public void onTransitionCancel(@NonNull Transition transition) {
+
+                }
+
+                @Override
+                public void onTransitionPause(@NonNull Transition transition) {
+
+                }
+
+                @Override
+                public void onTransitionResume(@NonNull Transition transition) {
+
+                }
+            });
+        }
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mEnrollingViewModel.restoreSavedState(savedInstanceState);
         mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
     }
 
     @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        mEnrollingViewModel.onSaveInstanceState(outState);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         mView = initUdfpsLayout(inflater, container);
         return mView;
     }
 
-    private View initUdfpsLayout(LayoutInflater inflater, ViewGroup container) {
-        final View containView = inflater.inflate(R.layout.udfps_enroll_enrolling, container,
-                false);
+    private GlifLayout initUdfpsLayout(LayoutInflater inflater, ViewGroup container) {
+        final GlifLayout containView = (GlifLayout) inflater.inflate(
+                R.layout.udfps_enroll_enrolling, container, false);
 
-        final Activity activity = getActivity();
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
-                (GlifLayout) containView);
         final int rotation = mRotationViewModel.getLiveData().getValue();
-        final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
-                Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
-
-
-        //TODO  implement b/20653554
         if (rotation == Surface.ROTATION_90) {
+            final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
+                    Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
             final LinearLayout layoutContainer = containView.findViewById(
                     R.id.layout_container);
+            layoutContainer.setPaddingRelative(
+                    (int) getResources().getDimension(R.dimen.rotation_90_enroll_padding_start),
+                    0,
+                    isLayoutRtl ? 0 : (int) getResources().getDimension(
+                            R.dimen.rotation_90_enroll_padding_end),
+                    0);
             final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                     LinearLayout.LayoutParams.MATCH_PARENT,
                     LinearLayout.LayoutParams.MATCH_PARENT);
             lp.setMarginEnd((int) getResources().getDimension(
                     R.dimen.rotation_90_enroll_margin_end));
-            layoutContainer.setPaddingRelative((int) getResources().getDimension(
-                    R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
-                    ? 0 : (int) getResources().getDimension(
-                    R.dimen.rotation_90_enroll_padding_end), 0);
             layoutContainer.setLayoutParams(lp);
             containView.setLayoutParams(lp);
         }
-        glifLayoutHelper.setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
+
+        final Activity activity = getActivity();
+        new GlifLayoutHelper(activity, containView).setDescriptionText(
+                getString(R.string.security_settings_udfps_enroll_start_message));
         updateTitleAndDescription();
 
         mShouldShowLottie = shouldShowLottie();
         boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
                 || BiometricUtils.isLandscape(activity);
-        updateOrientation((isLandscape
+        updateOrientation(containView, (isLandscape
                 ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
 
         mErrorText = containView.findViewById(R.id.error_text);
-        mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
-        mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin = containView.getMixin(FooterBarMixin.class);
         mFooterBarMixin.setSecondaryButton(
                 new FooterButton.Builder(activity)
                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
@@ -181,16 +238,6 @@
                         .build()
         );
 
-        final LayerDrawable fingerprintDrawable = mProgressBar != null
-                ? (LayerDrawable) mProgressBar.getBackground() : null;
-        if (fingerprintDrawable != null) {
-            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(
@@ -198,41 +245,100 @@
         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
                 activity, android.R.interpolator.fast_out_linear_in);
 
-        if (mProgressBar != null) {
-            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();
+        if (true /* TODO mRestoring && !mIsCanceled */) {
+            startEnrollment();
+        }
+        updateProgress(false /* animate */, mProgressViewModel.getProgressLiveData().getValue());
+        updateTitleAndDescription();
+        if (true /* TODO mRestoring */) {
+            startIconAnimation();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        stopIconAnimation();
+        removeEnrollmentObserver();
+        if (!getActivity().isChangingConfigurations()) {
+            mProgressViewModel.cancelEnrollment();
+        }
+        super.onStop();
+    }
+
+    private void removeEnrollmentObserver() {
+        mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
+        mProgressViewModel.getHelpMessageLiveData().removeObserver(mHelpMessageObserver);
+        mProgressViewModel.getErrorMessageLiveData().removeObserver(mErrorMessageObserver);
+        mProgressViewModel.getAcquireLiveData().removeObserver(mAcquireObserver);
+        mProgressViewModel.getPointerDownLiveData().removeObserver(mPointerDownObserver);
+        mProgressViewModel.getPointerUpLiveData().removeObserver(mPointerUpObserver);
+    }
+
+    private void startEnrollment() {
+        mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
+        mProgressViewModel.getHelpMessageLiveData().observe(this, mHelpMessageObserver);
+        mProgressViewModel.getErrorMessageLiveData().observe(this, mErrorMessageObserver);
+        mProgressViewModel.getAcquireLiveData().observe(this, mAcquireObserver);
+        mProgressViewModel.getPointerDownLiveData().observe(this, mPointerDownObserver);
+        mProgressViewModel.getPointerUpLiveData().observe(this, mPointerUpObserver);
+        mProgressViewModel.startEnrollment(ENROLL_ENROLL);
+    }
+
+    private void updateProgress(boolean animate, @NonNull EnrollmentProgress enrollmentProgress) {
+        if (!mProgressViewModel.isEnrolling()) {
+            Log.d(TAG, "Enrollment not started yet");
+            return;
         }
 
-        return containView;
+        final int progress = getProgress(enrollmentProgress);
+
+        if (animate) {
+            animateProgress(progress);
+        } else 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);
+    }
+
+    @Override
+    public void onDestroy() {
+        // TODO stopListenOrientationEvent();
+        super.onDestroy();
+    }
+
+    private void animateProgress(int progress) {
+        // UDFPS animations are owned by SystemUI
+        if (progress >= PROGRESS_BAR_MAX) {
+            // Wait for any animations in SysUI to finish, then proceed to next page
+            getActivity().getMainThreadHandler().postDelayed(mDelayedFinishRunnable, 400L);
+        }
     }
 
     private void updateTitleAndDescription() {
 
         final Activity activity = getActivity();
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
-                (GlifLayout) mView);
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mView);
 
         switch (getCurrentStage()) {
             case STAGE_CENTER:
                 glifLayoutHelper.setHeaderText(
                         R.string.security_settings_fingerprint_enroll_repeat_title);
                 if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
-                    glifLayoutHelper.setDescriptionText(
-                            R.string.security_settings_udfps_enroll_start_message);
+                    glifLayoutHelper.setDescriptionText(getString(
+                            R.string.security_settings_udfps_enroll_start_message));
                 } else if (!mHaveShownUdfpsCenterLottie && mIllustrationLottie != null) {
                     mHaveShownUdfpsCenterLottie = true;
                     // Note: Update string reference when differentiate in between udfps & sfps
@@ -247,8 +353,8 @@
                 glifLayoutHelper.setHeaderText(
                         R.string.security_settings_fingerprint_enroll_repeat_title);
                 if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
-                    glifLayoutHelper.setDescriptionText(
-                            R.string.security_settings_udfps_enroll_repeat_a11y_message);
+                    glifLayoutHelper.setDescriptionText(getString(
+                            R.string.security_settings_udfps_enroll_repeat_a11y_message));
                 } else if (!mHaveShownUdfpsGuideLottie && mIllustrationLottie != null) {
                     mHaveShownUdfpsGuideLottie = true;
                     mIllustrationLottie.setContentDescription(
@@ -280,11 +386,11 @@
                     configureEnrollmentStage("", R.raw.udfps_left_edge_hint_lottie);
                 } else if (mIllustrationLottie == null) {
                     if (isStageHalfCompleted()) {
-                        glifLayoutHelper.setDescriptionText(
-                                R.string.security_settings_fingerprint_enroll_repeat_message);
+                        glifLayoutHelper.setDescriptionText(getString(
+                                R.string.security_settings_fingerprint_enroll_repeat_message));
                     } else {
-                        glifLayoutHelper.setDescriptionText(
-                                R.string.security_settings_udfps_enroll_edge_message);
+                        glifLayoutHelper.setDescriptionText(getString(
+                                R.string.security_settings_udfps_enroll_edge_message));
                     }
                 }
                 break;
@@ -300,11 +406,11 @@
 
                 } else if (mIllustrationLottie == null) {
                     if (isStageHalfCompleted()) {
-                        glifLayoutHelper.setDescriptionText(
-                                R.string.security_settings_fingerprint_enroll_repeat_message);
+                        glifLayoutHelper.setDescriptionText(getString(
+                                R.string.security_settings_fingerprint_enroll_repeat_message));
                     } else {
-                        glifLayoutHelper.setDescriptionText(
-                                R.string.security_settings_udfps_enroll_edge_message);
+                        glifLayoutHelper.setDescriptionText(getString(
+                                R.string.security_settings_udfps_enroll_edge_message));
                     }
                 }
                 break;
@@ -317,11 +423,11 @@
                 // announce a different string for a11y upon entering the page.
                 glifLayoutHelper.setHeaderText(
                         R.string.security_settings_fingerprint_enroll_udfps_title);
-                glifLayoutHelper.setDescriptionText(
-                        R.string.security_settings_udfps_enroll_start_message);
+                glifLayoutHelper.setDescriptionText(getString(
+                        R.string.security_settings_udfps_enroll_start_message));
                 final CharSequence description = getString(
                         R.string.security_settings_udfps_enroll_a11y);
-                ((GlifLayout) mView).getHeaderTextView().setContentDescription(description);
+                mView.getHeaderTextView().setContentDescription(description);
                 activity.setTitle(description);
                 break;
 
@@ -337,7 +443,7 @@
         return defaultDensity == currentDensity;
     }
 
-    private void updateOrientation(int orientation) {
+    private void updateOrientation(@NonNull GlifLayout glifLayout, int orientation) {
         switch (orientation) {
             case Configuration.ORIENTATION_LANDSCAPE: {
                 mIllustrationLottie = null;
@@ -345,7 +451,7 @@
             }
             case Configuration.ORIENTATION_PORTRAIT: {
                 if (mShouldShowLottie) {
-                    mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+                    mIllustrationLottie = glifLayout.findViewById(R.id.illustration_lottie);
                 }
                 break;
             }
@@ -390,7 +496,6 @@
     }
 
     private boolean isStageHalfCompleted() {
-
         EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
         if (progressLiveData == null || progressLiveData.getSteps() == -1) {
             return false;
@@ -431,9 +536,7 @@
     }
 
     private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
-        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(),
-                (GlifLayout) mView);
-        glifLayoutHelper.setDescriptionText(description);
+        new GlifLayoutHelper(getActivity(), mView).setDescriptionText(description);
         LottieCompositionFactory.fromRawRes(getActivity(), lottie)
                 .addListener((c) -> {
                     mIllustrationLottie.setComposition(c);
@@ -449,6 +552,14 @@
         }
     };
 
+    // Give the user a chance to see progress completed before jumping to the next stage.
+    private final Runnable mDelayedFinishRunnable = new Runnable() {
+        @Override
+        public void run() {
+            /* TODO launchFinish(); */
+        }
+    };
+
     private final Animatable2.AnimationCallback mIconAnimationCallback =
             new Animatable2.AnimationCallback() {
                 @Override
@@ -458,13 +569,14 @@
                     }
 
                     // Start animation after it has ended.
+                    /* TODO check mProgressBar?
                     mProgressBar.post(new Runnable() {
                         @Override
                         public void run() {
                             startIconAnimation();
                         }
                     });
+                     */
                 }
             };
-
 }
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java
index d753784..fd181d4 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java
@@ -21,6 +21,7 @@
 
 import android.app.Activity;
 import android.content.Context;
+import android.hardware.fingerprint.FingerprintManager;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -37,6 +38,7 @@
 import com.android.settings.R;
 import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation;
 import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
 
@@ -64,7 +66,7 @@
     private static final String TAG = "FingerprintEnrollFindRfpsFragment";
 
     private FingerprintEnrollFindSensorViewModel mViewModel;
-    private FingerprintEnrollProgressViewModel mPorgressViewModel;
+    private FingerprintEnrollProgressViewModel mProgressViewModel;
 
     private View mView;
     private GlifLayout mGlifLayout;
@@ -77,7 +79,21 @@
             Log.d(TAG, "mProgressObserver(" + progress + ")");
         }
         if (progress != null && !progress.isInitialStep()) {
-            mViewModel.onStartButtonClick();
+            stopLookingForFingerprint(true);
+        }
+    };
+
+    private final Observer<EnrollmentStatusMessage> mLastCancelMessageObserver = errorMessage -> {
+        if (DEBUG) {
+            Log.d(TAG, "mErrorMessageObserver(" + errorMessage + ")");
+        }
+        if (errorMessage != null) {
+            if (errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
+                mProgressViewModel.clearProgressLiveData();
+                mViewModel.onStartButtonClick();
+            } else {
+                Log.e(TAG, "mErrorMessageObserver(" + errorMessage + ")");
+            }
         }
     };
 
@@ -151,40 +167,48 @@
     @Override
     public void onStop() {
         super.onStop();
+        final boolean isEnrolling = mProgressViewModel.isEnrolling();
         if (DEBUG) {
-            Log.d(TAG, "onStop(), stop looking for fingerprint, animation exist:"
+            Log.d(TAG, "onStop(), current enrolling: " + isEnrolling + ", animation exist:"
                     + (mAnimation != null));
         }
-        stopLookingForFingerprint();
+        if (!isEnrolling) {
+            stopLookingForFingerprint(false);
+        }
     }
 
     private void startLookingForFingerprint() {
-        if (mPorgressViewModel.isEnrolling()) {
+        if (mProgressViewModel.isEnrolling()) {
             Log.d(TAG, "startLookingForFingerprint(), failed because isEnrolling is true before"
                     + " starting");
             return;
         }
 
-        mPorgressViewModel.clearProgressLiveData();
-        mPorgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
-        final boolean startResult = mPorgressViewModel.startEnrollment(ENROLL_FIND_SENSOR);
+        mProgressViewModel.clearProgressLiveData();
+        mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
+        final boolean startResult = mProgressViewModel.startEnrollment(ENROLL_FIND_SENSOR);
         if (!startResult) {
             Log.e(TAG, "startLookingForFingerprint(), failed to start enrollment");
         }
     }
 
-    private void stopLookingForFingerprint() {
-        if (!mPorgressViewModel.isEnrolling()) {
+    private void stopLookingForFingerprint(boolean waitForLastCancelErrMsg) {
+        if (!mProgressViewModel.isEnrolling()) {
             Log.d(TAG, "stopLookingForFingerprint(), failed because isEnrolling is false before"
                     + " stopping");
             return;
         }
 
-        mPorgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
-        final boolean cancelResult = mPorgressViewModel.cancelEnrollment();
+        mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
+        final boolean cancelResult = mProgressViewModel.cancelEnrollment();
         if (!cancelResult) {
             Log.e(TAG, "stopLookingForFingerprint(), failed to cancel enrollment");
         }
+
+        if (waitForLastCancelErrMsg) {
+            mProgressViewModel.getErrorMessageLiveData().observe(this,
+                    mLastCancelMessageObserver);
+        }
     }
 
     @Override
@@ -203,7 +227,7 @@
         final FragmentActivity activity = getActivity();
         final ViewModelProvider provider = new ViewModelProvider(activity);
         mViewModel = provider.get(FingerprintEnrollFindSensorViewModel.class);
-        mPorgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+        mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
         super.onAttach(context);
     }
 }
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java
index c363f04..aad503e 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java
@@ -98,6 +98,7 @@
             Log.d(TAG, "mProgressObserver(" + progress + ")");
         }
         if (progress != null && !progress.isInitialStep()) {
+            mProgressViewModel.clearProgressLiveData();
             mViewModel.onStartButtonClick();
         }
     };
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
index 02aa5f2..3f98d25 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
@@ -85,15 +85,15 @@
                 (GlifLayout) mView);
 
         glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title);
-        glifLayoutHelper.setDescriptionText(
-                R.string.security_settings_fingerprint_enroll_finish_v2_message);
+        glifLayoutHelper.setDescriptionText(getString(
+                R.string.security_settings_fingerprint_enroll_finish_v2_message));
 
         final int maxEnrollments = mFingerprintEnrollFinishViewModel.getMaxFingerprints();
         final int enrolled = mFingerprintEnrollFinishViewModel.getNumOfEnrolledFingerprintsSize();
         if (mCanAssumeSfps) {
             if (enrolled < maxEnrollments) {
-                glifLayoutHelper.setDescriptionText(R.string
-                        .security_settings_fingerprint_enroll_finish_v2_add_fingerprint_message);
+                glifLayoutHelper.setDescriptionText(getString(R.string
+                        .security_settings_fingerprint_enroll_finish_v2_add_fingerprint_message));
             }
         }
 
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
index dddd641..e12e982 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
@@ -26,6 +26,7 @@
 import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
 import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_IS_GENERATING_CHALLENGE;
 import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_VALID;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.ErrorDialogData;
 import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG;
 import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP;
 import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START;
@@ -63,8 +64,8 @@
 import com.android.settings.R;
 import com.android.settings.Utils;
 import com.android.settings.biometrics.BiometricEnrollBase;
-import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
-import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollEnrolling;
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollFinish;
+import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFinish;
 import com.android.settings.biometrics2.data.repository.FingerprintRepository;
 import com.android.settings.biometrics2.factory.BiometricsViewModelFactory;
 import com.android.settings.biometrics2.ui.model.CredentialModel;
@@ -72,6 +73,7 @@
 import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator;
 import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
@@ -88,11 +90,16 @@
     private static final boolean DEBUG = false;
     private static final String TAG = "FingerprintEnrollmentActivity";
 
-    private static final String INTRO_TAG = "enroll-intro";
-    private static final String FIND_UDFPS_TAG = "enroll-find-udfps";
-    private static final String FIND_SFPS_TAG = "enroll-find-sfps";
-    private static final String FIND_RFPS_TAG = "enroll-find-rfps";
+    private static final String INTRO_TAG = "intro";
+    private static final String FIND_UDFPS_TAG = "find-udfps";
+    private static final String FIND_SFPS_TAG = "find-sfps";
+    private static final String FIND_RFPS_TAG = "find-rfps";
+    private static final String ENROLLING_UDFPS_TAG = "enrolling-udfps";
+    private static final String ENROLLING_SFPS_TAG = "enrolling-sfps";
+    private static final String ENROLLING_RFPS_TAG = "enrolling-rfps";
+    private static final String FINISH_TAG = "finish";
     private static final String SKIP_SETUP_FIND_FPS_DIALOG_TAG = "skip-setup-dialog";
+    private static final String ENROLLING_ERROR_DIALOG_TAG = "enrolling-error-dialog";
 
     protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1;
 
@@ -115,6 +122,22 @@
             onFindSensorAction(action);
         }
     };
+    private final Observer<Boolean> mEnrollingDoneObserver = isDone -> {
+        if (DEBUG) {
+            Log.d(TAG, "mEnrollingDoneObserver(" + isDone + ")");
+        }
+        if (isDone != null) {
+            onEnrollingDone(isDone);
+        }
+    };
+    private final Observer<ErrorDialogData> mEnrollingErrorDialogObserver = data -> {
+        if (DEBUG) {
+            Log.d(TAG, "mEnrollingErrorDialogObserver(" + data + ")");
+        }
+        if (data != null) {
+            startEnrollingErrorDialog();
+        }
+    };
     private final ActivityResultCallback<ActivityResult> mNextActivityResultCallback =
             result -> mViewModel.onContinueEnrollActivityResult(result,
                     mAutoCredentialViewModel.getUserId());
@@ -154,6 +177,9 @@
         } else {
             final FragmentManager manager = getSupportFragmentManager();
             String[] tags = new String[] {
+                    ENROLLING_UDFPS_TAG,
+                    ENROLLING_SFPS_TAG,
+                    ENROLLING_RFPS_TAG,
                     FIND_UDFPS_TAG,
                     FIND_SFPS_TAG,
                     FIND_RFPS_TAG,
@@ -169,7 +195,12 @@
                 }
                 if (tag.equals(INTRO_TAG)) {
                     attachIntroViewModel();
-                } else { // FIND_UDFPS_TAG, FIND_SFPS_TAG, FIND_RFPS_TAG
+                } else if (tag.equals(FIND_UDFPS_TAG) || tag.equals(FIND_SFPS_TAG)
+                        || tag.equals(FIND_RFPS_TAG)) {
+                    attachFindSensorViewModel();
+                    attachIntroViewModel();
+                } else { // ENROLLING_UDFPS_TAG, ENROLLING_SFPS_TAG, ENROLLING_RFPS_TAG
+                    attachEnrollingViewModel();
                     attachFindSensorViewModel();
                     attachIntroViewModel();
                 }
@@ -205,35 +236,29 @@
 
     // We need to make sure token is valid before entering find sensor page
     private void startFindSensorFragment() {
-        attachFindSensorViewModel();
-        if (mViewModel.canAssumeUdfps()) {
-            // UDFPS does not need to start real fingerprint enrolling during finding sensor
-            startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindUdfpsFragment.class,
-                    FIND_UDFPS_TAG, false /* initProgressViewModel */);
-        } else if (mViewModel.canAssumeSfps()) {
-            startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindSfpsFragment.class,
-                    FIND_SFPS_TAG, true /* initProgressViewModel */);
-        } else {
-            startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindRfpsFragment.class,
-                    FIND_RFPS_TAG, true /* initProgressViewModel */);
-        }
-    }
+        // Always setToken into progressViewModel even it is not necessary action for UDFPS
+        mViewModelProvider.get(FingerprintEnrollProgressViewModel.class)
+                .setToken(mAutoCredentialViewModel.getToken());
 
-    private void startFindFpsFragmentWithProgressViewModel(
-            @NonNull Class<? extends Fragment> findFpsClass, @NonNull String tag,
-            boolean initProgressViewModel) {
-        if (initProgressViewModel) {
-            final FingerprintEnrollProgressViewModel progressViewModel =
-                    mViewModelProvider.get(FingerprintEnrollProgressViewModel.class);
-            progressViewModel.setToken(mAutoCredentialViewModel.getToken());
+        attachFindSensorViewModel();
+
+        final String tag;
+        final Class<? extends Fragment> fragmentClass;
+        if (mViewModel.canAssumeUdfps()) {
+            tag = FIND_UDFPS_TAG;
+            fragmentClass = FingerprintEnrollFindUdfpsFragment.class;
+        } else if (mViewModel.canAssumeSfps()) {
+            tag = FIND_SFPS_TAG;
+            fragmentClass = FingerprintEnrollFindSfpsFragment.class;
+        } else {
+            tag = FIND_RFPS_TAG;
+            fragmentClass = FingerprintEnrollFindRfpsFragment.class;
         }
-        final FingerprintEnrollFindSensorViewModel findSensorViewModel =
-                mViewModelProvider.get(FingerprintEnrollFindSensorViewModel.class);
         getSupportFragmentManager().beginTransaction()
                 .setReorderingAllowed(true)
                 .setCustomAnimations(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out,
                         R.anim.sud_slide_back_in, R.anim.sud_slide_back_out)
-                .replace(R.id.fragment_container_view, findFpsClass, null, tag)
+                .replace(R.id.fragment_container_view, fragmentClass, null, tag)
                 .addToBackStack(tag)
                 .commit();
     }
@@ -248,11 +273,68 @@
         findSensorViewModel.getActionLiveData().observe(this, mFindSensorActionObserver);
     }
 
+    private void startEnrollingFragment() {
+        // Always setToken into progressViewModel even it is not necessary action for SFPS or RFPS
+        mViewModelProvider.get(FingerprintEnrollProgressViewModel.class)
+                .setToken(mAutoCredentialViewModel.getToken());
+
+        attachEnrollingViewModel();
+
+        final String tag;
+        final Class<? extends Fragment> fragmentClass;
+        if (mViewModel.canAssumeUdfps()) {
+            tag = ENROLLING_UDFPS_TAG;
+            fragmentClass = FingerprintEnrollEnrollingUdfpsFragment.class;
+        } else if (mViewModel.canAssumeSfps()) {
+            tag = ENROLLING_SFPS_TAG;
+            fragmentClass = FingerprintEnrollEnrollingSfpsFragment.class;
+        } else {
+            tag = ENROLLING_RFPS_TAG;
+            fragmentClass = FingerprintEnrollEnrollingRfpsFragment.class;
+        }
+        getSupportFragmentManager().beginTransaction()
+                .setReorderingAllowed(true)
+                .setCustomAnimations(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out,
+                        R.anim.sud_slide_back_in, R.anim.sud_slide_back_out)
+                .replace(R.id.fragment_container_view, fragmentClass, null, tag)
+                .addToBackStack(tag)
+                .commit();
+    }
+
+    private void attachEnrollingViewModel() {
+        final FingerprintEnrollEnrollingViewModel enrollingViewModel =
+                mViewModelProvider.get(FingerprintEnrollEnrollingViewModel.class);
+        enrollingViewModel.clearBackPressedData();
+        enrollingViewModel.getErrorDialogLiveData().observe(this, mEnrollingErrorDialogObserver);
+
+        final FingerprintEnrollProgressViewModel progressViewModel =
+                mViewModelProvider.get(FingerprintEnrollProgressViewModel.class);
+        progressViewModel.getDoneLiveData().observe(this, mEnrollingDoneObserver);
+    }
+
+    private void startFinishActivity() {
+        final boolean isSuw = mViewModel.getRequest().isSuw();
+        if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+            Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag");
+        }
+        Intent intent = new Intent(this, isSuw
+                ? SetupFingerprintEnrollFinish.class
+                : FingerprintEnrollFinish.class);
+        intent.putExtras(mAutoCredentialViewModel.createCredentialIntentExtra());
+        intent.putExtras(mViewModel.getNextActivityBaseIntentExtras());
+        mNextActivityLauncher.launch(intent);
+    }
+
     private void startSkipSetupFindFpsDialog() {
         new SkipSetupFindFpsDialog().show(getSupportFragmentManager(),
                 SKIP_SETUP_FIND_FPS_DIALOG_TAG);
     }
 
+    private void startEnrollingErrorDialog() {
+        new FingerprintEnrollEnrollingErrorDialog().show(getSupportFragmentManager(),
+                ENROLLING_ERROR_DIALOG_TAG);
+    }
+
     private void onGenerateChallengeFailed(@NonNull Boolean ignoredBoolean) {
         onSetActivityResult(new ActivityResult(RESULT_CANCELED, null));
     }
@@ -365,20 +447,22 @@
                 return;
             }
             case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START: {
-                final boolean isSuw = mViewModel.getRequest().isSuw();
-                if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
-                    Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag");
-                }
-                Intent intent = new Intent(this, isSuw
-                        ? SetupFingerprintEnrollEnrolling.class
-                        : FingerprintEnrollEnrolling.class);
-                intent.putExtras(mAutoCredentialViewModel.createCredentialIntentExtra());
-                intent.putExtras(mViewModel.getNextActivityBaseIntentExtras());
-                mNextActivityLauncher.launch(intent);
+                startEnrollingFragment();
             }
         }
     }
 
+    private void onEnrollingDone(boolean isDone) {
+        if (!isDone) {
+            return;
+        }
+        final FingerprintEnrollProgressViewModel progressViewModel =
+                mViewModelProvider.get(FingerprintEnrollProgressViewModel.class);
+        progressViewModel.clearProgressLiveData();
+
+        startFinishActivity();
+    }
+
     @Override
     protected void onPause() {
         super.onPause();
@@ -386,6 +470,21 @@
     }
 
     @Override
+    public void onBackPressed() {
+        final FragmentManager manager = getSupportFragmentManager();
+        final String[] tags = new String[] {ENROLLING_UDFPS_TAG, ENROLLING_SFPS_TAG,
+                ENROLLING_RFPS_TAG };
+        for (String tag: tags) {
+            final Fragment fragment = manager.findFragmentByTag(tag);
+            if (fragment != null) {
+                mViewModelProvider.get(FingerprintEnrollEnrollingViewModel.class).onBackPressed();
+                break;
+            }
+        }
+        super.onBackPressed();
+    }
+
+    @Override
     protected void onApplyThemeResource(Resources.Theme theme, @StyleRes int resid, boolean first) {
         theme.applyStyle(R.style.SetupWizardPartnerResource, true);
         super.onApplyThemeResource(theme, resid, first);
diff --git a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
index 814f579..6af4928 100644
--- a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
+++ b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
@@ -68,15 +68,13 @@
         }
     }
 
-    /**
-     * Sets description resId to GlifLayout
-     */
-    public void setDescriptionText(int resId) {
-        CharSequence previousDescription = mGlifLayout.getDescriptionText();
-        CharSequence description = mActivity.getString(resId);
-        // Prevent a11y for re-reading the same string
-        if (!TextUtils.equals(previousDescription, description)) {
-            mGlifLayout.setDescriptionText(resId);
-        }
+    @NonNull
+    public Activity getActivity() {
+        return mActivity;
+    }
+
+    @NonNull
+    public GlifLayout getGlifLayout() {
+        return mGlifLayout;
     }
 }
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
index 058b50b..2877c9a 100644
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
@@ -16,26 +16,32 @@
 
 package com.android.settings.biometrics2.ui.viewmodel;
 
+import android.annotation.IntDef;
 import android.app.Application;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.os.Bundle;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.util.Log;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
 import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
 import com.android.settings.biometrics2.data.repository.FingerprintRepository;
 import com.android.settings.biometrics2.data.repository.VibratorRepository;
-import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * ViewModel explaining the fingerprint enrolling page
  */
-public class FingerprintEnrollEnrollingViewModel extends AndroidViewModel
-        implements DefaultLifecycleObserver {
+public class FingerprintEnrollEnrollingViewModel extends AndroidViewModel {
 
     private static final String TAG = FingerprintEnrollEnrollingViewModel.class.getSimpleName();
     private static final boolean DEBUG = false;
@@ -45,35 +51,118 @@
     private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
 
-    //Enrolling skip
+    /**
+     * Enrolling skipped
+     */
     public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP = 0;
 
-    //Icon touch dialog show
-    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG = 0;
+    /**
+     * Enrolling finished
+     */
+    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE = 1;
 
-    //Icon touch dialog dismiss
-    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG = 1;
+    /**
+     * Icon touch dialog show
+     */
+    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG = 2;
 
+    /**
+     * Icon touch dialog dismiss
+     */
+    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG = 3;
+
+    @IntDef(prefix = { "FINGERPRINT_ENROLL_ENROLLING_ACTION_" }, value = {
+            FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP,
+            FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE,
+            FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG,
+            FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FingerprintEnrollEnrollingAction {}
+
+    /**
+     * Enrolling skipped
+     */
+    public static final int FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH = 0;
+
+    /**
+     * Enrolling finished
+     */
+    public static final int FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT = 1;
+
+    /**
+     * Icon touch dialog show
+     */
+    public static final int FINGERPRINT_ERROR_DIALOG_ACTION_RESTART = 2;
+
+    @IntDef(prefix = { "FINGERPRINT_ERROR_DIALOG_ACTION_" }, value = {
+            FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH,
+            FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT,
+            FINGERPRINT_ERROR_DIALOG_ACTION_RESTART
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FingerprintErrorDialogAction {}
+
+
+    private final int mUserId;
     private final FingerprintRepository mFingerprintRepository;
     private final AccessibilityRepository mAccessibilityRepository;
     private final VibratorRepository mVibratorRepository;
 
-    private EnrollmentRequest mEnrollmentRequest = null;
+    private final MutableLiveData<Boolean> mBackPressedLiveData = new MutableLiveData<>(false);
     private final MutableLiveData<Integer> mEnrollingLiveData = new MutableLiveData<>();
     private final MutableLiveData<Integer> mIconTouchDialogLiveData = new MutableLiveData<>();
-
+    private final MutableLiveData<ErrorDialogData> mErrorDialogLiveData = new MutableLiveData<>();
+    private final MutableLiveData<Integer> mErrorDialogActionLiveData = new MutableLiveData<>();
 
     public FingerprintEnrollEnrollingViewModel(Application application,
+            int userId,
             FingerprintRepository fingerprintRepository,
             AccessibilityRepository accessibilityRepository,
             VibratorRepository vibratorRepository) {
         super(application);
+        mUserId = userId;
         mFingerprintRepository = fingerprintRepository;
         mAccessibilityRepository = accessibilityRepository;
         mVibratorRepository = vibratorRepository;
     }
 
     /**
+     * Notifies activity to show error dialog
+     */
+    public void showErrorDialog(@NonNull ErrorDialogData errorDialogData) {
+        mErrorDialogLiveData.postValue(errorDialogData);
+    }
+
+    public LiveData<ErrorDialogData> getErrorDialogLiveData() {
+        return mErrorDialogLiveData;
+    }
+
+    /**
+     * Saves new user dialog action to mErrorDialogActionLiveData
+     */
+    public void onErrorDialogAction(@FingerprintErrorDialogAction int action) {
+        if (DEBUG) {
+            Log.d(TAG, "onErrorDialogAction(" + action + ")");
+        }
+        mErrorDialogActionLiveData.postValue(action);
+    }
+
+    /**
+     * Clears back press data
+     */
+    public void clearBackPressedData() {
+        mBackPressedLiveData.setValue(false);
+    }
+
+    /**
+     * User trigger back pressed
+     */
+    public void onBackPressed() {
+        mBackPressedLiveData.postValue(true);
+    }
+
+    /**
      * User clicks skip button
      */
     public void onSkipButtonClick() {
@@ -85,6 +174,17 @@
     }
 
     /**
+     * Is enrolling finished
+     */
+    public void onEnrollingDone() {
+        final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP;
+        if (DEBUG) {
+            Log.d(TAG, "onEnrollingDone, post action " + action);
+        }
+        mEnrollingLiveData.postValue(action);
+    }
+
+    /**
      * Icon touch dialog show
      */
     public void onIconTouchDialogShow() {
@@ -121,20 +221,6 @@
     }
 
     /**
-     * The first sensor type is UDFPS sensor or not
-     */
-    public boolean canAssumeUdfps() {
-        return mFingerprintRepository.canAssumeUdfps();
-    }
-
-    /**
-     * The first sensor type is SFPS sensor or not
-     */
-    public boolean canAssumeSfps() {
-        return mFingerprintRepository.canAssumeSfps();
-    }
-
-    /**
      * Requests interruption of the accessibility feedback from all accessibility services.
      */
     public void clearTalkback() {
@@ -154,8 +240,80 @@
      * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the
      * caller to specify the vibration is owned by someone else and set a reason for vibration.
      */
-    public void vibrateError(int uid, String opPkg, String reason) {
-        mVibratorRepository.vibrate(uid, opPkg, VIBRATE_EFFECT_ERROR, reason,
-                FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
+    public void vibrateError(String reason) {
+        mVibratorRepository.vibrate(mUserId, getApplication().getOpPackageName(),
+                VIBRATE_EFFECT_ERROR, reason, FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
+    }
+
+    /**
+     * Gets the first FingerprintSensorPropertiesInternal from FingerprintManager
+     */
+    @Nullable
+    public FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
+        return mFingerprintRepository.getFirstFingerprintSensorPropertiesInternal();
+    }
+
+    /**
+     * The first sensor type is UDFPS sensor or not
+     */
+    public boolean canAssumeUdfps() {
+        return mFingerprintRepository.canAssumeUdfps();
+    }
+
+    /**
+     * Saves current state to outState
+     */
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        // TODO
+//        mRestoring = true;
+//        mIsCanceled = savedInstanceState.getBoolean(KEY_STATE_CANCELED, false);
+//        mPreviousRotation = savedInstanceState.getInt(KEY_STATE_PREVIOUS_ROTATION,
+//                getDisplay().getRotation());
+//        mIsOrientationChanged = mPreviousRotation != getDisplay().getRotation();
+    }
+
+    /**
+     * Restores saved state from previous savedInstanceState
+     */
+    public void restoreSavedState(@Nullable Bundle savedInstanceState) {
+        if (savedInstanceState == null) {
+            return;
+        }
+        // TODO
+//        outState.putBoolean(KEY_STATE_CANCELED, mIsCanceled);
+//        outState.putInt(KEY_STATE_PREVIOUS_ROTATION, mPreviousRotation);
+    }
+
+    /**
+     * Data for passing to FingerprintEnrollEnrollingErrorDialog
+     */
+    public static class ErrorDialogData {
+        @NonNull private final CharSequence mErrMsg;
+        @NonNull private final CharSequence mErrTitle;
+        @NonNull private final int mErrMsgId;
+
+        public ErrorDialogData(@NonNull CharSequence errMsg, @NonNull CharSequence errTitle,
+                int errMsgId) {
+            mErrMsg = errMsg;
+            mErrTitle = errTitle;
+            mErrMsgId = errMsgId;
+        }
+
+        public CharSequence getErrMsg() {
+            return mErrMsg;
+        }
+
+        public CharSequence getErrTitle() {
+            return mErrTitle;
+        }
+
+        public int getErrMsgId() {
+            return mErrMsgId;
+        }
+
+        @Override
+        public String toString() {
+            return ErrorDialogData.class.getSimpleName() + "{id:" + mErrMsgId + "}";
+        }
     }
 }
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
index 532e2cc..2cb94ec 100644
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
@@ -55,17 +55,17 @@
             new MutableLiveData<>();
     private final MutableLiveData<EnrollmentStatusMessage> mErrorMessageLiveData =
             new MutableLiveData<>();
-
     private final MutableLiveData<Boolean> mAcquireLiveData = new MutableLiveData<>();
     private final MutableLiveData<Integer> mPointerDownLiveData = new MutableLiveData<>();
     private final MutableLiveData<Integer> mPointerUpLiveData = new MutableLiveData<>();
+    private final MutableLiveData<Boolean> mDoneLiveData = new MutableLiveData<>(false);
 
     private byte[] mToken = null;
     private final int mUserId;
 
     private final FingerprintUpdater mFingerprintUpdater;
     private final MessageDisplayController mMessageDisplayController;
-    private EnrollmentHelper mEnrollmentHelper;
+    @Nullable private CancellationSignal mCancellationSignal = null;
     private final EnrollmentCallback mEnrollmentCallback = new EnrollmentCallback() {
 
         @Override
@@ -78,7 +78,11 @@
                         + ", post progress as " + progress);
             }
             mProgressLiveData.postValue(progress);
-            // TODO set enrolling to false when remaining is 0 during implementing b/260957933
+
+            final Boolean done = remaining == 0;
+            if (!done.equals(mDoneLiveData.getValue())) {
+                mDoneLiveData.postValue(done);
+            }
         }
 
         @Override
@@ -136,7 +140,10 @@
      * clear progress
      */
     public void clearProgressLiveData() {
+        mDoneLiveData.setValue(false);
         mProgressLiveData.setValue(new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING));
+        mHelpMessageLiveData.setValue(null);
+        mErrorMessageLiveData.setValue(null);
     }
 
     public LiveData<EnrollmentProgress> getProgressLiveData() {
@@ -151,18 +158,21 @@
         return mErrorMessageLiveData;
     }
 
-    public MutableLiveData<Boolean> getAcquireLiveData() {
+    public LiveData<Boolean> getAcquireLiveData() {
         return mAcquireLiveData;
     }
 
-    public MutableLiveData<Integer> getPointerDownLiveData() {
+    public LiveData<Integer> getPointerDownLiveData() {
         return mPointerDownLiveData;
     }
 
-    public MutableLiveData<Integer> getPointerUpLiveData() {
+    public LiveData<Integer> getPointerUpLiveData() {
         return mPointerUpLiveData;
     }
 
+    public LiveData<Boolean> getDoneLiveData() {
+        return mDoneLiveData;
+    }
 
     /**
      * Starts enrollment and return latest isEnrolling() result
@@ -172,16 +182,18 @@
             Log.e(TAG, "Null hardware auth token for enroll");
             return false;
         }
-        if (isEnrolling()) {
+        if (mCancellationSignal != null) {
             Log.w(TAG, "Enrolling has started, shall not start again");
             return true;
         }
+        if (DEBUG) {
+            Log.e(TAG, "startEnrollment(" + reason + ")");
+        }
 
-        mEnrollmentHelper = new EnrollmentHelper(
-                mMessageDisplayController != null
-                        ? mMessageDisplayController
-                        : mEnrollmentCallback);
-        mEnrollmentHelper.startEnrollment(mFingerprintUpdater, mToken, mUserId, reason);
+        mCancellationSignal = new CancellationSignal();
+        mFingerprintUpdater.enroll(mToken, mCancellationSignal, mUserId,
+                mMessageDisplayController != null ? mMessageDisplayController : mEnrollmentCallback,
+                reason);
         return true;
     }
 
@@ -189,86 +201,22 @@
      * Cancels enrollment and return latest isEnrolling result
      */
     public boolean cancelEnrollment() {
-        if (!isEnrolling() || mEnrollmentHelper == null) {
-            Log.e(TAG, "Fail to cancel enrollment, enrollmentController exist:"
-                    + (mEnrollmentHelper != null));
+        final CancellationSignal cancellationSignal = mCancellationSignal;
+        if (cancellationSignal == null) {
+            Log.e(TAG, "Fail to cancel enrollment, has cancelled or not start");
             return false;
         }
 
-        mEnrollmentHelper.cancelEnrollment();
-        mEnrollmentHelper = null;
+        mCancellationSignal = null;
+        cancellationSignal.cancel();
         return true;
     }
 
     public boolean isEnrolling() {
-        return (mEnrollmentHelper != null);
+        return (mCancellationSignal != null);
     }
 
     private int getSteps() {
         return mProgressLiveData.getValue().getSteps();
     }
-
-    /**
-     * This class is used to stop latest message from onEnrollmentError() after user cancelled
-     * enrollment. This class will not forward message anymore after mCancellationSignal is sent.
-     */
-    private static class EnrollmentHelper extends EnrollmentCallback {
-
-        @NonNull private final EnrollmentCallback mEnrollmentCallback;
-        @Nullable private CancellationSignal mCancellationSignal = new CancellationSignal();
-
-        EnrollmentHelper(@NonNull EnrollmentCallback enrollmentCallback) {
-            mEnrollmentCallback = enrollmentCallback;
-        }
-
-        @Override
-        public void onEnrollmentError(int errMsgId, CharSequence errString) {
-            if (mCancellationSignal == null) {
-                return;
-            }
-            mEnrollmentCallback.onEnrollmentError(errMsgId, errString);
-        }
-
-        @Override
-        public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
-            if (mCancellationSignal == null) {
-                return;
-            }
-            mEnrollmentCallback.onEnrollmentHelp(helpMsgId, helpString);
-        }
-
-        @Override
-        public void onEnrollmentProgress(int remaining) {
-            if (mCancellationSignal == null) {
-                return;
-            }
-            mEnrollmentCallback.onEnrollmentProgress(remaining);
-        }
-
-        /**
-         * Starts enrollment
-         */
-        public boolean startEnrollment(@NonNull FingerprintUpdater fingerprintUpdater,
-                @NonNull byte[] token, int userId, @EnrollReason int reason) {
-            if (mCancellationSignal == null) {
-                // Not allow enrolling twice as same instance. Allocate a new instance for second
-                // enrollment.
-                return false;
-            }
-            fingerprintUpdater.enroll(token, mCancellationSignal, userId, this, reason);
-            return true;
-        }
-
-        /**
-         * Cancels current enrollment
-         */
-        public void cancelEnrollment() {
-            final CancellationSignal cancellationSignal = mCancellationSignal;
-            mCancellationSignal = null;
-
-            if (cancellationSignal != null) {
-                cancellationSignal.cancel();
-            }
-        }
-    }
 }