[BiometricsV2] Refactor EnrollIntroViewModel

1. Refactor FingerprintEnrollIntroViewModel to kotlin and replace
   LiveData as Flow
2. Set importantForAccessibility to sud_scroll_view (porting solution of
   b/244595576 into fingerprint enrollment v2)

Bug: 286198097
Test: atest -m FingerprintEnrollIntroViewModelTest
Test: atest -m FingerprintEnrollmentActivityTest
Test: atest -m biometrics-enrollment-test
Change-Id: Idd4e9d77d040d7efd61342284d7b6a493a20a539
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt
index 2ba1df1..e7e1cc8 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt
@@ -28,12 +28,15 @@
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
+import android.widget.ScrollView
 import android.widget.TextView
 import androidx.annotation.StringRes
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.LiveData
+import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
 import com.android.settings.R
 import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus
 import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
@@ -47,6 +50,8 @@
 import com.google.android.setupdesign.util.DeviceHelper
 import com.google.android.setupdesign.util.DynamicColorPalette
 import com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
 import java.util.function.Supplier
 
 /**
@@ -59,12 +64,7 @@
 
     private var _viewModel: FingerprintEnrollIntroViewModel? = null
     private val viewModel: FingerprintEnrollIntroViewModel
-        get() {
-            if (_viewModel == null) {
-                _viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
-            }
-            return _viewModel!!
-        }
+        get() = _viewModel!!
 
     private var introView: GlifLayout? = null
 
@@ -73,10 +73,18 @@
     private var secondaryFooterButton: FooterButton? = null
 
     private val onNextClickListener =
-        View.OnClickListener { _: View? -> viewModel.onNextButtonClick() }
+        View.OnClickListener { _: View? ->
+            activity?.lifecycleScope?.let {
+                viewModel.onNextButtonClick(it)
+            }
+        }
 
     private val onSkipOrCancelClickListener =
-        View.OnClickListener { _: View? -> viewModel.onSkipOrCancelButtonClick() }
+        View.OnClickListener { _: View? ->
+            activity?.lifecycleScope?.let {
+                viewModel.onSkipOrCancelButtonClick(it)
+            }
+        }
 
     override fun onCreateView(
         inflater: LayoutInflater,
@@ -95,7 +103,7 @@
         super.onViewCreated(view, savedInstanceState)
         requireActivity().bindFingerprintEnrollIntroView(
             view = introView!!,
-            canAssumeUdfps = viewModel.canAssumeUdfps(),
+            canAssumeUdfps = viewModel.canAssumeUdfps,
             isBiometricUnlockDisabledByAdmin = viewModel.isBiometricUnlockDisabledByAdmin,
             isParentalConsentRequired = viewModel.isParentalConsentRequired,
             descriptionDisabledByAdminSupplier = { getDescriptionDisabledByAdmin(view.context) }
@@ -105,9 +113,10 @@
     override fun onStart() {
         val context: Context = requireContext()
         val footerBarMixin: FooterBarMixin = footerBarMixin
+        viewModel.updateEnrollableStatus(lifecycleScope)
         initPrimaryFooterButton(context, footerBarMixin)
         initSecondaryFooterButton(context, footerBarMixin)
-        observePageStatusLiveDataIfNeed()
+        collectPageStatusFlowIfNeed()
         super.onStart()
     }
 
@@ -152,46 +161,41 @@
             }
     }
 
-    private fun observePageStatusLiveDataIfNeed() {
-        val statusLiveData: LiveData<FingerprintEnrollIntroStatus> =
-            viewModel.pageStatusLiveData
-        val status: FingerprintEnrollIntroStatus? = statusLiveData.value
-
-        if (DEBUG) {
-            Log.e(
-                TAG, "observePageStatusLiveDataIfNeed() requireScrollWithButton, status:"
-                        + status
-            )
-        }
-
-        if (status != null && (status.hasScrollToBottom()
-                    || status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
-        ) {
-            // Update once and do not requireScrollWithButton() again when page has scrolled to
-            // bottom or User has enrolled at least a fingerprint, because if we
-            // requireScrollWithButton() again, primary button will become "More" after scrolling.
-            updateFooterButtons(status)
-            return
-        }
-
-        introView!!.getMixin(RequireScrollMixin::class.java).let {
-            it.requireScrollWithButton(
-                requireActivity(),
-                primaryFooterButton!!,
-                moreButtonTextRes,
-                onNextClickListener
-            )
-            it.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean ->
-                viewModel.setHasScrolledToBottom(!scrollNeeded)
+    private fun collectPageStatusFlowIfNeed() {
+        lifecycleScope.launch {
+            val status = viewModel.pageStatusFlow.first()
+            Log.d(TAG, "collectPageStatusFlowIfNeed status:$status")
+            if (status.hasScrollToBottom()
+                || status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+            ) {
+                // Update once and do not requireScrollWithButton() again when page has
+                // scrolled to bottom or User has enrolled at least a fingerprint, because if
+                // we requireScrollWithButton() again, primary button will become "More" after
+                // scrolling.
+                updateFooterButtons(status)
+            } else {
+                introView!!.getMixin(RequireScrollMixin::class.java).let {
+                    it.requireScrollWithButton(
+                        requireActivity(),
+                        primaryFooterButton!!,
+                        moreButtonTextRes,
+                        onNextClickListener
+                    )
+                    it.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean ->
+                        viewModel.setHasScrolledToBottom(!scrollNeeded, lifecycleScope)
+                    }
+                }
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.pageStatusFlow.collect(
+                        this@FingerprintEnrollIntroFragment::updateFooterButtons
+                    )
+                }
             }
         }
-        statusLiveData.observe(this) { newStatus: FingerprintEnrollIntroStatus ->
-            updateFooterButtons(newStatus)
-        }
     }
 
     override fun onAttach(context: Context) {
-        _viewModel = null
+        _viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
         super.onAttach(context)
     }
 
@@ -319,4 +323,7 @@
             )
         )
     }
+
+    view.findViewById<ScrollView>(R.id.sud_scroll_view)?.importantForAccessibility =
+        View.IMPORTANT_FOR_ACCESSIBILITY_YES
 }
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt
index 562b7dd..c295bb3 100644
--- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt
@@ -68,11 +68,8 @@
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FingerprintEnrollFinishAction
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel
-import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL
-import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH
-import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
-import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FingerprintEnrollIntroAction
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintErrorDialogSetResultAction.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH
@@ -129,13 +126,6 @@
 
     private var isFirstFragmentAdded = false
 
-    private val introActionObserver: Observer<Int> = Observer<Int> { action ->
-        if (DEBUG) {
-            Log.d(TAG, "introActionObserver($action)")
-        }
-        action?.let { onIntroAction(it) }
-    }
-
     private val findSensorActionObserver: Observer<Int> = Observer<Int> { action ->
         if (DEBUG) {
             Log.d(TAG, "findSensorActionObserver($action)")
@@ -290,12 +280,10 @@
         if (request.isSkipIntro || request.isSkipFindSensor) {
             return
         }
-        introViewModel.let {
-            // Clear ActionLiveData in FragmentViewModel to prevent getting previous action during
-            // recreate, like press 'Agree' then press 'back' in FingerprintEnrollFindSensor
-            // activity.
-            it.clearActionLiveData()
-            it.actionLiveData.observe(this, introActionObserver)
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                introViewModel.actionFlow.collect(this@FingerprintEnrollmentActivity::onIntroAction)
+            }
         }
     }
 
@@ -480,23 +468,20 @@
         }
     }
 
-    private fun onIntroAction(@FingerprintEnrollIntroAction action: Int) {
+    private fun onIntroAction(action: FingerprintEnrollIntroAction) {
+        Log.d(TAG, "onIntroAction($action)")
         when (action) {
-            FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH -> {
-                onSetActivityResult(
-                    ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null)
-                )
+            FingerprintEnrollIntroAction.DONE_AND_FINISH -> {
+                onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null))
                 return
             }
 
-            FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL -> {
-                onSetActivityResult(
-                    ActivityResult(BiometricEnrollBase.RESULT_SKIP, null)
-                )
+            FingerprintEnrollIntroAction.SKIP_OR_CANCEL -> {
+                onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_SKIP, null))
                 return
             }
 
-            FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL -> {
+            FingerprintEnrollIntroAction.CONTINUE_ENROLL -> {
                 startFindSensorFragment()
             }
         }
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
deleted file mode 100644
index 5e9085a..0000000
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * 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.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
-import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK;
-import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN;
-
-import android.annotation.IntDef;
-import android.app.Application;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
-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 com.android.settings.biometrics2.ui.model.FingerprintEnrollable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Fingerprint intro onboarding page view model implementation
- */
-public class FingerprintEnrollIntroViewModel extends AndroidViewModel {
-
-    private static final String TAG = "FingerprintEnrollIntroViewModel";
-    private static final boolean HAS_SCROLLED_TO_BOTTOM_DEFAULT = false;
-    private static final FingerprintEnrollable 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<FingerprintEnrollable> mEnrollableStatusLiveData =
-            new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT);
-    private final MediatorLiveData<FingerprintEnrollIntroStatus> mPageStatusLiveData =
-            new MediatorLiveData<>();
-    private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
-    private final int mUserId;
-    @NonNull private final EnrollmentRequest mRequest;
-
-    public FingerprintEnrollIntroViewModel(@NonNull Application application,
-            @NonNull FingerprintRepository fingerprintRepository,
-            @NonNull EnrollmentRequest request, int userId) {
-        super(application);
-        mFingerprintRepository = fingerprintRepository;
-        mRequest = request;
-        mUserId = userId;
-
-        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 FingerprintEnrollable enrollableValue =
-                            mEnrollableStatusLiveData.getValue();
-                    final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
-                            hasScrolledToBottom,
-                            enrollableValue != null ? enrollableValue : ENROLLABLE_STATUS_DEFAULT);
-                    mPageStatusLiveData.setValue(status);
-                });
-    }
-
-    /**
-     * Get enrollment request
-     */
-    public EnrollmentRequest getRequest() {
-        return mRequest;
-    }
-
-    private void updateEnrollableStatus() {
-        final int num = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
-        final int max =
-                mRequest.isSuw() && !mRequest.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() {
-        updateEnrollableStatus();
-        return mPageStatusLiveData;
-    }
-
-    /**
-     * Clear user's action live data
-     */
-    public void clearActionLiveData() {
-        mActionLiveData.setValue(null);
-    }
-
-    /**
-     * 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(boolean value) {
-        mHasScrolledToBottomLiveData.postValue(value);
-    }
-
-    /**
-     * 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() {
-        final FingerprintEnrollable 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() {
-        mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
-    }
-}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.kt b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.kt
new file mode 100644
index 0000000..98137b4
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.biometrics2.ui.viewmodel
+
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+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.biometrics2.ui.model.FingerprintEnrollable
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.CONTINUE_ENROLL
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.DONE_AND_FINISH
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.SKIP_OR_CANCEL
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Fingerprint intro onboarding page view model implementation */
+class FingerprintEnrollIntroViewModel(
+    application: Application,
+    private val fingerprintRepository: FingerprintRepository,
+    val request: EnrollmentRequest,
+    private val userId: Int
+) : AndroidViewModel(application) {
+
+    /** User's action flow (like clicking Agree, Skip, or Done) */
+    private val _actionFlow = MutableSharedFlow<FingerprintEnrollIntroAction>()
+    val actionFlow: SharedFlow<FingerprintEnrollIntroAction>
+        get() = _actionFlow.asSharedFlow()
+
+    private fun getEnrollableStatus(): FingerprintEnrollable {
+        val num = fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId)
+        val max =
+            if (request.isSuw && !request.isAfterSuwOrSuwSuggestedAction)
+                fingerprintRepository.getMaxFingerprintsInSuw(
+                    getApplication<Application>().resources
+                )
+            else
+                fingerprintRepository.maxFingerprints
+        return if (num >= max)
+            FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+        else
+            FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
+    }
+
+    private val hasScrolledToBottomFlow = MutableStateFlow(HAS_SCROLLED_TO_BOTTOM_DEFAULT)
+    private val enrollableStatusFlow = MutableStateFlow(getEnrollableStatus())
+
+    /** Enrollable status and hasScrollToBottom live data */
+    val pageStatusFlow: Flow<FingerprintEnrollIntroStatus> =
+        hasScrolledToBottomFlow.combine(enrollableStatusFlow) {
+            hasScrolledToBottom: Boolean, enrollableStatus: FingerprintEnrollable ->
+            FingerprintEnrollIntroStatus(hasScrolledToBottom, enrollableStatus)
+        }
+
+    fun updateEnrollableStatus(scope: CoroutineScope) {
+        scope.launch {
+            enrollableStatusFlow.emit(getEnrollableStatus())
+        }
+    }
+
+    /** The first sensor type is UDFPS sensor or not */
+    val canAssumeUdfps: Boolean
+        get() = fingerprintRepository.canAssumeUdfps()
+
+    /** Update onboarding intro page has scrolled to bottom */
+    fun setHasScrolledToBottom(value: Boolean, scope: CoroutineScope) {
+        scope.launch {
+            hasScrolledToBottomFlow.emit(value)
+        }
+    }
+
+    /** Get parental consent required or not during enrollment process */
+    val isParentalConsentRequired: Boolean
+        get() = fingerprintRepository.isParentalConsentRequired(getApplication())
+
+    /** Get fingerprint is disable by admin or not */
+    val isBiometricUnlockDisabledByAdmin: Boolean
+        get() = fingerprintRepository.isDisabledByAdmin(getApplication(), userId)
+
+    /**
+     * User clicks next button
+     */
+    fun onNextButtonClick(scope: CoroutineScope) {
+        scope.launch {
+            when (val status = enrollableStatusFlow.value) {
+                FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX ->
+                    _actionFlow.emit(DONE_AND_FINISH)
+
+                FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK ->
+                    _actionFlow.emit(CONTINUE_ENROLL)
+
+                else -> Log.w(TAG, "fail to click next, enrolled:$status")
+            }
+        }
+    }
+
+    /** User clicks skip/cancel button */
+    fun onSkipOrCancelButtonClick(scope: CoroutineScope) {
+        scope.launch {
+            _actionFlow.emit(SKIP_OR_CANCEL)
+        }
+    }
+
+    companion object {
+        private const val TAG = "FingerprintEnrollIntroViewModel"
+        private const val HAS_SCROLLED_TO_BOTTOM_DEFAULT = false
+        private val ENROLLABLE_STATUS_DEFAULT = FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN
+    }
+}
+
+enum class FingerprintEnrollIntroAction {
+    /** User clicks 'Done' button on this page */
+    DONE_AND_FINISH,
+    /** User clicks 'Agree' button on this page */
+    CONTINUE_ENROLL,
+    /** User clicks 'Skip' button on this page */
+    SKIP_OR_CANCEL
+}
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
deleted file mode 100644
index 12b860b..0000000
--- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.biometrics2.ui.viewmodel;
-
-import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
-
-import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
-import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK;
-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.utils.EnrollmentRequestUtils.newAllFalseRequest;
-import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwDeferredRequest;
-import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwPortalRequest;
-import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwRequest;
-import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwSuggestedActionFlowRequest;
-import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository;
-import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints;
-import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupSuwMaxFingerprintsEnrollable;
-
-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.MutableLiveData;
-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 {
-
-    private static final int TEST_USER_ID = 33;
-
-    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-    @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
-
-    @Mock private Resources mResources;
-    @Mock private FingerprintManager mFingerprintManager;
-
-    private Application mApplication;
-
-    private FingerprintEnrollIntroViewModel newFingerprintEnrollIntroViewModel(
-            @NonNull FingerprintRepository fingerprintRepository,
-            @NonNull EnrollmentRequest enrollmentRequest) {
-        final FingerprintEnrollIntroViewModel viewModel =
-                new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository,
-                        enrollmentRequest, TEST_USER_ID);
-        // MediatorLiveData won't update itself unless observed
-        viewModel.getPageStatusLiveData().observeForever(event -> {});
-        return viewModel;
-    }
-
-    @Before
-    public void setUp() {
-        mApplication = ApplicationProvider.getApplicationContext();
-    }
-
-    @Test
-    public void testPageStatusLiveDataDefaultValue() {
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newAllFalseRequest(mApplication));
-        final FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.hasScrollToBottom()).isFalse();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
-    }
-
-    @Test
-    public void testPageStatusLiveDataRefreshWhenRefetch() {
-        final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
-                TYPE_UDFPS_OPTICAL, 1);
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                repository,
-                newAllFalseRequest(mApplication));
-        FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.hasScrollToBottom()).isFalse();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
-
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 1);
-
-        // Refetch PageStatusLiveData
-        status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.hasScrollToBottom()).isFalse();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
-    }
-
-    @Test
-    public void testClearActionLiveData() {
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newAllFalseRequest(mApplication));
-
-        final MutableLiveData<Integer> actionLiveData =
-                (MutableLiveData<Integer>) viewModel.getActionLiveData();
-        actionLiveData.postValue(1);
-        assertThat(actionLiveData.getValue()).isEqualTo(1);
-
-        viewModel.clearActionLiveData();
-
-        assertThat(actionLiveData.getValue()).isNull();
-    }
-
-    @Test
-    public void testGetEnrollmentRequest() {
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newAllFalseRequest(mApplication));
-
-        assertThat(viewModel.getRequest()).isNotNull();
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusOk_isSuw() {
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 0);
-        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
-
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newIsSuwRequest(mApplication));
-        final FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusReachMax_isSuw() {
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 1);
-        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
-
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newIsSuwRequest(mApplication));
-        final FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusOk_isNotSuw() {
-        testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() {
-        testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() {
-        testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() {
-        testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusOk_isSuwPortal() {
-        testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() {
-        testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() {
-        testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(mApplication));
-    }
-
-    @Test
-    public void testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() {
-        testOnStartToUpdateEnrollableStatusReachMax(
-                newIsSuwSuggestedActionFlowRequest(mApplication));
-    }
-
-    private void testOnStartToUpdateEnrollableStatusOk(@NonNull EnrollmentRequest request) {
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 0);
-
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                request);
-        FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
-    }
-
-    private void testOnStartToUpdateEnrollableStatusReachMax(@NonNull EnrollmentRequest request) {
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 5);
-
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                request);
-        FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
-    }
-
-    @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);
-        final FingerprintEnrollIntroViewModel viewModel = new FingerprintEnrollIntroViewModel(
-                mApplication, fingerprintRepository, newAllFalseRequest(mApplication),
-                TEST_USER_ID);
-
-        when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(true);
-        assertThat(viewModel.isParentalConsentRequired()).isEqualTo(true);
-
-        when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(false);
-        assertThat(viewModel.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);
-        final FingerprintEnrollIntroViewModel viewModel = new FingerprintEnrollIntroViewModel(
-                mApplication, fingerprintRepository, newAllFalseRequest(mApplication),
-                TEST_USER_ID);
-
-        when(fingerprintRepository.isDisabledByAdmin(mApplication, TEST_USER_ID)).thenReturn(true);
-        assertThat(viewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(true);
-
-        when(fingerprintRepository.isDisabledByAdmin(mApplication, TEST_USER_ID)).thenReturn(false);
-        assertThat(viewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(false);
-    }
-
-    @Test
-    public void testSetHasScrolledToBottom() {
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newAllFalseRequest(mApplication));
-
-        viewModel.setHasScrolledToBottom(true);
-        FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.hasScrollToBottom()).isEqualTo(true);
-
-        viewModel.setHasScrolledToBottom(false);
-        status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.hasScrollToBottom()).isEqualTo(false);
-    }
-
-    @Test
-    public void testOnNextButtonClick_enrollNext() {
-        // Set latest status to FINGERPRINT_ENROLLABLE_OK
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 0);
-        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
-
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newIsSuwRequest(mApplication));
-        FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
-
-        // Perform click on `next`
-        viewModel.onNextButtonClick();
-
-        assertThat(viewModel.getActionLiveData().getValue())
-                .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
-    }
-
-    @Test
-    public void testOnNextButtonClick_doneAndFinish() {
-        // Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
-        setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 1);
-        setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
-
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newIsSuwRequest(mApplication));
-        FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
-        assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
-
-        // Perform click on `next`
-        viewModel.onNextButtonClick();
-
-        assertThat(viewModel.getActionLiveData().getValue())
-                .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
-    }
-
-    @Test
-    public void testOnSkipOrCancelButtonClick() {
-        final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
-                newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
-                newAllFalseRequest(mApplication));
-
-        viewModel.onSkipOrCancelButtonClick();
-
-        assertThat(viewModel.getActionLiveData().getValue())
-                .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
-    }
-}
diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.kt b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.kt
new file mode 100644
index 0000000..08e5ac3
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.kt
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.biometrics2.ui.viewmodel
+
+import android.app.Application
+import android.content.res.Resources
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
+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.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.CONTINUE_ENROLL
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.DONE_AND_FINISH
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.SKIP_OR_CANCEL
+import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newAllFalseRequest
+import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwDeferredRequest
+import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwPortalRequest
+import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwRequest
+import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwSuggestedActionFlowRequest
+import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository
+import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints
+import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupSuwMaxFingerprintsEnrollable
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidJUnit4::class)
+class FingerprintEnrollIntroViewModelTest {
+
+    @get:Rule val mockito = MockitoJUnit.rule()
+
+    @Mock private lateinit var resources: Resources
+    @Mock private lateinit var fingerprintManager: FingerprintManager
+
+    private var application: Application = ApplicationProvider.getApplicationContext()
+
+    private fun newFingerprintEnrollIntroViewModel(
+        fingerprintRepository: FingerprintRepository,
+        enrollmentRequest: EnrollmentRequest
+    ) = FingerprintEnrollIntroViewModel(
+        application,
+        fingerprintRepository,
+        enrollmentRequest,
+        TEST_USER_ID
+    )
+
+    @Before
+    fun setUp() {
+        application = ApplicationProvider.getApplicationContext()
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testPageStatusFlowDefaultAndUpdate() = runTest {
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 1),
+            newAllFalseRequest(application)
+        )
+
+        val statusList = listOfPageStatusFlow(viewModel)
+
+        runCurrent()
+
+        // assert default values
+        assertThat(statusList.size).isEqualTo(1)
+        assertThat(statusList[0].hasScrollToBottom()).isFalse()
+        assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_OK)
+
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 1)
+        viewModel.updateEnrollableStatus(backgroundScope)
+        runCurrent()
+
+        // assert new updated value
+        assertThat(statusList.size).isEqualTo(2)
+        assertThat(statusList[1].hasScrollToBottom()).isFalse()
+        assertThat(statusList[1].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun testOnStartToUpdateEnrollableStatusOk_isSuw() = runTest {
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 0)
+        setupSuwMaxFingerprintsEnrollable(application, resources, 1)
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            newIsSuwRequest(application)
+        )
+
+        val statusList = listOfPageStatusFlow(viewModel)
+
+        runCurrent()
+
+        assertThat(statusList.size).isEqualTo(1)
+        assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_OK)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusReachMax_isSuw() = runTest {
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 1)
+        setupSuwMaxFingerprintsEnrollable(application, resources, 1)
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            newIsSuwRequest(application)
+        )
+
+        val statusList = listOfPageStatusFlow(viewModel)
+
+        runCurrent()
+
+        assertThat(statusList.size).isEqualTo(1)
+        assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusOk_isNotSuw() = runTest {
+        testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() = runTest {
+        testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() = runTest {
+        testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() = runTest {
+        testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusOk_isSuwPortal() = runTest {
+        testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() = runTest {
+        testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() = runTest {
+        testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(application))
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() = runTest {
+        testOnStartToUpdateEnrollableStatusReachMax(
+            newIsSuwSuggestedActionFlowRequest(application)
+        )
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun TestScope.testOnStartToUpdateEnrollableStatusOk(request: EnrollmentRequest) {
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 0)
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            request
+        )
+
+        val statusList = listOfPageStatusFlow(viewModel)
+
+        runCurrent()
+
+        assertThat(statusList.size).isEqualTo(1)
+        assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_OK)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun TestScope.testOnStartToUpdateEnrollableStatusReachMax(request: EnrollmentRequest) {
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 5)
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            request
+        )
+
+        val statusList = listOfPageStatusFlow(viewModel)
+
+        runCurrent()
+
+        assertThat(statusList.size).isEqualTo(1)
+        assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
+    }
+
+    @Test
+    fun testIsParentalConsentRequired() {
+        // We shall not mock FingerprintRepository, but
+        // FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
+        // mock static method
+        val fingerprintRepository = Mockito.mock(
+            FingerprintRepository::class.java
+        )
+        val viewModel = FingerprintEnrollIntroViewModel(
+            application,
+            fingerprintRepository,
+            newAllFalseRequest(application),
+            TEST_USER_ID
+        )
+        Mockito.`when`(
+            fingerprintRepository.isParentalConsentRequired(application)
+        ).thenReturn(true)
+        assertThat(viewModel.isParentalConsentRequired).isEqualTo(true)
+        Mockito.`when`(
+            fingerprintRepository.isParentalConsentRequired(application)
+        ).thenReturn(false)
+        assertThat(viewModel.isParentalConsentRequired).isEqualTo(false)
+    }
+
+    @Test
+    fun testIsBiometricUnlockDisabledByAdmin() {
+        // We shall not mock FingerprintRepository, but
+        // FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
+        // static method
+        val fingerprintRepository = Mockito.mock(FingerprintRepository::class.java)
+        val viewModel = FingerprintEnrollIntroViewModel(
+            application,
+            fingerprintRepository,
+            newAllFalseRequest(application),
+            TEST_USER_ID
+        )
+        Mockito.`when`(
+            fingerprintRepository.isDisabledByAdmin(application, TEST_USER_ID)
+        ).thenReturn(true)
+        assertThat(viewModel.isBiometricUnlockDisabledByAdmin).isEqualTo(true)
+        Mockito.`when`(
+            fingerprintRepository.isDisabledByAdmin(application, TEST_USER_ID)
+        ).thenReturn(false)
+        assertThat(viewModel.isBiometricUnlockDisabledByAdmin).isEqualTo(false)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testSetHasScrolledToBottom() = runTest {
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            newAllFalseRequest(application)
+        )
+
+        val pageStatusList = listOfPageStatusFlow(viewModel)
+
+        viewModel.setHasScrolledToBottom(true, backgroundScope)
+        runCurrent()
+
+        assertThat(pageStatusList[pageStatusList.size-1].hasScrollToBottom()).isEqualTo(true)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnNextButtonClick_enrollNext() = runTest {
+        // Set latest status to FINGERPRINT_ENROLLABLE_OK
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 0)
+        setupSuwMaxFingerprintsEnrollable(application, resources, 1)
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            newIsSuwRequest(application)
+        )
+
+        val actions = listOfActionFlow(viewModel)
+
+        // Perform click on `next`
+        viewModel.onNextButtonClick(backgroundScope)
+        runCurrent()
+
+        assertThat(actions.size).isEqualTo(1)
+        assertThat(actions[0]).isEqualTo(CONTINUE_ENROLL)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnNextButtonClick_doneAndFinish() = runTest {
+        // Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+        setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 1)
+        setupSuwMaxFingerprintsEnrollable(application, resources, 1)
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            newIsSuwRequest(application)
+        )
+
+        val statusList = listOfPageStatusFlow(viewModel)
+        val actionList = listOfActionFlow(viewModel)
+
+        runCurrent()
+
+        assertThat(statusList.size).isEqualTo(1)
+        assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
+
+        val actions = listOfActionFlow(viewModel)
+
+        // Perform click on `next`
+        viewModel.onNextButtonClick(backgroundScope)
+        runCurrent()
+
+        assertThat(actionList.size).isEqualTo(1)
+        assertThat(actionList[0]).isEqualTo(DONE_AND_FINISH)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testOnSkipOrCancelButtonClick() = runTest {
+        val viewModel = newFingerprintEnrollIntroViewModel(
+            newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
+            newAllFalseRequest(application)
+        )
+
+        val actions = listOfActionFlow(viewModel)
+
+        viewModel.onSkipOrCancelButtonClick(backgroundScope)
+        runCurrent()
+
+        assertThat(actions.size).isEqualTo(1)
+        assertThat(actions[0]).isEqualTo(SKIP_OR_CANCEL)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun TestScope.listOfActionFlow(
+        viewModel: FingerprintEnrollIntroViewModel
+    ): List<FingerprintEnrollIntroAction> =
+        mutableListOf<FingerprintEnrollIntroAction>().also {
+            backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
+                viewModel.actionFlow.toList(it)
+            }
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun TestScope.listOfPageStatusFlow(
+        viewModel: FingerprintEnrollIntroViewModel
+    ): List<FingerprintEnrollIntroStatus> =
+        mutableListOf<FingerprintEnrollIntroStatus>().also {
+            backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
+                viewModel.pageStatusFlow.toList(it)
+            }
+        }
+
+    companion object {
+        private const val TEST_USER_ID = 33
+    }
+}