Moving FingerprintSettings to Kotlin

This change is the first of many, it will

1. Change java -> kotlin
2. Use the MVVM architecture
3. Be feature flagged

This change in particular is focused on transitions to and from various activities.

Enable feature via
adb shell setprop sys.fflag.override.settings_biometrics2_fingerprint true

Bug: 280862076
Test: atest FingerprintSettingsViewModelTest
Change-Id: I8eb5c30e6f2e92c256ae7c257a9d560439ba418f
Merged-In: I8eb5c30e6f2e92c256ae7c257a9d560439ba418f
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index a8fa527..3efa18f 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -73,6 +73,7 @@
     }
 
     public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ }
+    public static class FingerprintSettingsActivityV2 extends SettingsActivity { /* empty */ }
     public static class CombinedBiometricSettingsActivity extends SettingsActivity { /* empty */ }
     public static class CombinedBiometricProfileSettingsActivity extends SettingsActivity { /* empty */ }
     public static class TetherSettingsActivity extends SettingsActivity {
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt
new file mode 100644
index 0000000..d4249ff
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.biometrics.fingerprint2.ui.binder
+
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a [FingerprintSettingsViewModel] to a [FingerprintSettingsV2Fragment]
+ */
+object FingerprintViewBinder {
+
+    interface Binding {
+        fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?)
+        fun onEnrollSuccess()
+        fun onEnrollAdditionalFailure()
+        fun onEnrollFirstFailure(reason: String)
+        fun onEnrollFirstFailure(reason: String, resultCode: Int)
+        fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?)
+    }
+
+    /** Initial listener for the first enrollment request */
+    fun bind(
+        viewModel: FingerprintSettingsViewModel,
+        lifecycleScope: LifecycleCoroutineScope,
+        token: ByteArray?,
+        challenge: Long?,
+        launchFullFingerprintEnrollment: (
+            userId: Int,
+            gateKeeperPasswordHandle: Long?,
+            challenge: Long?,
+            challengeToken: ByteArray?
+        ) -> Unit,
+        launchAddFingerprint: (userId: Int, challengeToken: ByteArray?) -> Unit,
+        launchConfirmOrChooseLock: (userId: Int) -> Unit,
+        finish: () -> Unit,
+        setResultExternal: (resultCode: Int) -> Unit,
+    ): Binding {
+
+        lifecycleScope.launch {
+            viewModel.nextStep.filterNotNull().collect { nextStep ->
+                when (nextStep) {
+                    is EnrollFirstFingerprint -> launchFullFingerprintEnrollment(
+                        nextStep.userId,
+                        nextStep.gateKeeperPasswordHandle,
+                        nextStep.challenge,
+                        nextStep.challengeToken
+                    )
+
+                    is EnrollAdditionalFingerprint -> launchAddFingerprint(
+                        nextStep.userId, nextStep.challengeToken
+                    )
+
+                    is LaunchConfirmDeviceCredential -> launchConfirmOrChooseLock(nextStep.userId)
+
+                    is FinishSettings -> {
+                        println("Finishing due to ${nextStep.reason}")
+                        finish()
+                    }
+
+                    is FinishSettingsWithResult -> {
+                        println("Finishing with result ${nextStep.result} due to ${nextStep.reason}")
+                        setResultExternal(nextStep.result)
+                        finish()
+                    }
+
+                    is ShowSettings -> println("show settings")
+                }
+
+                viewModel.onUiCommandExecuted()
+            }
+        }
+
+        viewModel.updateTokenAndChallenge(token, if (challenge == -1L) null else challenge)
+
+        return object : Binding {
+            override fun onConfirmDevice(
+                wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?
+            ) {
+                viewModel.onConfirmDevice(wasSuccessful, theGateKeeperPasswordHandle)
+            }
+
+            override fun onEnrollSuccess() {
+                viewModel.onEnrollSuccess()
+            }
+
+            override fun onEnrollAdditionalFailure() {
+                viewModel.onEnrollAdditionalFailure()
+            }
+
+            override fun onEnrollFirstFailure(reason: String) {
+                viewModel.onEnrollFirstFailure(reason)
+            }
+
+            override fun onEnrollFirstFailure(reason: String, resultCode: Int) {
+                viewModel.onEnrollFirstFailure(reason, resultCode)
+            }
+
+            override fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
+                viewModel.onEnrollFirst(token, keyChallenge)
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
new file mode 100644
index 0000000..9b85564
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.biometrics.fingerprint2.ui.fragment
+
+import android.app.Activity
+import android.app.settings.SettingsEnums
+import android.content.Context.FINGERPRINT_SERVICE
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintManager
+import android.os.Bundle
+import android.util.FeatureFlagUtils
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import com.android.settings.R
+import com.android.settings.Utils
+import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintViewBinder
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.core.SettingsBaseActivity
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.password.ChooseLockGeneric
+import com.android.settings.password.ChooseLockSettingsHelper
+import com.android.settingslib.transition.SettingsTransitionHelper
+
+const val TAG = "FingerprintSettingsV2Fragment"
+
+/**
+ * A class responsible for showing FingerprintSettings. Typical activity Flows are
+ * 1. Settings > FingerprintSettings > PIN/PATTERN/PASS -> FingerprintSettings
+ * 2. FingerprintSettings -> FingerprintEnrollment fow
+ *
+ * This page typically allows for
+ * 1. Fingerprint deletion
+ * 2. Fingerprint enrollment
+ * 3. Renaming a fingerprint
+ * 4. Enabling/Disabling a feature
+ */
+class FingerprintSettingsV2Fragment : DashboardFragment() {
+    private lateinit var binding: FingerprintViewBinder.Binding
+
+    private val launchFirstEnrollmentListener =
+        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+
+            val resultCode = result.resultCode
+            val data = result.data
+
+            Log.d(
+                TAG, "onEnrollFirstFingerprint($resultCode, $data)"
+            )
+            if (resultCode != BiometricEnrollBase.RESULT_FINISHED || data == null) {
+                if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+                    binding.onEnrollFirstFailure(
+                        "Received RESULT_TIMEOUT when enrolling", resultCode
+                    )
+                } else {
+                    binding.onEnrollFirstFailure("Incorrect resultCode or data was null")
+                }
+            } else {
+                val token =
+                    data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+                val keyChallenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
+                binding.onEnrollFirst(token, keyChallenge)
+            }
+        }
+
+    /** Result listener for launching enrollments **after** a user has reached the settings page. */
+    private val launchAdditionalFingerprintListener =
+        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+            val resultCode = result.resultCode
+            Log.d(
+                TAG, "onEnrollAdditionalFingerprint($resultCode)"
+            )
+
+            if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+                binding.onEnrollAdditionalFailure()
+            } else {
+                binding.onEnrollSuccess()
+            }
+        }
+
+    /** Result listener for ChooseLock activity flow. */
+    private val confirmDeviceResultListener =
+        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+            val resultCode = result.resultCode
+            val data = result.data
+            onConfirmDevice(resultCode, data)
+        }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        // This is needed to support ChooseLockSettingBuilder...show(). All other activity
+        // calls should use the registerForActivity method call.
+        super.onActivityResult(requestCode, resultCode, data)
+        val wasSuccessful =
+            resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
+        val gateKeeperPasswordHandle =
+            data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
+        binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
+    }
+
+
+    override fun onCreate(icicle: Bundle?) {
+        super.onCreate(icicle)
+        if (!FeatureFlagUtils.isEnabled(
+                context, FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
+            )
+        ) {
+            Log.d(
+                TAG, "Finishing due to feature not being enabled"
+            )
+            finish()
+            return
+        }
+        val viewModel = ViewModelProvider(
+            this, FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+                requireContext().applicationContext.userId, requireContext().getSystemService(
+                    FINGERPRINT_SERVICE
+                ) as FingerprintManager
+            )
+        )[FingerprintSettingsViewModel::class.java]
+
+        val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+        val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
+
+        binding = FingerprintViewBinder.bind(
+            viewModel,
+            lifecycleScope,
+            token,
+            challenge,
+            ::launchFullFingerprintEnrollment,
+            ::launchAddFingerprint,
+            ::launchConfirmOrChooseLock,
+            ::finish,
+            ::setResultExternal,
+        )
+    }
+
+    override fun getMetricsCategory(): Int {
+        return SettingsEnums.FINGERPRINT
+    }
+
+    override fun getPreferenceScreenResId(): Int {
+        return R.xml.security_settings_fingerprint_limbo
+    }
+
+    override fun getLogTag(): String {
+        return TAG
+    }
+
+    /**
+     * Helper function that will try and launch confirm lock, if that fails we will prompt user
+     * to choose a PIN/PATTERN/PASS.
+     */
+    private fun launchConfirmOrChooseLock(userId: Int) {
+        val intent = Intent()
+        val builder = ChooseLockSettingsHelper.Builder(requireActivity(), this)
+        val launched = builder.setRequestCode(BiometricEnrollBase.CONFIRM_REQUEST)
+            .setTitle(getString(R.string.security_settings_fingerprint_preference_title))
+            .setRequestGatekeeperPasswordHandle(true).setUserId(userId).setForegroundOnly(true)
+            .setReturnCredentials(true).show()
+        if (!launched) {
+            intent.setClassName(
+                Utils.SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name
+            )
+            intent.putExtra(
+                ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true
+            )
+            intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
+            intent.putExtra(Intent.EXTRA_USER_ID, userId)
+            confirmDeviceResultListener.launch(intent)
+        }
+    }
+
+    /**
+     * Helper for confirming a PIN/PATTERN/PASS
+     */
+    private fun onConfirmDevice(resultCode: Int, data: Intent?) {
+        val wasSuccessful =
+            resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
+        val gateKeeperPasswordHandle =
+            data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
+        binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
+    }
+
+    /**
+     * Helper function to launch fingerprint enrollment(This should be the default behavior
+     * when a user enters their PIN/PATTERN/PASS and no fingerprints are enrolled.
+     */
+    private fun launchFullFingerprintEnrollment(
+        userId: Int,
+        gateKeeperPasswordHandle: Long?,
+        challenge: Long?,
+        challengeToken: ByteArray?,
+    ) {
+        val intent = Intent()
+        intent.setClassName(
+            Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollIntroductionInternal::class.java.name
+        )
+        intent.putExtra(BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY, true)
+        intent.putExtra(
+            SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
+            SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
+        )
+
+        intent.putExtra(Intent.EXTRA_USER_ID, userId)
+
+        if (gateKeeperPasswordHandle != null) {
+            intent.putExtra(
+                ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle
+            )
+        } else {
+            intent.putExtra(
+                ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken
+            )
+            intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
+        }
+        launchFirstEnrollmentListener.launch(intent)
+    }
+
+    private fun setResultExternal(resultCode: Int) {
+        setResult(resultCode)
+    }
+
+    /** Helper to launch an add fingerprint request */
+    private fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
+        val intent = Intent()
+        intent.setClassName(
+            Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollEnrolling::class.qualifiedName.toString()
+        )
+        intent.putExtra(Intent.EXTRA_USER_ID, userId)
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+        launchAdditionalFingerprintListener.launch(intent)
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
new file mode 100644
index 0000000..6cddb24
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.biometrics.fingerprint2.ui.viewmodel
+
+import android.hardware.fingerprint.FingerprintManager
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/**
+ * Models the UI state for fingerprint settings.
+ */
+class FingerprintSettingsViewModel(
+    private val userId: Int,
+    gateKeeperPassword: Long?,
+    theChallenge: Long?,
+    theChallengeToken: ByteArray?,
+    private val fingerprintManager: FingerprintManager
+) : ViewModel() {
+
+    private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
+    /**
+     *  This flow represents the high level state for the FingerprintSettingsV2Fragment. The
+     *  consumer of this flow should call [onUiCommandExecuted] which will set the state to null,
+     *  confirming that the UI has consumed the last command and is ready to consume another
+     *  command.
+     */
+    val nextStep = _nextStep.asStateFlow()
+
+
+    private var gateKeeperPasswordHandle: Long? = gateKeeperPassword
+    private var challenge: Long? = theChallenge
+    private var challengeToken: ByteArray? = theChallengeToken
+
+    /**
+     * Indicates to the view model that a confirm device credential action has been completed
+     * with a [theGateKeeperPasswordHandle] which will be used for [FingerprintManager]
+     * operations such as [FingerprintManager.enroll].
+     */
+    fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
+
+        if (!wasSuccessful) {
+            launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
+            return
+        }
+        if (theGateKeeperPasswordHandle == null) {
+            launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
+            return
+        }
+
+        gateKeeperPasswordHandle = theGateKeeperPasswordHandle
+        launchEnrollNextStep()
+    }
+
+    /**
+     * Notifies that enrollment was successful.
+     */
+    fun onEnrollSuccess() {
+        _nextStep.update {
+            ShowSettings(userId)
+        }
+    }
+
+    /**
+     * Notifies that an additional enrollment failed.
+     */
+    fun onEnrollAdditionalFailure() {
+        launchFinishSettings("Failed to enroll additional fingerprint")
+    }
+
+    /**
+     * Notifies that the first enrollment failed.
+     */
+    fun onEnrollFirstFailure(reason: String) {
+        launchFinishSettings(reason)
+    }
+
+    /**
+     * Notifies that first enrollment failed (with resultCode)
+     */
+    fun onEnrollFirstFailure(reason: String, resultCode: Int) {
+        launchFinishSettings(reason, resultCode)
+    }
+
+    /**
+     * Notifies that a users first enrollment succeeded.
+     */
+    fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
+        if (token == null) {
+            launchFinishSettings("Error, empty token")
+            return
+        }
+        if (keyChallenge == null) {
+            launchFinishSettings("Error, empty keyChallenge")
+            return
+        }
+        challengeToken = token
+        challenge = keyChallenge
+
+        _nextStep.update {
+            ShowSettings(userId)
+        }
+    }
+
+
+    /**
+     * Indicates if this settings activity has been called with correct token and challenge
+     * and that we do not need to launch confirm device credential.
+     */
+    fun updateTokenAndChallenge(token: ByteArray?, theChallenge: Long?) {
+        challengeToken = token
+        challenge = theChallenge
+        if (challengeToken == null) {
+            _nextStep.update {
+                LaunchConfirmDeviceCredential(userId)
+            }
+        } else {
+            launchEnrollNextStep()
+        }
+    }
+
+    /**
+     * Indicates a UI command has been consumed by the UI, and the logic can send another
+     * UI command.
+     */
+    fun onUiCommandExecuted() {
+        _nextStep.update {
+            null
+        }
+    }
+
+    private fun launchEnrollNextStep() {
+        if (fingerprintManager.getEnrolledFingerprints(userId).isEmpty()) {
+            _nextStep.update {
+                EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, challenge, challengeToken)
+            }
+        } else {
+            _nextStep.update {
+                ShowSettings(userId)
+            }
+        }
+    }
+
+    private fun launchFinishSettings(reason: String) {
+        _nextStep.update {
+            FinishSettings(reason)
+        }
+    }
+
+    private fun launchFinishSettings(reason: String, errorCode: Int) {
+        _nextStep.update {
+            FinishSettingsWithResult(errorCode, reason)
+        }
+    }
+
+    class FingerprintSettingsViewModelFactory(
+        private val userId: Int,
+        private val fingerprintManager: FingerprintManager,
+    ) : ViewModelProvider.Factory {
+
+        @Suppress("UNCHECKED_CAST")
+        override fun <T : ViewModel> create(
+            modelClass: Class<T>,
+        ): T {
+
+            return FingerprintSettingsViewModel(
+                userId, null, null, null, fingerprintManager
+            ) as T
+        }
+    }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
new file mode 100644
index 0000000..1046f51
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.biometrics.fingerprint2.ui.viewmodel
+
+/**
+ * A class to represent a next step for FingerprintSettings. This is typically to perform an action
+ * such that launches another activity such as EnrollFirstFingerprint() or
+ * LaunchConfirmDeviceCredential().
+ */
+sealed class NextStepViewModel
+
+data class EnrollFirstFingerprint(
+    val userId: Int, val gateKeeperPasswordHandle: Long?,
+    val challenge: Long?,
+    val challengeToken: ByteArray?,
+) : NextStepViewModel()
+
+data class EnrollAdditionalFingerprint(
+    val userId: Int,
+    val challengeToken: ByteArray?,
+) : NextStepViewModel()
+
+data class FinishSettings(
+    val reason: String
+) : NextStepViewModel()
+
+data class FinishSettingsWithResult(
+    val result: Int, val reason: String
+) : NextStepViewModel()
+
+data class ShowSettings(val userId: Int) : NextStepViewModel()
+
+data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()
+
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index b5d12a5..3100706 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -72,6 +72,7 @@
 import com.android.settings.biometrics.combination.CombinedBiometricSettings;
 import com.android.settings.biometrics.face.FaceSettings;
 import com.android.settings.biometrics.fingerprint.FingerprintSettings;
+import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment;
 import com.android.settings.bluetooth.BluetoothBroadcastDialog;
 import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
 import com.android.settings.bluetooth.BluetoothFindBroadcastsFragment;
@@ -266,6 +267,7 @@
             AssistGestureSettings.class.getName(),
             FaceSettings.class.getName(),
             FingerprintSettings.FingerprintSettingsFragment.class.getName(),
+            FingerprintSettingsV2Fragment.class.getName(),
             CombinedBiometricSettings.class.getName(),
             CombinedBiometricProfileSettings.class.getName(),
             SwipeToNotificationSettings.class.getName(),