Fingerprint Introduction FragmentActivity

Introducing MVVM architecture & fragments to biometric settings.
Here, we modify the first page of FingerprintEnrollIntroduction to use
new MVVM with Fragment architecture.

And with this new architecture, unit test and screen order will be
easier to be written or changed.

Bug: 236072782
Test: atest FingerprintEnrollmentViewModelTest AutoCredentialViewModelTest
	    FingerprintEnrollIntroViewModelTest FingerprintRepositoryTest
Change-Id: Icf12c91625db86c2c99081a0108203e607e77f74
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5d713b0..d580064 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2384,6 +2384,11 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".biometrics2.ui.view.FingerprintEnrollmentActivity"
+            android:exported="true"
+            android:permission="android.permission.MANAGE_FINGERPRINT"
+            android:theme="@style/GlifTheme.Light"/>
+
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal"
                   android:exported="false"
                   android:theme="@style/GlifTheme.Light"
diff --git a/res/layout/biometric_enrollment_container.xml b/res/layout/biometric_enrollment_container.xml
new file mode 100644
index 0000000..b6ed0ae
--- /dev/null
+++ b/res/layout/biometric_enrollment_container.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.fragment.app.FragmentContainerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/fragment_container_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
index ad0d4ea..0bd9996 100644
--- a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
+++ b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
@@ -39,6 +39,7 @@
 import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
 import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
 import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
 import com.android.settings.core.FeatureFlags;
 import com.android.settings.homepage.DeepLinkHomepageActivity;
 import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
@@ -225,6 +226,7 @@
                     .buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
             addActivityFilter(activityFilters, searchIntent);
         }
+        addActivityFilter(activityFilters, FingerprintEnrollmentActivity.class);
         addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
         addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
         addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index f395aca..08c8c4f 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -25,6 +25,7 @@
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorPropertiesInternal;
+import android.os.Bundle;
 import android.os.storage.StorageManager;
 import android.util.Log;
 import android.view.Surface;
@@ -147,6 +148,31 @@
 
     /**
      * @param context caller's context
+     * @param isSuw if it is running in setup wizard flows
+     * @param suwExtras setup wizard extras for new intent
+     * @return Intent for starting ChooseLock*
+     */
+    public static Intent getChooseLockIntent(@NonNull Context context,
+            boolean isSuw, @NonNull Bundle suwExtras) {
+        if (isSuw) {
+            // Default to PIN lock in setup wizard
+            Intent intent = new Intent(context, SetupChooseLockGeneric.class);
+            if (StorageManager.isFileEncrypted()) {
+                intent.putExtra(
+                        LockPatternUtils.PASSWORD_TYPE_KEY,
+                        DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+                intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment
+                        .EXTRA_SHOW_OPTIONS_BUTTON, true);
+            }
+            intent.putExtras(suwExtras);
+            return intent;
+        } else {
+            return new Intent(context, ChooseLockGeneric.class);
+        }
+    }
+
+    /**
+     * @param context caller's context
      * @param activityIntent The intent that started the caller's activity
      * @return Intent for starting FingerprintEnrollFindSensor
      */
diff --git a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
index b313961..eb68687 100644
--- a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
@@ -32,7 +32,7 @@
     /**
      * Returns the number of fingerprint enrolled.
      */
-    private static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
+    public static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
 
     private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent";
 
diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
new file mode 100644
index 0000000..f58175a
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 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.data.repository;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.ParentalControlsUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+
+import java.util.List;
+
+/**
+ * This repository is used to call all APIs in {@link FingerprintManager}
+ */
+public class FingerprintRepository {
+
+    @NonNull private final FingerprintManager mFingerprintManager;
+
+    public FingerprintRepository(@NonNull FingerprintManager fingerprintManager) {
+        mFingerprintManager = fingerprintManager;
+    }
+
+    /**
+     * The first sensor type is UDFPS sensor or not
+     */
+    public boolean canAssumeUdfps() {
+        FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
+        return prop != null && prop.isAnyUdfpsType();
+    }
+
+    /**
+     * Get max possible number of fingerprints for a user
+     */
+    public int getMaxFingerprints() {
+        FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
+        return prop != null ? prop.maxEnrollmentsPerUser : 0;
+    }
+
+    /**
+     * Get number of fingerprints that this user enrolled.
+     */
+    public int getNumOfEnrolledFingerprintsSize(int userId) {
+        final List<Fingerprint> list = mFingerprintManager.getEnrolledFingerprints(userId);
+        return list != null ? list.size() : 0;
+    }
+
+    /**
+     * Get maximum possible fingerprints in setup wizard flow
+     */
+    public int getMaxFingerprintsInSuw(@NonNull Resources resources) {
+        return resources.getInteger(R.integer.suw_max_fingerprints_enrollable);
+    }
+
+    @Nullable
+    private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
+        final List<FingerprintSensorPropertiesInternal> props =
+                mFingerprintManager.getSensorPropertiesInternal();
+        return props.size() > 0 ? props.get(0) : null;
+    }
+
+    /**
+     * Call FingerprintManager to generate challenge for first sensor
+     */
+    public void generateChallenge(int userId,
+            @NonNull FingerprintManager.GenerateChallengeCallback callback) {
+        mFingerprintManager.generateChallenge(userId, callback);
+    }
+
+    /**
+     * Get parental consent required or not during enrollment process
+     */
+    public boolean isParentalConsentRequired(@NonNull Context context) {
+        return ParentalControlsUtils.parentConsentRequired(context, TYPE_FINGERPRINT) != null;
+    }
+
+    /**
+     * Get fingerprint is disable by admin or not
+     */
+    public boolean isDisabledByAdmin(@NonNull Context context, int userId) {
+        return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+                context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null;
+    }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java
new file mode 100644
index 0000000..9a0cab2
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 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.factory;
+
+import android.app.Application;
+import android.app.admin.DevicePolicyManager;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentFactory;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollIntroFragment;
+
+/**
+ * Fragment factory for biometrics
+ */
+public class BiometricsFragmentFactory extends FragmentFactory {
+
+    private final Application mApplication;
+    private final ViewModelProvider mViewModelProvider;
+
+    public BiometricsFragmentFactory(Application application,
+            ViewModelProvider viewModelProvider) {
+        mApplication = application;
+        mViewModelProvider = viewModelProvider;
+    }
+
+    @NonNull
+    @Override
+    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
+        final Class<? extends Fragment> clazz = loadFragmentClass(classLoader, className);
+        if (FingerprintEnrollIntroFragment.class.equals(clazz)) {
+            final DevicePolicyManager devicePolicyManager =
+                    mApplication.getSystemService(DevicePolicyManager.class);
+            if (devicePolicyManager != null) {
+                return new FingerprintEnrollIntroFragment(mViewModelProvider,
+                        devicePolicyManager.getResources());
+            }
+        }
+        return super.instantiate(classLoader, className);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
new file mode 100644
index 0000000..fdc5745
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.factory;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+
+/**
+ * Interface for BiometricsRepositoryProvider
+ */
+public interface BiometricsRepositoryProvider {
+
+    /**
+     * Get FingerprintRepository
+     */
+    @Nullable
+    FingerprintRepository getFingerprintRepository(@NonNull Application application);
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
new file mode 100644
index 0000000..87b41e9
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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.factory;
+
+import android.app.Application;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.Utils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+
+/**
+ * Implementation for BiometricsRepositoryProvider
+ */
+public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider {
+
+    /**
+     * Get FingerprintRepository
+     */
+    @Nullable
+    @Override
+    public FingerprintRepository getFingerprintRepository(@NonNull Application application) {
+        final FingerprintManager fingerprintManager =
+                Utils.getFingerprintManagerOrNull(application);
+        if (fingerprintManager == null) {
+            return null;
+        }
+        return new FingerprintRepository(fingerprintManager);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
new file mode 100644
index 0000000..477fdb6
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 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.factory;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
+import androidx.lifecycle.viewmodel.CreationExtras;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
+import com.android.settings.overlay.FeatureFactory;
+
+/**
+ * View model factory for biometric enrollment fragment
+ */
+public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
+
+    private static final String TAG = "BiometricsViewModelFact";
+
+    public static final CreationExtras.Key<ChallengeGenerator> CHALLENGE_GENERATOR =
+            new CreationExtras.Key<>() {};
+
+    @NonNull
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends ViewModel> T create(@NonNull Class<T> modelClass,
+            @NonNull CreationExtras extras) {
+        final Application application = extras.get(AndroidViewModelFactory.APPLICATION_KEY);
+
+        if (application == null) {
+            Log.w(TAG, "create, null application");
+            return create(modelClass);
+        }
+        final FeatureFactory featureFactory = FeatureFactory.getFactory(application);
+        final BiometricsRepositoryProvider provider = FeatureFactory.getFactory(application)
+                .getBiometricsRepositoryProvider();
+
+        if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
+            final FingerprintRepository repository = provider.getFingerprintRepository(application);
+            if (repository != null) {
+                return (T) new FingerprintEnrollIntroViewModel(application, repository);
+            }
+        } else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
+            final FingerprintRepository repository = provider.getFingerprintRepository(application);
+            if (repository != null) {
+                return (T) new FingerprintEnrollmentViewModel(application, repository,
+                        application.getSystemService(KeyguardManager.class));
+            }
+        } else if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) {
+            final LockPatternUtils lockPatternUtils =
+                    featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application);
+            final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR);
+            if (challengeGenerator != null) {
+                return (T) new AutoCredentialViewModel(application, lockPatternUtils,
+                        challengeGenerator);
+            }
+        }
+        return create(modelClass);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/CredentialModel.java b/src/com/android/settings/biometrics2/ui/model/CredentialModel.java
new file mode 100644
index 0000000..06caf5e
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/CredentialModel.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2022 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.model;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
+
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.time.Clock;
+
+/**
+ * Secret credential data including
+ * 1. userId
+ * 2. sensorId
+ * 3. challenge
+ * 4. token
+ * 5. gkPwHandle
+ */
+public final class CredentialModel {
+
+    /**
+     * Default value for an invalid challenge
+     */
+    @VisibleForTesting
+    public static final long INVALID_CHALLENGE = -1L;
+
+    /**
+     * Default value if GkPwHandle is invalid.
+     */
+    public static final long INVALID_GK_PW_HANDLE = 0L;
+
+    /**
+     * Default value for a invalid sensor id
+     */
+    @VisibleForTesting
+    public static final int INVALID_SENSOR_ID = -1;
+
+    private final Clock mClock;
+
+    private final long mInitMillis;
+
+    private final int mUserId;
+
+    private int mSensorId;
+    @Nullable
+    private Long mUpdateSensorIdMillis = null;
+
+    private long mChallenge;
+    @Nullable
+    private Long mUpdateChallengeMillis = null;
+
+    @Nullable
+    private byte[] mToken;
+    @Nullable
+    private Long mUpdateTokenMillis = null;
+
+    private long mGkPwHandle;
+    @Nullable
+    private Long mClearGkPwHandleMillis = null;
+
+    public CredentialModel(@NonNull Intent intent, @NonNull Clock clock) {
+        mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
+        mSensorId = intent.getIntExtra(EXTRA_KEY_SENSOR_ID, INVALID_SENSOR_ID);
+        mChallenge = intent.getLongExtra(EXTRA_KEY_CHALLENGE, INVALID_CHALLENGE);
+        mToken = intent.getByteArrayExtra(EXTRA_KEY_CHALLENGE_TOKEN);
+        mGkPwHandle = intent.getLongExtra(EXTRA_KEY_GK_PW_HANDLE,
+                INVALID_GK_PW_HANDLE);
+        mClock = clock;
+        mInitMillis = mClock.millis();
+    }
+
+    /**
+     * Get userId for this credential
+     */
+    public int getUserId() {
+        return mUserId;
+    }
+
+    /**
+     * Check user id is valid or not
+     */
+    public static boolean isValidUserId(int userId) {
+        return userId != UserHandle.USER_NULL;
+    }
+
+    /**
+     * Get challenge
+     */
+    public long getChallenge() {
+        return mChallenge;
+    }
+
+    /**
+     * Set challenge
+     */
+    public void setChallenge(long value) {
+        mUpdateChallengeMillis = mClock.millis();
+        mChallenge = value;
+    }
+
+    /**
+     * Get challenge token
+     */
+    @Nullable
+    public byte[] getToken() {
+        return mToken;
+    }
+
+    /**
+     * Set challenge token
+     */
+    public void setToken(@Nullable byte[] value) {
+        mUpdateTokenMillis = mClock.millis();
+        mToken = value;
+    }
+
+    /**
+     * Check challengeToken is valid or not
+     */
+    public static boolean isValidToken(@Nullable byte[] token) {
+        return token != null;
+    }
+
+    /**
+     * Get gatekeeper password handle
+     */
+    public long getGkPwHandle() {
+        return mGkPwHandle;
+    }
+
+    /**
+     * Clear gatekeeper password handle data
+     */
+    public void clearGkPwHandle() {
+        mClearGkPwHandleMillis = mClock.millis();
+        mGkPwHandle = INVALID_GK_PW_HANDLE;
+    }
+
+    /**
+     * Check gkPwHandle is valid or not
+     */
+    public static boolean isValidGkPwHandle(long gkPwHandle) {
+        return gkPwHandle != INVALID_GK_PW_HANDLE;
+    }
+
+    /**
+     * Get sensor id
+     */
+    public int getSensorId() {
+        return mSensorId;
+    }
+
+    /**
+     * Set sensor id
+     */
+    public void setSensorId(int value) {
+        mUpdateSensorIdMillis = mClock.millis();
+        mSensorId = value;
+    }
+
+    /**
+     * Returns a string representation of the object
+     */
+    @Override
+    public String toString() {
+        final int gkPwHandleLen = ("" + mGkPwHandle).length();
+        final int tokenLen = mToken == null ? 0 : mToken.length;
+        final int challengeLen = ("" + mChallenge).length();
+        return getClass().getSimpleName() + ":{initMillis:" + mInitMillis
+                + ", userId:" + mUserId
+                + ", challenge:{len:" + challengeLen
+                + ", updateMillis:" + mUpdateChallengeMillis + "}"
+                + ", token:{len:" + tokenLen + ", isValid:" + isValidToken(mToken)
+                + ", updateMillis:" + mUpdateTokenMillis + "}"
+                + ", gkPwHandle:{len:" + gkPwHandleLen + ", isValid:"
+                + isValidGkPwHandle(mGkPwHandle) + ", clearMillis:" + mClearGkPwHandleMillis + "}"
+                + ", mSensorId:{id:" + mSensorId + ", updateMillis:" + mUpdateSensorIdMillis + "}"
+                + " }";
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java b/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java
new file mode 100644
index 0000000..de8526a
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 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.model;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.SetupWizardUtils;
+
+import com.google.android.setupcompat.util.WizardManagerHelper;
+
+/**
+ * Biometric enrollment generic intent data, which includes
+ * 1. isSuw
+ * 2. isAfterSuwOrSuwSuggestedAction
+ * 3. theme
+ * 4. isFromSettingsSummery
+ * 5. a helper method, getSetupWizardExtras
+ */
+public final class EnrollmentRequest {
+
+    private final boolean mIsSuw;
+    private final boolean mIsAfterSuwOrSuwSuggestedAction;
+    private final boolean mIsFromSettingsSummery;
+    private final int mTheme;
+    private final Bundle mSuwExtras;
+
+    public EnrollmentRequest(@NonNull Intent intent, @NonNull Context context) {
+        mIsSuw = WizardManagerHelper.isAnySetupWizard(intent);
+        mIsAfterSuwOrSuwSuggestedAction = WizardManagerHelper.isDeferredSetupWizard(intent)
+                || WizardManagerHelper.isPortalSetupWizard(intent)
+                || intent.getBooleanExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, false);
+        mSuwExtras = getSuwExtras(mIsSuw, intent);
+        mIsFromSettingsSummery = intent.getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false);
+        mTheme = SetupWizardUtils.getTheme(context, intent);
+    }
+
+    public boolean isSuw() {
+        return mIsSuw;
+    }
+
+    public boolean isAfterSuwOrSuwSuggestedAction() {
+        return mIsAfterSuwOrSuwSuggestedAction;
+    }
+
+    public boolean isFromSettingsSummery() {
+        return mIsFromSettingsSummery;
+    }
+
+    public int getTheme() {
+        return mTheme;
+    }
+
+    @NonNull
+    public Bundle getSuwExtras() {
+        return new Bundle(mSuwExtras);
+    }
+
+    /**
+     * Returns a string representation of the object
+     */
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + ":{isSuw:" + mIsSuw
+                + ", isAfterSuwOrSuwSuggestedAction:" + mIsAfterSuwOrSuwSuggestedAction
+                + ", isFromSettingsSummery:" + mIsFromSettingsSummery
+                + "}";
+    }
+
+    @NonNull
+    private static Bundle getSuwExtras(boolean isSuw, @NonNull Intent intent) {
+        final Intent toIntent = new Intent();
+        if (isSuw) {
+            SetupWizardUtils.copySetupExtras(intent, toIntent);
+        }
+        return toIntent.getExtras() != null ? toIntent.getExtras() : new Bundle();
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java b/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java
new file mode 100644
index 0000000..b5e462e
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 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.model;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fingerprint onboarding introduction page data, it contains following information which needs
+ * to be passed from view model to view.
+ * 1. mEnrollableStatus: User is allowed to enroll a new fingerprint or not.
+ * 2. mHasScrollToBottom: User has scrolled to the bottom of this page or not.
+ */
+public final class FingerprintEnrollIntroStatus {
+
+    /**
+     * Unconfirmed case, it means that this value is invalid, and view shall bypass this value.
+     */
+    public static final int FINGERPRINT_ENROLLABLE_UNKNOWN = -1;
+
+    /**
+     * User is allowed to enrolled a new fingerprint.
+     */
+    public static final int FINGERPRINT_ENROLLABLE_OK = 0;
+
+    /**
+     * User is not allowed to enrolled a new fingerprint because the number of enrolled fingerprint
+     * has reached maximum.
+     */
+    public static final int FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX = 1;
+
+    @IntDef(prefix = {"FINGERPRINT_ENROLLABLE_"}, value = {
+            FINGERPRINT_ENROLLABLE_UNKNOWN,
+            FINGERPRINT_ENROLLABLE_OK,
+            FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FingerprintEnrollableStatus {
+    }
+
+    private final boolean mHasScrollToBottom;
+
+    @FingerprintEnrollableStatus
+    private final int mEnrollableStatus;
+
+    public FingerprintEnrollIntroStatus(boolean hasScrollToBottom, int enrollableStatus) {
+        mEnrollableStatus = enrollableStatus;
+        mHasScrollToBottom = hasScrollToBottom;
+    }
+
+    /**
+     * Get enrollable status. It means that user is allowed to enroll a new fingerprint or not.
+     */
+    @FingerprintEnrollableStatus
+    public int getEnrollableStatus() {
+        return mEnrollableStatus;
+    }
+
+    /**
+     * Get info for this onboarding introduction page has scrolled to bottom or not
+     */
+    public boolean hasScrollToBottom() {
+        return mHasScrollToBottom;
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java
new file mode 100644
index 0000000..e788da5
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import static android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED;
+
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+import static com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyResourcesManager;
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.template.RequireScrollMixin;
+import com.google.android.setupdesign.util.DynamicColorPalette;
+
+/**
+ * Fingerprint intro onboarding page fragment implementation
+ */
+public class FingerprintEnrollIntroFragment extends Fragment {
+
+    private static final String TAG = "FingerprintEnrollIntroFragment";
+
+    @NonNull private final ViewModelProvider mViewModelProvider;
+    @Nullable private final DevicePolicyResourcesManager mDevicePolicyMgrRes;
+
+    private FingerprintEnrollIntroViewModel mViewModel = null;
+
+    private View mView = null;
+    private FooterButton mPrimaryFooterButton = null;
+    private FooterButton mSecondaryFooterButton = null;
+    private ImageView mIconShield = null;
+    private TextView mFooterMessage6 = null;
+    @Nullable private PorterDuffColorFilter mIconColorFilter;
+
+    public FingerprintEnrollIntroFragment(
+            @NonNull ViewModelProvider viewModelProvider,
+            @Nullable DevicePolicyResourcesManager devicePolicyMgrRes) {
+        super();
+        mViewModelProvider = viewModelProvider;
+        mDevicePolicyMgrRes = devicePolicyMgrRes;
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+
+        final Context context = inflater.getContext();
+        mView = inflater.inflate(R.layout.fingerprint_enroll_introduction, container);
+
+        final ImageView iconFingerprint = mView.findViewById(R.id.icon_fingerprint);
+        final ImageView iconDeviceLocked = mView.findViewById(R.id.icon_device_locked);
+        final ImageView iconTrashCan = mView.findViewById(R.id.icon_trash_can);
+        final ImageView iconInfo = mView.findViewById(R.id.icon_info);
+        mIconShield = mView.findViewById(R.id.icon_shield);
+        final ImageView iconLink = mView.findViewById(R.id.icon_link);
+        iconFingerprint.getDrawable().setColorFilter(getIconColorFilter(context));
+        iconDeviceLocked.getDrawable().setColorFilter(getIconColorFilter(context));
+        iconTrashCan.getDrawable().setColorFilter(getIconColorFilter(context));
+        iconInfo.getDrawable().setColorFilter(getIconColorFilter(context));
+        mIconShield.getDrawable().setColorFilter(getIconColorFilter(context));
+        iconLink.getDrawable().setColorFilter(getIconColorFilter(context));
+
+        final TextView footerMessage2 = mView.findViewById(R.id.footer_message_2);
+        final TextView footerMessage3 = mView.findViewById(R.id.footer_message_3);
+        final TextView footerMessage4 = mView.findViewById(R.id.footer_message_4);
+        final TextView footerMessage5 = mView.findViewById(R.id.footer_message_5);
+        mFooterMessage6 = mView.findViewById(R.id.footer_message_6);
+        footerMessage2.setText(
+                R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_2);
+        footerMessage3.setText(
+                R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_3);
+        footerMessage4.setText(
+                R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_4);
+        footerMessage5.setText(
+                R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5);
+        mFooterMessage6.setText(
+                R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_6);
+
+        final TextView footerTitle1 = mView.findViewById(R.id.footer_title_1);
+        final TextView footerTitle2 = mView.findViewById(R.id.footer_title_2);
+        footerTitle1.setText(
+                R.string.security_settings_fingerprint_enroll_introduction_footer_title_1);
+        footerTitle2.setText(
+                R.string.security_settings_fingerprint_enroll_introduction_footer_title_2);
+
+        return mView;
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        final Context context = view.getContext();
+
+        final TextView footerLink = mView.findViewById(R.id.footer_learn_more);
+        footerLink.setMovementMethod(LinkMovementMethod.getInstance());
+        final String footerLinkStr = getContext().getString(
+                R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more,
+                Html.FROM_HTML_MODE_LEGACY);
+        footerLink.setText(Html.fromHtml(footerLinkStr));
+
+        // footer buttons
+        mPrimaryFooterButton = new FooterButton.Builder(context)
+                .setText(R.string.security_settings_fingerprint_enroll_introduction_agree)
+                .setListener(mViewModel::onNextButtonClick)
+                .setButtonType(FooterButton.ButtonType.OPT_IN)
+                .setTheme(R.style.SudGlifButton_Primary)
+                .build();
+        mSecondaryFooterButton = new FooterButton.Builder(context)
+                .setListener(mViewModel::onSkipOrCancelButtonClick)
+                .setButtonType(FooterButton.ButtonType.NEXT)
+                .setTheme(R.style.SudGlifButton_Primary)
+                .build();
+        getFooterBarMixin().setPrimaryButton(mPrimaryFooterButton);
+        getFooterBarMixin().setSecondaryButton(mSecondaryFooterButton, true /* usePrimaryStyle */);
+
+        if (mViewModel.canAssumeUdfps()) {
+            mFooterMessage6.setVisibility(View.VISIBLE);
+            mIconShield.setVisibility(View.VISIBLE);
+        } else {
+            mFooterMessage6.setVisibility(View.GONE);
+            mIconShield.setVisibility(View.GONE);
+        }
+        mSecondaryFooterButton.setText(getContext(),
+                mViewModel.getEnrollmentRequest().isAfterSuwOrSuwSuggestedAction()
+                ? R.string.security_settings_fingerprint_enroll_introduction_cancel
+                : R.string.security_settings_fingerprint_enroll_introduction_no_thanks);
+
+        if (mViewModel.isBiometricUnlockDisabledByAdmin()
+                && !mViewModel.isParentalConsentRequired()) {
+            setHeaderText(
+                    getActivity(),
+                    R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled
+            );
+            getLayout().setDescriptionText(getDescriptionDisabledByAdmin(context));
+        } else {
+            setHeaderText(getActivity(),
+                    R.string.security_settings_fingerprint_enroll_introduction_title);
+        }
+
+        mViewModel.getPageStatusLiveData().observe(this, this::updateFooterButtons);
+
+        final RequireScrollMixin requireScrollMixin = getLayout()
+                .getMixin(RequireScrollMixin.class);
+        requireScrollMixin.requireScrollWithButton(getActivity(), mPrimaryFooterButton,
+                getMoreButtonTextRes(), mViewModel::onNextButtonClick);
+        requireScrollMixin.setOnRequireScrollStateChangedListener(scrollNeeded -> {
+            if (!scrollNeeded) {
+                mViewModel.setHasScrolledToBottom();
+            }
+        });
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        mViewModel = mViewModelProvider.get(FingerprintEnrollIntroViewModel.class);
+        getLifecycle().addObserver(mViewModel);
+        super.onAttach(context);
+    }
+
+    @Override
+    public void onDetach() {
+        getLifecycle().removeObserver(mViewModel);
+        super.onDetach();
+    }
+
+    @NonNull
+    private PorterDuffColorFilter getIconColorFilter(@NonNull Context context) {
+        if (mIconColorFilter == null) {
+            mIconColorFilter = new PorterDuffColorFilter(
+                    DynamicColorPalette.getColor(context, ACCENT),
+                    PorterDuff.Mode.SRC_IN);
+        }
+        return mIconColorFilter;
+    }
+
+    private GlifLayout getLayout() {
+        return mView.findViewById(R.id.setup_wizard_layout);
+    }
+
+    @NonNull
+    private FooterBarMixin getFooterBarMixin() {
+        final GlifLayout layout = getLayout();
+        return layout.getMixin(FooterBarMixin.class);
+    }
+
+    @NonNull
+    private String getDescriptionDisabledByAdmin(@NonNull Context context) {
+        final int defaultStrId =
+                R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled;
+        if (mDevicePolicyMgrRes == null) {
+            Log.w(TAG, "getDescriptionDisabledByAdmin, null device policy manager res");
+            return "";
+        }
+        return mDevicePolicyMgrRes.getString(FINGERPRINT_UNLOCK_DISABLED,
+                () -> context.getString(defaultStrId));
+    }
+
+    private void setHeaderText(@NonNull Activity activity, int resId) {
+        TextView layoutTitle = getLayout().getHeaderTextView();
+        CharSequence previousTitle = layoutTitle.getText();
+        CharSequence title = activity.getText(resId);
+        if (previousTitle != title) {
+            if (!TextUtils.isEmpty(previousTitle)) {
+                layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+            }
+            getLayout().setHeaderText(title);
+            getLayout().getHeaderTextView().setContentDescription(title);
+            activity.setTitle(title);
+        }
+        getLayout().getHeaderTextView().setContentDescription(activity.getText(resId));
+    }
+
+    void updateFooterButtons(@NonNull FingerprintEnrollIntroStatus status) {
+        @StringRes final int scrollToBottomPrimaryResId =
+                status.getEnrollableStatus() == FINGERPRINT_ENROLLABLE_OK
+                        ? R.string.security_settings_fingerprint_enroll_introduction_agree
+                        : R.string.done;
+
+        mPrimaryFooterButton.setText(getContext(),
+                status.hasScrollToBottom() ? scrollToBottomPrimaryResId : getMoreButtonTextRes());
+        mSecondaryFooterButton.setVisibility(
+                status.hasScrollToBottom() ? View.VISIBLE : View.INVISIBLE);
+
+        final TextView errorTextView = mView.findViewById(R.id.error_text);
+        switch (status.getEnrollableStatus()) {
+            case FINGERPRINT_ENROLLABLE_OK:
+                errorTextView.setText(null);
+                errorTextView.setVisibility(View.GONE);
+                break;
+            case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
+                errorTextView.setText(R.string.fingerprint_intro_error_max);
+                errorTextView.setVisibility(View.VISIBLE);
+                break;
+            case FINGERPRINT_ENROLLABLE_UNKNOWN:
+                // default case, do nothing.
+        }
+    }
+
+    @StringRes
+    private int getMoreButtonTextRes() {
+        return R.string.security_settings_face_enroll_introduction_more;
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
new file mode 100644
index 0000000..e9cf6fd
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2022 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 androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY;
+
+import static com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
+import androidx.lifecycle.viewmodel.MutableCreationExtras;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
+import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollEnrolling;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.factory.BiometricsFragmentFactory;
+import com.android.settings.biometrics2.factory.BiometricsViewModelFactory;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
+import com.android.settings.overlay.FeatureFactory;
+
+import com.google.android.setupdesign.util.ThemeHelper;
+
+/**
+ * Fingerprint enrollment activity implementation
+ */
+public class FingerprintEnrollmentActivity extends FragmentActivity {
+
+    private static final String TAG = "FingerprintEnrollmentActivity";
+
+    protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1;
+
+    private FingerprintEnrollmentViewModel mViewModel;
+    private AutoCredentialViewModel mAutoCredentialViewModel;
+    private ActivityResultLauncher<Intent> mNextActivityLauncher;
+    private ActivityResultLauncher<Intent> mChooseLockLauncher;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mNextActivityLauncher = registerForActivityResult(
+                new ActivityResultContracts.StartActivityForResult(),
+                (it) -> mViewModel.onContinueEnrollActivityResult(
+                        it,
+                        mAutoCredentialViewModel.getUserId())
+        );
+        mChooseLockLauncher = registerForActivityResult(
+                new ActivityResultContracts.StartActivityForResult(),
+                (it) -> onChooseOrConfirmLockResult(true, it)
+        );
+
+        ViewModelProvider viewModelProvider = new ViewModelProvider(this);
+
+        mViewModel = viewModelProvider.get(FingerprintEnrollmentViewModel.class);
+        mViewModel.setRequest(new EnrollmentRequest(getIntent(), getApplicationContext()));
+        mViewModel.setSavedInstanceState(savedInstanceState);
+        getLifecycle().addObserver(mViewModel);
+
+        mAutoCredentialViewModel = viewModelProvider.get(AutoCredentialViewModel.class);
+        mAutoCredentialViewModel.setCredentialModel(new CredentialModel(getIntent(),
+                SystemClock.elapsedRealtimeClock()));
+        getLifecycle().addObserver(mAutoCredentialViewModel);
+
+        mViewModel.getSetResultLiveData().observe(this, this::onSetActivityResult);
+        mAutoCredentialViewModel.getActionLiveData().observe(this, this::onCredentialAction);
+
+        // Theme
+        setTheme(mViewModel.getRequest().getTheme());
+        ThemeHelper.trySetDynamicColor(this);
+        getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT);
+
+        // fragment
+        setContentView(R.layout.biometric_enrollment_container);
+        final FragmentManager fragmentManager = getSupportFragmentManager();
+        fragmentManager.setFragmentFactory(
+                new BiometricsFragmentFactory(getApplication(), viewModelProvider));
+
+        final FingerprintEnrollIntroViewModel fingerprintEnrollIntroViewModel =
+                viewModelProvider.get(FingerprintEnrollIntroViewModel.class);
+        fingerprintEnrollIntroViewModel.setEnrollmentRequest(mViewModel.getRequest());
+        fingerprintEnrollIntroViewModel.setUserId(mAutoCredentialViewModel.getUserId());
+        fingerprintEnrollIntroViewModel.getActionLiveData().observe(
+                this, this::observeIntroAction);
+        final String tag = "FingerprintEnrollIntroFragment";
+        fragmentManager.beginTransaction()
+                .setReorderingAllowed(true)
+                .add(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, tag)
+                .commit();
+    }
+
+    private void onSetActivityResult(@NonNull ActivityResult result) {
+        setResult(mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()
+                        ? RESULT_CANCELED
+                        : result.getResultCode(),
+                result.getData());
+        finish();
+    }
+
+    private void onCredentialAction(@NonNull Integer action) {
+        switch (action) {
+            case CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK: {
+                final Intent intent = mAutoCredentialViewModel.getChooseLockIntent(this,
+                        mViewModel.getRequest().isSuw(), mViewModel.getRequest().getSuwExtras());
+                if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+                    Log.w(TAG, "chooseLock, fail to set isWaiting flag to true");
+                }
+                mChooseLockLauncher.launch(intent);
+                return;
+            }
+            case CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK: {
+                final boolean launched = mAutoCredentialViewModel.getConfirmLockLauncher(
+                        this,
+                        LAUNCH_CONFIRM_LOCK_ACTIVITY,
+                        getString(R.string.security_settings_fingerprint_preference_title)
+                ).launch();
+                if (!launched) {
+                    // This shouldn't happen, as we should only end up at this step if a lock thingy
+                    // is already set.
+                    Log.e(TAG, "confirmLock, launched is true");
+                    finish();
+                } else if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+                    Log.w(TAG, "confirmLock, fail to set isWaiting flag to true");
+                }
+                return;
+            }
+            case CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE: {
+                Log.w(TAG, "observeCredentialLiveData, finish with action:" + action);
+                if (mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()) {
+                    setResult(Activity.RESULT_CANCELED);
+                }
+                finish();
+            }
+        }
+    }
+
+    private void onChooseOrConfirmLockResult(boolean isChooseLock,
+            @NonNull ActivityResult activityResult) {
+        if (!mViewModel.isWaitingActivityResult().compareAndSet(true, false)) {
+            Log.w(TAG, "isChooseLock:" + isChooseLock + ", fail to unset waiting flag");
+        }
+        if (mAutoCredentialViewModel.checkNewCredentialFromActivityResult(
+                isChooseLock, activityResult)) {
+            overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
+        }
+    }
+
+    private void observeIntroAction(@NonNull Integer action) {
+        switch (action) {
+            case FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH: {
+                onSetActivityResult(
+                        new ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null));
+                return;
+            }
+            case FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL: {
+                onSetActivityResult(
+                        new ActivityResult(BiometricEnrollBase.RESULT_SKIP, null));
+                return;
+            }
+            case FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL: {
+                final boolean isSuw = mViewModel.getRequest().isSuw();
+                if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+                    Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag");
+                }
+                final Intent intent = new Intent(this, isSuw
+                        ? SetupFingerprintEnrollEnrolling.class
+                        : FingerprintEnrollFindSensor.class);
+                intent.putExtras(mAutoCredentialViewModel.getCredentialBundle());
+                intent.putExtras(mViewModel.getNextActivityBaseIntentExtras());
+                mNextActivityLauncher.launch(intent);
+            }
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mViewModel.checkFinishActivityDuringOnPause(isFinishing(), isChangingConfigurations());
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) {
+            onChooseOrConfirmLockResult(false, new ActivityResult(resultCode, data));
+            return;
+        }
+        super.onActivityResult(requestCode, resultCode, data);
+    }
+
+    @NonNull
+    @Override
+    public CreationExtras getDefaultViewModelCreationExtras() {
+        final Application application =
+                super.getDefaultViewModelCreationExtras().get(APPLICATION_KEY);
+        final MutableCreationExtras ret = new MutableCreationExtras();
+        ret.set(APPLICATION_KEY, application);
+        final FingerprintRepository repository = FeatureFactory.getFactory(application)
+                .getBiometricsRepositoryProvider().getFingerprintRepository(application);
+        ret.set(CHALLENGE_GENERATOR, new FingerprintChallengeGenerator(repository));
+        return ret;
+    }
+
+    @NonNull
+    @Override
+    public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
+        return new BiometricsViewModelFactory();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getWindow().setStatusBarColor(getBackgroundColor());
+    }
+
+    @ColorInt
+    private int getBackgroundColor() {
+        final ColorStateList stateList = Utils.getColorAttr(this, android.R.attr.windowBackground);
+        return stateList != null ? stateList.getDefaultColor() : Color.TRANSPARENT;
+    }
+
+    @Override
+    protected void onDestroy() {
+        getLifecycle().removeObserver(mViewModel);
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mViewModel.onSaveInstanceState(outState);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java
new file mode 100644
index 0000000..b1a7f90
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2022 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.viewmodel;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
+
+import android.annotation.IntDef;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.password.ChooseLockGeneric;
+import com.android.settings.password.ChooseLockPattern;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like
+ * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing.
+ */
+public class AutoCredentialViewModel extends AndroidViewModel implements DefaultLifecycleObserver {
+
+    private static final String TAG = "AutoCredentialViewModel";
+    private static final boolean DEBUG = true;
+
+    /**
+     * Need activity to run choose lock
+     */
+    public static final int CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK = 1;
+
+    /**
+     * Need activity to run confirm lock
+     */
+    public static final int CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK = 2;
+
+    /**
+     * Fail to use challenge from hardware generateChallenge(), shall finish activity with proper
+     * error code
+     */
+    public static final int CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE = 3;
+
+    @IntDef(prefix = { "CREDENTIAL_" }, value = {
+            CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK,
+            CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK,
+            CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CredentialAction {}
+
+    /**
+     * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge
+     */
+    public interface GenerateChallengeCallback {
+        /**
+         * Generic generateChallenge method for FingerprintManager or FaceManager
+         */
+        void onChallengeGenerated(int sensorId, int userId, long challenge);
+    }
+
+    /**
+     * A generic interface class for calling different generateChallenge from FingerprintManager or
+     * FaceManager
+     */
+    public interface ChallengeGenerator {
+        /**
+         * Get callback that will be called later after challenge generated
+         */
+        @Nullable
+        GenerateChallengeCallback getCallback();
+
+        /**
+         * Set callback that will be called later after challenge generated
+         */
+        void setCallback(@Nullable GenerateChallengeCallback callback);
+
+        /**
+         * Method for generating challenge from FingerprintManager or FaceManager
+         */
+        void generateChallenge(int userId);
+    }
+
+    /**
+     * Used to generate challenge through FingerprintRepository
+     */
+    public static class FingerprintChallengeGenerator implements ChallengeGenerator {
+
+        private static final String TAG = "FingerprintChallengeGenerator";
+
+        @NonNull
+        private final FingerprintRepository mFingerprintRepository;
+
+        @Nullable
+        private GenerateChallengeCallback mCallback = null;
+
+        public FingerprintChallengeGenerator(@NonNull FingerprintRepository fingerprintRepository) {
+            mFingerprintRepository = fingerprintRepository;
+        }
+
+        @Nullable
+        @Override
+        public GenerateChallengeCallback getCallback() {
+            return mCallback;
+        }
+
+        @Override
+        public void setCallback(@Nullable GenerateChallengeCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void generateChallenge(int userId) {
+            final GenerateChallengeCallback callback = mCallback;
+            if (callback == null) {
+                Log.e(TAG, "generateChallenge, null callback");
+                return;
+            }
+            mFingerprintRepository.generateChallenge(userId, callback::onChallengeGenerated);
+        }
+    }
+
+
+    @NonNull private final LockPatternUtils mLockPatternUtils;
+    @NonNull private final ChallengeGenerator mChallengeGenerator;
+    private CredentialModel mCredentialModel = null;
+    @NonNull private final MutableLiveData<Integer> mActionLiveData =
+            new MutableLiveData<>();
+
+    public AutoCredentialViewModel(
+            @NonNull Application application,
+            @NonNull LockPatternUtils lockPatternUtils,
+            @NonNull ChallengeGenerator challengeGenerator) {
+        super(application);
+        mLockPatternUtils = lockPatternUtils;
+        mChallengeGenerator = challengeGenerator;
+    }
+
+    public void setCredentialModel(@NonNull CredentialModel credentialModel) {
+        mCredentialModel = credentialModel;
+    }
+
+    /**
+     * Observe ActionLiveData for actions about choosing lock, confirming lock, or finishing
+     * activity
+     */
+    @NonNull
+    public LiveData<Integer> getActionLiveData() {
+        return mActionLiveData;
+    }
+
+    @Override
+    public void onCreate(@NonNull LifecycleOwner owner) {
+        checkCredential();
+    }
+
+    /**
+     * Check credential status for biometric enrollment.
+     */
+    private void checkCredential() {
+        if (isValidCredential()) {
+            return;
+        }
+        final long gkPwHandle = mCredentialModel.getGkPwHandle();
+        if (isUnspecifiedPassword()) {
+            mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
+        } else if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
+            generateChallenge(gkPwHandle);
+        } else {
+            mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+        }
+    }
+
+    private void generateChallenge(long gkPwHandle) {
+        mChallengeGenerator.setCallback((sensorId, userId, challenge) -> {
+            mCredentialModel.setSensorId(sensorId);
+            mCredentialModel.setChallenge(challenge);
+            try {
+                final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId);
+                mCredentialModel.setToken(newToken);
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "generateChallenge, IllegalStateException", e);
+                mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
+                return;
+            }
+
+            mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle);
+            mCredentialModel.clearGkPwHandle();
+
+            if (DEBUG) {
+                Log.d(TAG, "generateChallenge " + mCredentialModel);
+            }
+
+            // Check credential again
+            if (!isValidCredential()) {
+                Log.w(TAG, "generateChallenge, invalid Credential");
+                mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
+            }
+        });
+        mChallengeGenerator.generateChallenge(getUserId());
+    }
+
+    private boolean isValidCredential() {
+        return !isUnspecifiedPassword()
+                && CredentialModel.isValidToken(mCredentialModel.getToken());
+    }
+
+    private boolean isUnspecifiedPassword() {
+        return mLockPatternUtils.getActivePasswordQuality(getUserId())
+                == PASSWORD_QUALITY_UNSPECIFIED;
+    }
+
+    /**
+     * Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern
+     * @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is
+     *                     coming from ConfirmLockPassword or ConfirmLockPattern
+     * @param result activity result
+     * @return if it is a valid result
+     */
+    public boolean checkNewCredentialFromActivityResult(boolean isChooseLock,
+            @NonNull ActivityResult result) {
+        if ((isChooseLock && result.getResultCode() == ChooseLockPattern.RESULT_FINISHED)
+                || (!isChooseLock && result.getResultCode() == Activity.RESULT_OK)) {
+            final Intent data = result.getData();
+            if (data != null) {
+                final long gkPwHandle = result.getData().getLongExtra(
+                        EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE);
+                generateChallenge(gkPwHandle);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get userId for this credential
+     */
+    public int getUserId() {
+        return mCredentialModel.getUserId();
+    }
+
+    @Nullable
+    private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId)
+            throws IllegalStateException {
+        final VerifyCredentialResponse response = mLockPatternUtils
+                .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId);
+        if (!response.isMatched()) {
+            throw new IllegalStateException("Unable to request Gatekeeper HAT");
+        }
+        return response.getGatekeeperHAT();
+    }
+
+    /**
+     * Get Credential bundle which will be used to launch next activity.
+     */
+    @NonNull
+    public Bundle getCredentialBundle() {
+        final Bundle retBundle = new Bundle();
+        final long gkPwHandle = mCredentialModel.getGkPwHandle();
+        if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
+            retBundle.putLong(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+        }
+        final byte[] token = mCredentialModel.getToken();
+        if (CredentialModel.isValidToken(token)) {
+            retBundle.putByteArray(EXTRA_KEY_CHALLENGE_TOKEN, token);
+        }
+        final int userId = getUserId();
+        if (CredentialModel.isValidUserId(userId)) {
+            retBundle.putInt(Intent.EXTRA_USER_ID, userId);
+        }
+        retBundle.putLong(EXTRA_KEY_CHALLENGE, mCredentialModel.getChallenge());
+        retBundle.putInt(EXTRA_KEY_SENSOR_ID, mCredentialModel.getSensorId());
+        return retBundle;
+    }
+
+    /**
+     * Get Intent for choosing lock
+     */
+    @NonNull
+    public Intent getChooseLockIntent(@NonNull Context context, boolean isSuw,
+            @NonNull Bundle suwExtras) {
+        final Intent intent = BiometricUtils.getChooseLockIntent(context, isSuw,
+                suwExtras);
+        intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS,
+                true);
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true);
+
+        final int userId = getUserId();
+        if (CredentialModel.isValidUserId(userId)) {
+            intent.putExtra(Intent.EXTRA_USER_ID, userId);
+        }
+        return intent;
+    }
+
+    /**
+     * Get ConfirmLockLauncher
+     */
+    @NonNull
+    public ChooseLockSettingsHelper getConfirmLockLauncher(@NonNull Activity activity,
+            int requestCode, @NonNull String title) {
+        final ChooseLockSettingsHelper.Builder builder =
+                new ChooseLockSettingsHelper.Builder(activity);
+        builder.setRequestCode(requestCode)
+                .setTitle(title)
+                .setRequestGatekeeperPasswordHandle(true)
+                .setForegroundOnly(true)
+                .setReturnCredentials(true);
+
+        final int userId = mCredentialModel.getUserId();
+        if (CredentialModel.isValidUserId(userId)) {
+            builder.setUserId(userId);
+        }
+        return builder.build();
+    }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
new file mode 100644
index 0000000..252a508
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2022 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.viewmodel;
+
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+import android.annotation.IntDef;
+import android.app.Application;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fingerprint intro onboarding page view model implementation
+ */
+public class FingerprintEnrollIntroViewModel extends AndroidViewModel
+        implements DefaultLifecycleObserver {
+
+    private static final String TAG = "FingerprintEnrollIntroViewModel";
+    private static final boolean HAS_SCROLLED_TO_BOTTOM_DEFAULT = false;
+    private static final int ENROLLABLE_STATUS_DEFAULT = FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+    /**
+     * User clicks 'Done' button on this page
+     */
+    public static final int FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH = 0;
+
+    /**
+     * User clicks 'Agree' button on this page
+     */
+    public static final int FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL = 1;
+
+    /**
+     * User clicks 'Skip' button on this page
+     */
+    public static final int FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL = 2;
+
+    @IntDef(prefix = { "FINGERPRINT_ENROLL_INTRO_ACTION_" }, value = {
+            FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH,
+            FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL,
+            FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FingerprintEnrollIntroAction {}
+
+    @NonNull private final FingerprintRepository mFingerprintRepository;
+
+    private final MutableLiveData<Boolean> mHasScrolledToBottomLiveData =
+            new MutableLiveData<>(HAS_SCROLLED_TO_BOTTOM_DEFAULT);
+    private final MutableLiveData<Integer> mEnrollableStatusLiveData =
+            new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT);
+    private final MediatorLiveData<FingerprintEnrollIntroStatus> mPageStatusLiveData =
+            new MediatorLiveData<>();
+    private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
+    private int mUserId = UserHandle.myUserId();
+    private EnrollmentRequest mEnrollmentRequest = null;
+
+    public FingerprintEnrollIntroViewModel(@NonNull Application application,
+            @NonNull FingerprintRepository fingerprintRepository) {
+        super(application);
+        mFingerprintRepository = fingerprintRepository;
+
+        mPageStatusLiveData.addSource(
+                mEnrollableStatusLiveData,
+                enrollable -> {
+                    final Boolean toBottomValue = mHasScrolledToBottomLiveData.getValue();
+                    final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
+                            toBottomValue != null ? toBottomValue : HAS_SCROLLED_TO_BOTTOM_DEFAULT,
+                            enrollable);
+                    mPageStatusLiveData.setValue(status);
+                });
+        mPageStatusLiveData.addSource(
+                mHasScrolledToBottomLiveData,
+                hasScrolledToBottom -> {
+                    final Integer enrollableValue = mEnrollableStatusLiveData.getValue();
+                    final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
+                            hasScrolledToBottom,
+                            enrollableValue != null ? enrollableValue : ENROLLABLE_STATUS_DEFAULT);
+                    mPageStatusLiveData.setValue(status);
+                });
+    }
+
+    public void setUserId(int userId) {
+        mUserId = userId;
+    }
+
+    public void setEnrollmentRequest(@NonNull EnrollmentRequest enrollmentRequest) {
+        mEnrollmentRequest = enrollmentRequest;
+    }
+
+    /**
+     * Get enrollment request
+     */
+    public EnrollmentRequest getEnrollmentRequest() {
+        return mEnrollmentRequest;
+    }
+
+    private void updateEnrollableStatus() {
+        final int num = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+        final int max =
+                mEnrollmentRequest.isSuw() && !mEnrollmentRequest.isAfterSuwOrSuwSuggestedAction()
+                ? mFingerprintRepository.getMaxFingerprintsInSuw(getApplication().getResources())
+                : mFingerprintRepository.getMaxFingerprints();
+        mEnrollableStatusLiveData.postValue(num >= max
+                ? FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+                : FINGERPRINT_ENROLLABLE_OK);
+    }
+
+    /**
+     * Get enrollable status and hasScrollToBottom live data
+     */
+    public LiveData<FingerprintEnrollIntroStatus> getPageStatusLiveData() {
+        return mPageStatusLiveData;
+    }
+
+    /**
+     * Get user's action live data (like clicking Agree, Skip, or Done)
+     */
+    public LiveData<Integer> getActionLiveData() {
+        return mActionLiveData;
+    }
+
+    /**
+     * The first sensor type is UDFPS sensor or not
+     */
+    public boolean canAssumeUdfps() {
+        return mFingerprintRepository.canAssumeUdfps();
+    }
+
+    /**
+     * Update onboarding intro page has scrolled to bottom
+     */
+    public void setHasScrolledToBottom() {
+        mHasScrolledToBottomLiveData.postValue(true);
+    }
+
+    /**
+     * Get parental consent required or not during enrollment process
+     */
+    public boolean isParentalConsentRequired() {
+        return mFingerprintRepository.isParentalConsentRequired(getApplication());
+    }
+
+    /**
+     * Get fingerprint is disable by admin or not
+     */
+    public boolean isBiometricUnlockDisabledByAdmin() {
+        return mFingerprintRepository.isDisabledByAdmin(getApplication(), mUserId);
+    }
+
+    /**
+     * User clicks next button
+     */
+    public void onNextButtonClick(View ignoredView) {
+        final Integer status = mEnrollableStatusLiveData.getValue();
+        switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) {
+            case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
+                mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
+                break;
+            case FINGERPRINT_ENROLLABLE_OK:
+                mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
+                break;
+            default:
+                Log.w(TAG, "fail to click next, enrolled:" + status);
+        }
+    }
+
+    /**
+     * User clicks skip/cancel button
+     */
+    public void onSkipOrCancelButtonClick(View ignoredView) {
+        mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        updateEnrollableStatus();
+    }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java
new file mode 100644
index 0000000..468e132
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 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.viewmodel;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
+import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.password.SetupSkipDialog;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Fingerprint enrollment view model implementation
+ */
+public class FingerprintEnrollmentViewModel extends AndroidViewModel implements
+        DefaultLifecycleObserver {
+
+    private static final String TAG = "FingerprintEnrollmentViewModel";
+
+    @VisibleForTesting
+    static final String SAVED_STATE_IS_WAITING_ACTIVITY_RESULT = "is_waiting_activity_result";
+
+    @NonNull private final FingerprintRepository mFingerprintRepository;
+    @Nullable private final KeyguardManager mKeyguardManager;
+
+    private final AtomicBoolean mIsWaitingActivityResult = new AtomicBoolean(false);
+    private final MutableLiveData<ActivityResult> mSetResultLiveData = new MutableLiveData<>();
+
+    /**
+     * Even this variable may be nullable, but activity will call setIntent() immediately during
+     * its onCreate(), we do not assign @Nullable for this variable here.
+     */
+    private EnrollmentRequest mRequest = null;
+
+    public FingerprintEnrollmentViewModel(
+            @NonNull Application application,
+            @NonNull FingerprintRepository fingerprintRepository,
+            @Nullable KeyguardManager keyguardManager) {
+        super(application);
+        mFingerprintRepository = fingerprintRepository;
+        mKeyguardManager = keyguardManager;
+    }
+
+    /**
+     * Set EnrollmentRequest
+     */
+    public void setRequest(@NonNull EnrollmentRequest request) {
+        mRequest = request;
+    }
+
+    /**
+     * Get EnrollmentRequest
+     */
+    public EnrollmentRequest getRequest() {
+        return mRequest;
+    }
+
+    /**
+     * Copy necessary extra data from activity intent
+     */
+    @NonNull
+    public Bundle getNextActivityBaseIntentExtras() {
+        final Bundle bundle = mRequest.getSuwExtras();
+        bundle.putBoolean(EXTRA_FROM_SETTINGS_SUMMARY, mRequest.isFromSettingsSummery());
+        return bundle;
+    }
+
+    /**
+     * Handle activity result from FingerprintFindSensor
+     */
+    public void onContinueEnrollActivityResult(@NonNull ActivityResult result, int userId) {
+        if (mIsWaitingActivityResult.compareAndSet(true, false)) {
+            Log.w(TAG, "fail to reset isWaiting flag for enrollment");
+        }
+        if (result.getResultCode() == RESULT_FINISHED
+                || result.getResultCode() == RESULT_TIMEOUT) {
+            Intent data = result.getData();
+            if (mRequest.isSuw() && isKeyguardSecure()
+                    && result.getResultCode() == RESULT_FINISHED) {
+                if (data == null) {
+                    data = new Intent();
+                }
+                data.putExtras(getSuwFingerprintCountExtra(userId));
+            }
+            mSetResultLiveData.postValue(new ActivityResult(result.getResultCode(), data));
+        } else if (result.getResultCode() == RESULT_SKIP
+                || result.getResultCode() == SetupSkipDialog.RESULT_SKIP) {
+            mSetResultLiveData.postValue(result);
+        }
+    }
+
+
+
+    private boolean isKeyguardSecure() {
+        return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure();
+    }
+
+    /**
+     * Activity calls this method during onPause() to finish itself when back to background.
+     *
+     * @param isActivityFinishing Activity has called finish() or not
+     * @param isChangingConfigurations Activity is finished because of configuration changed or not.
+     */
+    public void checkFinishActivityDuringOnPause(boolean isActivityFinishing,
+            boolean isChangingConfigurations) {
+        if (isChangingConfigurations || isActivityFinishing || mRequest.isSuw()
+                || isWaitingActivityResult().get()) {
+            return;
+        }
+
+        mSetResultLiveData.postValue(new ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null));
+    }
+
+    @NonNull
+    private Bundle getSuwFingerprintCountExtra(int userId) {
+        final Bundle bundle = new Bundle();
+        bundle.putInt(EXTRA_FINGERPRINT_ENROLLED_COUNT,
+                mFingerprintRepository.getNumOfEnrolledFingerprintsSize(userId));
+        return bundle;
+    }
+
+    @NonNull
+    public LiveData<ActivityResult> getSetResultLiveData() {
+        return mSetResultLiveData;
+    }
+
+    @NonNull
+    public AtomicBoolean isWaitingActivityResult() {
+        return mIsWaitingActivityResult;
+    }
+
+    /**
+     * Handle savedInstanceState from activity onCreated()
+     */
+    public void setSavedInstanceState(@Nullable Bundle savedInstanceState) {
+        if (savedInstanceState == null) {
+            return;
+        }
+        mIsWaitingActivityResult.set(
+                savedInstanceState.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false)
+        );
+    }
+
+    /**
+     * Handle onSaveInstanceState from activity
+     */
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get());
+    }
+}
diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java
index cf8698c..5da9310 100644
--- a/src/com/android/settings/overlay/FeatureFactory.java
+++ b/src/com/android/settings/overlay/FeatureFactory.java
@@ -29,6 +29,7 @@
 import com.android.settings.applications.ApplicationFeatureProvider;
 import com.android.settings.aware.AwareFeatureProvider;
 import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
 import com.android.settings.bluetooth.BluetoothFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -162,6 +163,11 @@
     public abstract FaceFeatureProvider getFaceFeatureProvider();
 
     /**
+     * Gets implementation for Biometrics repository provider.
+     */
+    public abstract BiometricsRepositoryProvider getBiometricsRepositoryProvider();
+
+    /**
      * Gets implementation for the WifiTrackerLib.
      */
     public abstract WifiTrackerLibProvider getWifiTrackerLibProvider();
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java
index b779716..bc78f2e 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.java
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java
@@ -37,6 +37,8 @@
 import com.android.settings.aware.AwareFeatureProviderImpl;
 import com.android.settings.biometrics.face.FaceFeatureProvider;
 import com.android.settings.biometrics.face.FaceFeatureProviderImpl;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl;
 import com.android.settings.bluetooth.BluetoothFeatureProvider;
 import com.android.settings.bluetooth.BluetoothFeatureProviderImpl;
 import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl;
@@ -104,6 +106,7 @@
     private BluetoothFeatureProvider mBluetoothFeatureProvider;
     private AwareFeatureProvider mAwareFeatureProvider;
     private FaceFeatureProvider mFaceFeatureProvider;
+    private BiometricsRepositoryProvider mBiometricsRepositoryProvider;
     private WifiTrackerLibProvider mWifiTrackerLibProvider;
     private SecuritySettingsFeatureProvider mSecuritySettingsFeatureProvider;
     private AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider;
@@ -306,6 +309,14 @@
     }
 
     @Override
+    public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+        if (mBiometricsRepositoryProvider == null) {
+            mBiometricsRepositoryProvider = new BiometricsRepositoryProviderImpl();
+        }
+        return mBiometricsRepositoryProvider;
+    }
+
+    @Override
     public WifiTrackerLibProvider getWifiTrackerLibProvider() {
         if (mWifiTrackerLibProvider == null) {
             mWifiTrackerLibProvider = new WifiTrackerLibProviderImpl();
diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
index b87d983..c1e7bfb 100644
--- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -27,6 +27,7 @@
 import com.android.settings.applications.ApplicationFeatureProvider;
 import com.android.settings.aware.AwareFeatureProvider;
 import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
 import com.android.settings.bluetooth.BluetoothFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -78,6 +79,7 @@
     public final BluetoothFeatureProvider mBluetoothFeatureProvider;
     public final AwareFeatureProvider mAwareFeatureProvider;
     public final FaceFeatureProvider mFaceFeatureProvider;
+    public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
 
     public PanelFeatureProvider panelFeatureProvider;
     public SlicesFeatureProvider slicesFeatureProvider;
@@ -134,6 +136,7 @@
         mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
         mAwareFeatureProvider = mock(AwareFeatureProvider.class);
         mFaceFeatureProvider = mock(FaceFeatureProvider.class);
+        mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
         wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
         securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
         mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -257,6 +260,11 @@
     }
 
     @Override
+    public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+        return mBiometricsRepositoryProvider;
+    }
+
+    @Override
     public WifiTrackerLibProvider getWifiTrackerLibProvider() {
         return wifiTrackerLibProvider;
     }
diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
index 7a93f11..054b415 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
@@ -23,6 +23,7 @@
 import com.android.settings.applications.ApplicationFeatureProvider
 import com.android.settings.aware.AwareFeatureProvider
 import com.android.settings.biometrics.face.FaceFeatureProvider
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
 import com.android.settings.bluetooth.BluetoothFeatureProvider
 import com.android.settings.dashboard.DashboardFeatureProvider
 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider
@@ -153,6 +154,10 @@
         TODO("Not yet implemented")
     }
 
+    override fun getBiometricsRepositoryProvider(): BiometricsRepositoryProvider {
+        TODO("Not yet implemented")
+    }
+
     override fun getWifiTrackerLibProvider(): WifiTrackerLibProvider {
         TODO("Not yet implemented")
     }
diff --git a/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java
new file mode 100644
index 0000000..e5920f3
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 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.data.repository;
+
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_HOME_BUTTON;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UNKNOWN;
+
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintRepositoryTest {
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock private Resources mResources;
+    @Mock private FingerprintManager mFingerprintManager;
+
+    private Context mContext;
+    private FingerprintRepository mFingerprintRepository;
+
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+    }
+
+    @Test
+    public void testCanAssumeSensorType() {
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 1);
+        assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
+        assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
+        assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 1);
+        assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_POWER_BUTTON, 1);
+        assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_HOME_BUTTON, 1);
+        assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+    }
+
+    @Test
+    public void testGetMaxFingerprints() {
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 44);
+        assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(44);
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 999);
+        assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(999);
+    }
+
+    @Test
+    public void testGetNumOfEnrolledFingerprintsSize() {
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, 10, 3);
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, 22, 99);
+
+        assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(10)).isEqualTo(3);
+        assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(22)).isEqualTo(99);
+    }
+
+    @Test
+    public void testGetMaxFingerprintsInSuw() {
+        setupSuwMaxFingerprintsEnrollable(mContext, mResources, 333);
+        assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources))
+                .isEqualTo(333);
+
+        setupSuwMaxFingerprintsEnrollable(mContext, mResources, 20);
+        assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources)).isEqualTo(20);
+    }
+
+    public static void setupSuwMaxFingerprintsEnrollable(
+            @NonNull Context context,
+            @NonNull Resources mockedResources,
+            int numOfFp) {
+        final int resId = ResourcesUtils.getResourcesId(context, "integer",
+                "suw_max_fingerprints_enrollable");
+        when(mockedResources.getInteger(resId)).thenReturn(numOfFp);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java
new file mode 100644
index 0000000..7a13875
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2022 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.viewmodel;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_CHALLENGE;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CredentialAction;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.GenerateChallengeCallback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.password.ChooseLockPattern;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class AutoCredentialViewModelTest {
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+    @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+    @Mock private LifecycleOwner mLifecycleOwner;
+    @Mock private LockPatternUtils mLockPatternUtils;
+    private TestChallengeGenerator mChallengeGenerator = null;
+    private AutoCredentialViewModel mAutoCredentialViewModel;
+
+    @Before
+    public void setUp() {
+        mChallengeGenerator = new TestChallengeGenerator();
+        mAutoCredentialViewModel = new AutoCredentialViewModel(
+                ApplicationProvider.getApplicationContext(),
+                mLockPatternUtils,
+                mChallengeGenerator);
+    }
+
+    private CredentialModel newCredentialModel(int userId, long challenge,
+            @Nullable byte[] token, long gkPwHandle) {
+        final Intent intent = new Intent();
+        intent.putExtra(Intent.EXTRA_USER_ID, userId);
+        intent.putExtra(EXTRA_KEY_SENSOR_ID, 1);
+        intent.putExtra(EXTRA_KEY_CHALLENGE, challenge);
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+        return new CredentialModel(intent, SystemClock.elapsedRealtimeClock());
+    }
+
+    private CredentialModel newValidTokenCredentialModel(int userId) {
+        return newCredentialModel(userId, 1L, new byte[] { 0 }, 0L);
+    }
+
+    private CredentialModel newInvalidChallengeCredentialModel(int userId) {
+        return newCredentialModel(userId, INVALID_CHALLENGE, null, 0L);
+    }
+
+    private CredentialModel newGkPwHandleCredentialModel(int userId, long gkPwHandle) {
+        return newCredentialModel(userId, INVALID_CHALLENGE, null, gkPwHandle);
+    }
+
+    private void verifyNothingHappen() {
+        assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+    }
+
+    private void verifyOnlyActionLiveData(@CredentialAction int action) {
+        final Integer value = mAutoCredentialViewModel.getActionLiveData().getValue();
+        assertThat(value).isEqualTo(action);
+    }
+
+    private void setupGenerateTokenFlow(long gkPwHandle, int userId, int newSensorId,
+            long newChallenge) {
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_SOMETHING);
+        mChallengeGenerator.mUserId = userId;
+        mChallengeGenerator.mSensorId = newSensorId;
+        mChallengeGenerator.mChallenge = newChallenge;
+        when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId))
+                .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 }));
+    }
+
+    @Test
+    public void checkCredential_validCredentialCase() {
+        final int userId = 99;
+        mAutoCredentialViewModel.setCredentialModel(newValidTokenCredentialModel(userId));
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_SOMETHING);
+
+        // Run credential check
+        mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+        verifyNothingHappen();
+    }
+
+    @Test
+    public void checkCredential_needToChooseLock() {
+        final int userId = 100;
+        mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_UNSPECIFIED);
+
+        // Run credential check
+        mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+        verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
+    }
+
+    @Test
+    public void checkCredential_needToConfirmLockFoSomething() {
+        final int userId = 101;
+        mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_SOMETHING);
+
+        // Run credential check
+        mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+        verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+    }
+
+    @Test
+    public void checkCredential_needToConfirmLockForNumeric() {
+        final int userId = 102;
+        mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_NUMERIC);
+
+        // Run credential check
+        mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+        verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+    }
+
+    @Test
+    public void checkCredential_needToConfirmLockForAlphabetic() {
+        final int userId = 103;
+        mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_ALPHABETIC);
+
+        // Run credential check
+        mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+        verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+    }
+
+    @Test
+    public void checkCredential_generateChallenge() {
+        final int userId = 104;
+        final long gkPwHandle = 1111L;
+        final CredentialModel credentialModel = newGkPwHandleCredentialModel(userId, gkPwHandle);
+        mAutoCredentialViewModel.setCredentialModel(credentialModel);
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_SOMETHING);
+
+        final int newSensorId = 10;
+        final long newChallenge = 20L;
+        setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+
+        // Run credential check
+        mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+        assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+        assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+        assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+        assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+        assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+        assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+    }
+
+    @Test
+    public void testGetUserId() {
+        final int userId = 106;
+        mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+
+        // Get userId
+        assertThat(mAutoCredentialViewModel.getUserId()).isEqualTo(userId);
+    }
+
+    @Test
+    public void testCheckNewCredentialFromActivityResult_invalidChooseLock() {
+        final int userId = 107;
+        final long gkPwHandle = 3333L;
+        mAutoCredentialViewModel.setCredentialModel(
+                newGkPwHandleCredentialModel(userId, gkPwHandle));
+        final Intent intent = new Intent();
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+        // run checkNewCredentialFromActivityResult()
+        final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+                new ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent));
+
+        assertThat(ret).isFalse();
+        verifyNothingHappen();
+    }
+
+    @Test
+    public void testCheckNewCredentialFromActivityResult_invalidConfirmLock() {
+        final int userId = 107;
+        final long gkPwHandle = 3333L;
+        mAutoCredentialViewModel.setCredentialModel(
+                newGkPwHandleCredentialModel(userId, gkPwHandle));
+        final Intent intent = new Intent();
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+        // run checkNewCredentialFromActivityResult()
+        final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+                new ActivityResult(Activity.RESULT_OK + 1, intent));
+
+        assertThat(ret).isFalse();
+        verifyNothingHappen();
+    }
+
+    @Test
+    public void testCheckNewCredentialFromActivityResult_nullDataChooseLock() {
+        final int userId = 108;
+        final long gkPwHandle = 4444L;
+        mAutoCredentialViewModel.setCredentialModel(
+                newGkPwHandleCredentialModel(userId, gkPwHandle));
+
+        // run checkNewCredentialFromActivityResult()
+        final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+                new ActivityResult(ChooseLockPattern.RESULT_FINISHED, null));
+
+        assertThat(ret).isFalse();
+        verifyNothingHappen();
+    }
+
+    @Test
+    public void testCheckNewCredentialFromActivityResult_nullDataConfirmLock() {
+        final int userId = 109;
+        mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+
+        // run checkNewCredentialFromActivityResult()
+        final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+                new ActivityResult(Activity.RESULT_OK, null));
+
+        assertThat(ret).isFalse();
+        verifyNothingHappen();
+    }
+
+    @Test
+    public void testCheckNewCredentialFromActivityResult_validChooseLock() {
+        final int userId = 108;
+        final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
+        mAutoCredentialViewModel.setCredentialModel(credentialModel);
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_SOMETHING);
+
+        final long gkPwHandle = 6666L;
+        final int newSensorId = 50;
+        final long newChallenge = 60L;
+        setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+        final Intent intent = new Intent();
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+        // Run checkNewCredentialFromActivityResult()
+        final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+                new ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent));
+
+        assertThat(ret).isTrue();
+        assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+        assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+        assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+        assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+        assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+        assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+    }
+
+
+    @Test
+    public void testCheckNewCredentialFromActivityResult_validConfirmLock() {
+        final int userId = 109;
+        final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
+        mAutoCredentialViewModel.setCredentialModel(credentialModel);
+        when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+                PASSWORD_QUALITY_SOMETHING);
+
+        final long gkPwHandle = 5555L;
+        final int newSensorId = 80;
+        final long newChallenge = 90L;
+        setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+        final Intent intent = new Intent();
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+        // Run checkNewCredentialFromActivityResult()
+        final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+                new ActivityResult(Activity.RESULT_OK, intent));
+
+        assertThat(ret).isTrue();
+        assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+        assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+        assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+        assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+        assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+        assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+    }
+
+    public static class TestChallengeGenerator implements ChallengeGenerator {
+        public int mSensorId = INVALID_SENSOR_ID;
+        public int mUserId = UserHandle.myUserId();
+        public long mChallenge = INVALID_CHALLENGE;
+        public int mCallbackRunCount = 0;
+        private GenerateChallengeCallback mCallback;
+
+        @Nullable
+        @Override
+        public GenerateChallengeCallback getCallback() {
+            return mCallback;
+        }
+
+        @Override
+        public void setCallback(@Nullable GenerateChallengeCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void generateChallenge(int userId) {
+            final GenerateChallengeCallback callback = mCallback;
+            if (callback == null) {
+                return;
+            }
+            callback.onChallengeGenerated(mSensorId, mUserId, mChallenge);
+            ++mCallbackRunCount;
+        }
+    }
+
+    private VerifyCredentialResponse newGoodCredential(long gkPwHandle, @NonNull byte[] hat) {
+        return new VerifyCredentialResponse.Builder()
+                .setGatekeeperPasswordHandle(gkPwHandle)
+                .setGatekeeperHAT(hat)
+                .build();
+    }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java
new file mode 100644
index 0000000..5069ea1
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 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.viewmodel;
+
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
+
+import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupSuwMaxFingerprintsEnrollable;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwDeferredRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwPortalRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwSuggestedActionFlowRequest;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.content.res.Resources;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintEnrollIntroViewModelTest {
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+    @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+    @Mock private Resources mResources;
+    @Mock private LifecycleOwner mLifecycleOwner;
+    @Mock private FingerprintManager mFingerprintManager;
+
+    private Application mApplication;
+    private FingerprintRepository mFingerprintRepository;
+    private FingerprintEnrollIntroViewModel mViewModel;
+
+    @Before
+    public void setUp() {
+        mApplication = ApplicationProvider.getApplicationContext();
+        mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+        mViewModel = new FingerprintEnrollIntroViewModel(mApplication, mFingerprintRepository);
+        // MediatorLiveData won't update itself unless observed
+        mViewModel.getPageStatusLiveData().observeForever(event -> {});
+    }
+
+    @Test
+    public void testPageStatusLiveDataDefaultValue() {
+        final FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.hasScrollToBottom()).isFalse();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_UNKNOWN);
+    }
+
+    @Test
+    public void testGetEnrollmentRequest() {
+        final EnrollmentRequest request = newAllFalseRequest(mApplication);
+
+        mViewModel.setEnrollmentRequest(request);
+
+        assertThat(mViewModel.getEnrollmentRequest()).isEqualTo(request);
+    }
+
+    @Test
+    public void testOnStartToUpdateEnrollableStatus_isSuw() {
+        final int userId = 44;
+        mViewModel.setUserId(userId);
+        mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+        mViewModel.onStart(mLifecycleOwner);
+        FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
+        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+        mViewModel.onStart(mLifecycleOwner);
+        status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+    }
+
+    @Test
+    public void testOnStartToUpdateEnrollableStatus_isNotSuw() {
+        testOnStartToUpdateEnrollableStatus(newAllFalseRequest(mApplication));
+    }
+
+    @Test
+    public void testOnStartToUpdateEnrollableStatus_isSuwDeferred() {
+        testOnStartToUpdateEnrollableStatus(newIsSuwDeferredRequest(mApplication));
+    }
+
+    @Test
+    public void testOnStartToUpdateEnrollableStatus_isSuwPortal() {
+        testOnStartToUpdateEnrollableStatus(newIsSuwPortalRequest(mApplication));
+    }
+
+    @Test
+    public void testOnStartToUpdateEnrollableStatus_isSuwSuggestedActionFlow() {
+        testOnStartToUpdateEnrollableStatus(newIsSuwSuggestedActionFlowRequest(mApplication));
+    }
+
+    private void testOnStartToUpdateEnrollableStatus(@NonNull EnrollmentRequest request) {
+        final int userId = 45;
+        mViewModel.setUserId(userId);
+        mViewModel.setEnrollmentRequest(request);
+
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
+        mViewModel.onStart(mLifecycleOwner);
+        FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 5);
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
+        mViewModel.onStart(mLifecycleOwner);
+        status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+    }
+
+    @Test
+    public void textCanAssumeUdfps() {
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
+        assertThat(mViewModel.canAssumeUdfps()).isEqualTo(true);
+
+        setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
+        assertThat(mViewModel.canAssumeUdfps()).isEqualTo(false);
+    }
+
+    @Test
+    public void testIsParentalConsentRequired() {
+        // We shall not mock FingerprintRepository, but
+        // FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
+        // mock static method
+        final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
+        mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
+
+        when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(true);
+        assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(true);
+
+        when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(false);
+        assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(false);
+    }
+
+    @Test
+    public void testIsBiometricUnlockDisabledByAdmin() {
+        // We shall not mock FingerprintRepository, but
+        // FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
+        // static method
+        final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
+        mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
+
+        final int userId = 33;
+        mViewModel.setUserId(userId);
+
+        when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(true);
+        assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(true);
+
+        when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(false);
+        assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(false);
+    }
+
+    @Test
+    public void testSetHasScrolledToBottom() {
+        mViewModel.setHasScrolledToBottom();
+
+        FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+
+        assertThat(status.hasScrollToBottom()).isEqualTo(true);
+    }
+
+    @Test
+    public void testOnNextButtonClick_enrollNext() {
+        final int userId = 46;
+        mViewModel.setUserId(userId);
+        mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+        // Set latest status to FINGERPRINT_ENROLLABLE_OK
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+        mViewModel.onStart(mLifecycleOwner);
+        FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+        // Perform click on `next`
+        mViewModel.onNextButtonClick(null);
+
+        assertThat(mViewModel.getActionLiveData().getValue())
+                .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
+    }
+
+    @Test
+    public void testOnNextButtonClick_doneAndFinish() {
+        final int userId = 46;
+        mViewModel.setUserId(userId);
+        mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+        // Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
+        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+        mViewModel.onStart(mLifecycleOwner);
+        FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+
+        // Perform click on `next`
+        mViewModel.onNextButtonClick(null);
+
+        assertThat(mViewModel.getActionLiveData().getValue())
+                .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
+    }
+
+    @Test
+    public void testOnSkipOrCancelButtonClick() {
+        mViewModel.onSkipOrCancelButtonClick(null);
+
+        assertThat(mViewModel.getActionLiveData().getValue())
+                .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java
new file mode 100644
index 0000000..b1d55aa
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2022 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.viewmodel;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
+import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT;
+import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel.SAVED_STATE_IS_WAITING_ACTIVITY_RESULT;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest;
+import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResult;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.password.SetupSkipDialog;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintEnrollmentViewModelTest {
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+    @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+    @Mock private FingerprintManager mFingerprintManager;
+    @Mock private KeyguardManager mKeyguardManager;
+
+    private Application mApplication;
+    private FingerprintRepository mFingerprintRepository;
+    private FingerprintEnrollmentViewModel mViewModel;
+
+    @Before
+    public void setUp() {
+        mApplication = ApplicationProvider.getApplicationContext();
+        mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+        mViewModel = new FingerprintEnrollmentViewModel(mApplication, mFingerprintRepository,
+                mKeyguardManager);
+    }
+
+    @Test
+    public void testGetRequest() {
+        when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+        assertThat(mViewModel.getRequest()).isNull();
+
+        final EnrollmentRequest request = newAllFalseRequest(mApplication);
+        mViewModel.setRequest(request);
+        assertThat(mViewModel.getRequest()).isEqualTo(request);
+    }
+
+    @Test
+    public void testGetNextActivityBaseIntentExtras() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        assertThat(mViewModel.getNextActivityBaseIntentExtras()).isNotNull();
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelaySkip1Result() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        final ActivityResult result = new ActivityResult(RESULT_SKIP, null);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, 100);
+
+        assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelaySkip2Result() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        final ActivityResult result = new ActivityResult(SetupSkipDialog.RESULT_SKIP, null);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, 100);
+
+        assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelayNullDataTimeoutResult() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, null);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, 100);
+        final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+        assertThat(setResult).isNotNull();
+        assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+        assertThat(setResult.getData()).isEqualTo(result.getData());
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelayWithDataTimeoutResult() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        final Intent intent = new Intent("testAction");
+        intent.putExtra("testKey", "testValue");
+        final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, intent);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, 100);
+        final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+        assertThat(setResult).isNotNull();
+        assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+        assertThat(setResult.getData()).isEqualTo(intent);
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResult() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, 100);
+        final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+        assertThat(setResult).isNotNull();
+        assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+        assertThat(setResult.getData()).isEqualTo(result.getData());
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResult() {
+        mViewModel.setRequest(newAllFalseRequest(mApplication));
+        final Intent intent = new Intent("testAction");
+        intent.putExtra("testKey", "testValue");
+        final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, 100);
+        final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+        assertThat(setResult).isNotNull();
+        assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+        assertThat(setResult.getData()).isEqualTo(intent);
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResultAsNewData() {
+        when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+        final int userId = 111;
+        final int numOfFp = 4;
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
+        mViewModel.setRequest(newIsSuwRequest(mApplication));
+        final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, userId);
+        final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+        assertThat(setResult).isNotNull();
+        assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+        assertThat(setResult.getData()).isNotNull();
+        assertThat(setResult.getData().getExtras()).isNotNull();
+        assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
+                .isEqualTo(numOfFp);
+    }
+
+    @Test
+    public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResultAsNewData() {
+        when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+        final int userId = 20;
+        final int numOfFp = 9;
+        setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
+        mViewModel.setRequest(newIsSuwRequest(mApplication));
+        final String action = "testAction";
+        final String key = "testKey";
+        final String value = "testValue";
+        final Intent intent = new Intent(action);
+        intent.putExtra(key, value);
+        final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
+
+        // Run onContinueEnrollActivityResult
+        mViewModel.onContinueEnrollActivityResult(result, userId);
+        final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+        assertThat(setResult).isNotNull();
+        assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+        assertThat(setResult.getData()).isNotNull();
+        assertThat(setResult.getData().getExtras()).isNotNull();
+        assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
+                .isEqualTo(numOfFp);
+        assertThat(setResult.getData().getExtras().getString(key)).isEqualTo(value);
+    }
+
+    @Test
+    public void testSetSavedInstanceState() {
+        final Bundle bundle = new Bundle();
+        mViewModel.isWaitingActivityResult().set(true);
+
+        // setSavedInstanceState() as false
+        bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false);
+        mViewModel.setSavedInstanceState(bundle);
+        assertThat(mViewModel.isWaitingActivityResult().get()).isFalse();
+
+        // setSavedInstanceState() as false
+        bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, true);
+        mViewModel.setSavedInstanceState(bundle);
+        assertThat(mViewModel.isWaitingActivityResult().get()).isTrue();
+    }
+
+    @Test
+    public void testOnSaveInstanceState() {
+        final Bundle bundle = new Bundle();
+
+        // setSavedInstanceState() as false
+        mViewModel.isWaitingActivityResult().set(false);
+        mViewModel.onSaveInstanceState(bundle);
+        assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isFalse();
+
+        // setSavedInstanceState() as false
+        mViewModel.isWaitingActivityResult().set(true);
+        mViewModel.onSaveInstanceState(bundle);
+        assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isTrue();
+    }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java
new file mode 100644
index 0000000..5977c57a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 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.util;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_FIRST_RUN;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_PORTAL_SETUP;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_THEME;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+
+public class EnrollmentRequestUtil {
+
+    @NonNull
+    public static EnrollmentRequest newAllFalseRequest(@NonNull Context context) {
+        return newRequest(context, false, false, false, false, false, false, null);
+    }
+
+    @NonNull
+    public static EnrollmentRequest newIsSuwRequest(@NonNull Context context) {
+        return newRequest(context, true, false, false, false, false, false, null);
+    }
+
+    @NonNull
+    public static EnrollmentRequest newIsSuwDeferredRequest(@NonNull Context context) {
+        return newRequest(context, true, true, false, false, false, false, null);
+    }
+
+    @NonNull
+    public static EnrollmentRequest newIsSuwPortalRequest(@NonNull Context context) {
+        return newRequest(context, true, false, true, false, false, false, null);
+    }
+
+    @NonNull
+    public static EnrollmentRequest newIsSuwSuggestedActionFlowRequest(
+            @NonNull Context context) {
+        return newRequest(context, true, false, false, true, false, false, null);
+    }
+
+    @NonNull
+    public static EnrollmentRequest newRequest(@NonNull Context context, boolean isSuw,
+            boolean isSuwDeferred, boolean isSuwPortal, boolean isSuwSuggestedActionFlow,
+            boolean isSuwFirstRun, boolean isFromSettingsSummery, String theme) {
+        Intent i = new Intent();
+        i.putExtra(EXTRA_IS_SETUP_FLOW, isSuw);
+        i.putExtra(EXTRA_IS_DEFERRED_SETUP, isSuwDeferred);
+        i.putExtra(EXTRA_IS_PORTAL_SETUP, isSuwPortal);
+        i.putExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, isSuwSuggestedActionFlow);
+        i.putExtra(EXTRA_IS_FIRST_RUN, isSuwFirstRun);
+        i.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, isFromSettingsSummery);
+        if (!TextUtils.isEmpty(theme)) {
+            i.putExtra(EXTRA_THEME, theme);
+        }
+        return new EnrollmentRequest(i, context);
+    }
+
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java
new file mode 100644
index 0000000..cb45fa4
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.util;
+
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.SensorProperties;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+
+public class FingerprintManagerUtil {
+
+    public static void setupFingerprintFirstSensor(
+            @NonNull FingerprintManager mockedFingerprintManager,
+            @FingerprintSensorProperties.SensorType int sensorType,
+            int maxEnrollmentsPerUser) {
+        final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+        props.add(new FingerprintSensorPropertiesInternal(
+                0 /* sensorId */,
+                SensorProperties.STRENGTH_STRONG,
+                maxEnrollmentsPerUser,
+                new ArrayList<>() /* componentInfo */,
+                sensorType,
+                true /* resetLockoutRequiresHardwareAuthToken */));
+        when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
+    }
+
+    public static void setupFingerprintEnrolledFingerprints(
+            @NonNull FingerprintManager mockedFingerprintManager,
+            int userId,
+            int enrolledFingerprints) {
+        final ArrayList<Fingerprint> ret = new ArrayList<>();
+        for (int i = 0; i < enrolledFingerprints; ++i) {
+            ret.add(new Fingerprint("name", 0, 0, 0L));
+        }
+        when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
index 8f57f4e..d4127d7 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -25,6 +25,7 @@
 import com.android.settings.applications.ApplicationFeatureProvider;
 import com.android.settings.aware.AwareFeatureProvider;
 import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
 import com.android.settings.bluetooth.BluetoothFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -73,6 +74,7 @@
     public final BluetoothFeatureProvider mBluetoothFeatureProvider;
     public final AwareFeatureProvider mAwareFeatureProvider;
     public final FaceFeatureProvider mFaceFeatureProvider;
+    public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
 
     public PanelFeatureProvider panelFeatureProvider;
     public SlicesFeatureProvider slicesFeatureProvider;
@@ -120,6 +122,7 @@
         mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
         mAwareFeatureProvider = mock(AwareFeatureProvider.class);
         mFaceFeatureProvider = mock(FaceFeatureProvider.class);
+        mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
         wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
         securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
         mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -243,6 +246,11 @@
     }
 
     @Override
+    public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+        return mBiometricsRepositoryProvider;
+    }
+
+    @Override
     public WifiTrackerLibProvider getWifiTrackerLibProvider() {
         return wifiTrackerLibProvider;
     }
diff --git a/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java b/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java
new file mode 100644
index 0000000..0a45571
--- /dev/null
+++ b/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 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.testutils;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
+ * different one which executes each task synchronously.
+ *
+ * We can't refer it in prebuilt androidX library.
+ * Copied it from androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
+ */
+public class InstantTaskExecutorRule extends TestWatcher {
+    @Override
+    protected void starting(Description description) {
+        super.starting(description);
+        ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+            @Override
+            public void executeOnDiskIO(Runnable runnable) {
+                runnable.run();
+            }
+
+            @Override
+            public void postToMainThread(Runnable runnable) {
+                runnable.run();
+            }
+
+            @Override
+            public boolean isMainThread() {
+                return true;
+            }
+        });
+    }
+
+    @Override
+    protected void finished(Description description) {
+        super.finished(description);
+        ArchTaskExecutor.getInstance().setDelegate(null);
+    }
+}