From 2407b4033ab3a46ebcf0004528ca833caa1f21d4 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Wed, 26 Jul 2023 15:59:14 +0000 Subject: [PATCH] Basic structure for fingerprint enrollment. Bug: N/A Test: Enroll introduction screen works as expected Test: User is prompted with pin/pattern/pass if the token is not present. Change-Id: I32a182b09c3bcd9be43428c500bfae7b39a74e63 --- Android.bp | 4 +- AndroidManifest.xml | 10 + .../fingerprint_v2_enroll_find_sensor.xml | 44 +++ .../fingerprint_v2_enroll_introduction.xml | 214 +++++++++++++ res/layout/fingerprint_v2_enroll_main.xml | 29 ++ .../FingerprintEnrollmentV2Activity.kt | 259 ++++++++++++++++ ...FingerprintEnrollConfirmationV2Fragment.kt | 39 +++ .../FingerprintEnrollEnrollingV2Fragment.kt | 34 ++ .../FingerprintEnrollFindSensorV2Fragment.kt | 42 +++ .../FingerprintEnrollmentIntroV2Fragment.kt | 290 ++++++++++++++++++ ...ngerprintEnrolllmentNavigationViewModel.kt | 160 ++++++++++ .../FingerprintGatekeeperViewModel.kt | 124 ++++++++ .../viewmodel/FingerprintScrollViewModel.kt | 47 +++ .../ui/viewmodel/FingerprintStateViewModel.kt | 90 ++++++ .../ui/viewmodel/NextStepViewModel.kt | 104 +++++++ 15 files changed, 1489 insertions(+), 1 deletion(-) create mode 100644 res/layout/fingerprint_v2_enroll_find_sensor.xml create mode 100644 res/layout/fingerprint_v2_enroll_introduction.xml create mode 100644 res/layout/fingerprint_v2_enroll_main.xml create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/activity/FingerprintEnrollmentV2Activity.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollConfirmationV2Fragment.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollEnrollingV2Fragment.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollFindSensorV2Fragment.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollmentIntroV2Fragment.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintGatekeeperViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintScrollViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintStateViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/NextStepViewModel.kt diff --git a/Android.bp b/Android.bp index 861f95ff784..db52d18e060 100644 --- a/Android.bp +++ b/Android.bp @@ -106,7 +106,9 @@ android_library { "SystemUIUnfoldLib", ], - plugins: ["androidx.room_room-compiler-plugin"], + plugins: [ + "androidx.room_room-compiler-plugin", + ], libs: [ "telephony-common", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2c61ec62b28..c6e1e5df980 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2566,6 +2566,16 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/fingerprint_v2_enroll_introduction.xml b/res/layout/fingerprint_v2_enroll_introduction.xml new file mode 100644 index 00000000000..e9dd08ad7ca --- /dev/null +++ b/res/layout/fingerprint_v2_enroll_introduction.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/fingerprint_v2_enroll_main.xml b/res/layout/fingerprint_v2_enroll_main.xml new file mode 100644 index 00000000000..b3d6c3dd1d9 --- /dev/null +++ b/res/layout/fingerprint_v2_enroll_main.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/activity/FingerprintEnrollmentV2Activity.kt new file mode 100644 index 00000000000..7bea4b4e515 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/activity/FingerprintEnrollmentV2Activity.kt @@ -0,0 +1,259 @@ +/* + * 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.enrollment.ui.activity + +import android.annotation.ColorInt +import android.app.Activity +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color +import android.hardware.fingerprint.FingerprintManager +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.android.internal.widget.LockPatternUtils +import com.android.settings.R +import com.android.settings.SetupWizardUtils +import com.android.settings.Utils +import com.android.settings.Utils.SETTINGS_PACKAGE_NAME +import com.android.settings.biometrics.BiometricEnrollBase +import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST +import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED +import com.android.settings.biometrics.GatekeeperPasswordProvider +import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl +import com.android.settings.biometrics.fingerprint2.enrollment.ui.fragment.FingerprintEnrollConfirmationV2Fragment +import com.android.settings.biometrics.fingerprint2.enrollment.ui.fragment.FingerprintEnrollEnrollingV2Fragment +import com.android.settings.biometrics.fingerprint2.enrollment.ui.fragment.FingerprintEnrollFindSensorV2Fragment +import com.android.settings.biometrics.fingerprint2.enrollment.ui.fragment.FingerprintEnrollmentIntroV2Fragment +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.Confirmation +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.Education +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.Enrollment +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintEnrollmentNavigationViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintGatekeeperViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintScrollViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.Finish +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.GatekeeperInfo +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.Intro +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.LaunchConfirmDeviceCredential +import com.android.settings.password.ChooseLockGeneric +import com.android.settings.password.ChooseLockSettingsHelper +import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE +import com.google.android.setupdesign.util.ThemeHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +private const val TAG = "FingerprintEnrollmentV2Activity" + +/** + * This is the activity that controls the entire Fingerprint Enrollment experience through its + * children fragments. + */ +class FingerprintEnrollmentV2Activity : FragmentActivity() { + private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel + private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel + private val coroutineDispatcher = Dispatchers.Default + + /** 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?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == CONFIRM_REQUEST) { + onConfirmDevice(resultCode, data) + } + } + + override fun onAttachedToWindow() { + window.statusBarColor = getBackgroundColor() + super.onAttachedToWindow() + } + + @ColorInt + private fun getBackgroundColor(): Int { + val stateList: ColorStateList? = + Utils.getColorAttr(applicationContext, android.R.attr.windowBackground) + return stateList?.defaultColor ?: Color.TRANSPARENT + } + + private fun onConfirmDevice(resultCode: Int, data: Intent?) { + val wasSuccessful = resultCode == RESULT_FINISHED || resultCode == Activity.RESULT_OK + val gateKeeperPasswordHandle = data?.getExtra(EXTRA_KEY_GK_PW_HANDLE) as Long? + lifecycleScope.launch { + gatekeeperViewModel.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.fingerprint_v2_enroll_main) + + setTheme(SetupWizardUtils.getTheme(applicationContext, intent)) + ThemeHelper.trySetDynamicColor(applicationContext) + + val backgroundDispatcher = Dispatchers.IO + + val context = applicationContext + val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager + + val interactor = + FingerprintManagerInteractorImpl( + context, + backgroundDispatcher, + fingerprintManager, + GatekeeperPasswordProvider(LockPatternUtils(context)) + ) { + var toReturn: Int = + Settings.Secure.getIntForUser( + context.contentResolver, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, + -1, + context.userId, + ) + if (toReturn == -1) { + toReturn = + if ( + context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault) + ) { + 1 + } else { + 0 + } + Settings.Secure.putIntForUser( + context.contentResolver, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, + toReturn, + context.userId + ) + } + toReturn == 1 + } + + var challenge: Long? = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long? + val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN) + val gatekeeperInfo = FingerprintGatekeeperViewModel.toGateKeeperInfo(challenge, token) + + gatekeeperViewModel = + ViewModelProvider( + this, + FingerprintGatekeeperViewModel.FingerprintGatekeeperViewModelFactory( + gatekeeperInfo, + interactor, + ) + )[FingerprintGatekeeperViewModel::class.java] + + navigationViewModel = + ViewModelProvider( + this, + FingerprintEnrollmentNavigationViewModel.FingerprintEnrollmentNavigationViewModelFactory( + backgroundDispatcher, + interactor, + gatekeeperViewModel, + gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, /* canSkipConfirm */ + ) + )[FingerprintEnrollmentNavigationViewModel::class.java] + + // Initialize FingerprintViewModel + ViewModelProvider(this, FingerprintViewModel.FingerprintViewModelFactory(interactor))[ + FingerprintViewModel::class.java] + + // Initialize scroll view model + ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[ + FingerprintScrollViewModel::class.java] + + lifecycleScope.launch { + navigationViewModel.navigationViewModel.filterNotNull().collect { + Log.d(TAG, "navigationStep $it") + val isForward = it.forward + val currStep = it.currStep + val theClass: Class? = + when (currStep) { + Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class + Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class + Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class + Intro -> FingerprintEnrollmentIntroV2Fragment::class.java as Class + else -> null + } + + if (theClass != null) { + supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .add(R.id.fragment_container_view, theClass, null) + .commit() + } else { + + if (currStep is Finish) { + if (currStep.resultCode != null) { + finishActivity(currStep.resultCode) + } else { + finish() + } + } else if (currStep == LaunchConfirmDeviceCredential) { + launchConfirmOrChooseLock(userId) + } + } + } + } + + val fromSettingsSummary = + intent.getBooleanExtra(BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY, false) + if ( + fromSettingsSummary && GatekeeperPasswordProvider.containsGatekeeperPasswordHandle(intent) + ) { + overridePendingTransition( + com.google.android.setupdesign.R.anim.sud_slide_next_in, + com.google.android.setupdesign.R.anim.sud_slide_next_out + ) + } + } + + private fun launchConfirmOrChooseLock(userId: Int) { + val activity = this + lifecycleScope.launch(coroutineDispatcher) { + val intent = Intent() + val builder = ChooseLockSettingsHelper.Builder(activity) + val launched = + builder + .setRequestCode(CONFIRM_REQUEST) + .setTitle(getString(R.string.security_settings_fingerprint_preference_title)) + .setRequestGatekeeperPasswordHandle(true) + .setUserId(userId) + .setForegroundOnly(true) + .setReturnCredentials(true) + .show() + if (!launched) { + intent.setClassName(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) + } + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollConfirmationV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollConfirmationV2Fragment.kt new file mode 100644 index 00000000000..84a56587d22 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollConfirmationV2Fragment.kt @@ -0,0 +1,39 @@ +/* + * 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.enrollment.ui.fragment + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintEnrollmentNavigationViewModel + +/** + * A fragment to indicate that fingerprint enrollment has been completed. + * + * This page will display basic information about what a fingerprint can be used for and acts as the + * final step of enrollment. + */ +class FingerprintEnrollConfirmationV2Fragment : Fragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val navigationViewModel = + ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollEnrollingV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollEnrollingV2Fragment.kt new file mode 100644 index 00000000000..846bad7b491 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollEnrollingV2Fragment.kt @@ -0,0 +1,34 @@ +/* + * 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.enrollment.ui.fragment + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintEnrollmentNavigationViewModel + +/** A fragment that is responsible for enrolling a users fingerprint. */ +class FingerprintEnrollEnrollingV2Fragment : Fragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val navigationViewModel = + ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollFindSensorV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollFindSensorV2Fragment.kt new file mode 100644 index 00000000000..6b074678289 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollFindSensorV2Fragment.kt @@ -0,0 +1,42 @@ +/* + * 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.enrollment.ui.fragment + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintEnrollmentNavigationViewModel + +/** + * A fragment that is used to educate the user about the fingerprint sensor on this device. + * + * The main goals of this page are + * 1. Inform the user where the fingerprint sensor is on their device + * 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment] + * will work. + */ +class FingerprintEnrollFindSensorV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_find_sensor) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val navigationViewModel = + ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollmentIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollmentIntroV2Fragment.kt new file mode 100644 index 00000000000..14229eea5a4 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/fragment/FingerprintEnrollmentIntroV2Fragment.kt @@ -0,0 +1,290 @@ +/* + * 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.enrollment.ui.fragment + +import android.annotation.NonNull +import android.annotation.StringRes +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.hardware.fingerprint.FingerprintSensorProperties +import android.os.Bundle +import android.text.Html +import android.text.method.LinkMovementMethod +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.ScrollView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintEnrollmentNavigationViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintGatekeeperViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintScrollViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.enrollment.ui.viewmodel.Unicorn +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 +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +private const val TAG = "FingerprintEnrollmentIntroV2Fragment" + +/** This class represents the customizable text for FingerprintEnrollIntroduction. */ +private data class TextModel( + @StringRes val footerMessageTwo: Int, + @StringRes val footerMessageThree: Int, + @StringRes val footerMessageFour: Int, + @StringRes val footerMessageFive: Int, + @StringRes val footerMessageSix: Int, + @StringRes val negativeButton: Int, + @StringRes val footerTitleOne: Int, + @StringRes val footerTitleTwo: Int, + @StringRes val headerText: Int, + @StringRes val descriptionText: Int, +) + +/** + * The introduction fragment that is used to inform the user the basics of what a fingerprint sensor + * is and how it will be used. + * + * The main gaols of this page are + * 1. Inform the user what the fingerprint sensor is and does + * 2. How the data will be stored + * 3. How the user can access and remove their data + */ +class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_introduction) { + private lateinit var footerBarMixin: FooterBarMixin + private lateinit var textModel: TextModel + private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel + private lateinit var fingerprintStateViewModel: FingerprintViewModel + private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel + private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + navigationViewModel = + ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + fingerprintStateViewModel = + ViewModelProvider(requireActivity())[FingerprintViewModel::class.java] + fingerprintScrollViewModel = + ViewModelProvider(requireActivity())[FingerprintScrollViewModel::class.java] + gateKeeperViewModel = + ViewModelProvider(requireActivity())[FingerprintGatekeeperViewModel::class.java] + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + lifecycleScope.launch { + combine( + navigationViewModel.enrollType, + fingerprintStateViewModel.fingerprintStateViewModel, + ) { enrollType, fingerprintStateViewModel -> + Pair(enrollType, fingerprintStateViewModel) + } + .collect { (enrollType, fingerprintStateViewModel) -> + val sensorProps = fingerprintStateViewModel?.sensorProps + + textModel = + when (enrollType) { + Unicorn -> getUnicornTextModel() + else -> getNormalTextModel() + } + + setupFooterBarAndScrollView(view) + + if (savedInstanceState == null) { + getLayout()?.setHeaderText(textModel.headerText) + getLayout()?.setDescriptionText(textModel.descriptionText) + + // Set color filter for the following icons. + val colorFilter = getIconColorFilter() + listOf( + R.id.icon_fingerprint, + R.id.icon_device_locked, + R.id.icon_trash_can, + R.id.icon_info, + R.id.icon_shield, + R.id.icon_link + ) + .forEach { icon -> + view.findViewById(icon).drawable.colorFilter = colorFilter + } + + // Set the text for the footer text views. + listOf( + R.id.footer_message_2 to textModel.footerMessageTwo, + R.id.footer_message_3 to textModel.footerMessageThree, + R.id.footer_message_4 to textModel.footerMessageFour, + R.id.footer_message_5 to textModel.footerMessageFive, + R.id.footer_message_6 to textModel.footerMessageSix, + ) + .forEach { pair -> view.findViewById(pair.first).setText(pair.second) } + + setFooterLink(view) + + val iconShield: ImageView = view.findViewById(R.id.icon_shield) + val footerMessage6: TextView = view.findViewById(R.id.footer_message_6) + when (sensorProps?.sensorType) { + FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL -> { + footerMessage6.visibility = View.VISIBLE + iconShield.visibility = View.VISIBLE + } + else -> { + footerMessage6.visibility = View.GONE + iconShield.visibility = View.GONE + } + } + + view.findViewById(R.id.footer_title_1).setText(textModel.footerTitleOne) + view.findViewById(R.id.footer_title_2).setText(textModel.footerTitleOne) + } + } + } + } + + private fun setFooterLink(view: View) { + val footerLink: TextView = view.findViewById(R.id.footer_learn_more) + footerLink.movementMethod = LinkMovementMethod.getInstance() + footerLink.text = + Html.fromHtml( + getString(R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more), + Html.FROM_HTML_MODE_LEGACY + ) + } + + private fun setupFooterBarAndScrollView( + view: View, + ) { + val scrollView: ScrollView = + view.findViewById(com.google.android.setupdesign.R.id.sud_scroll_view) + scrollView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES + // Next button responsible for starting the next fragment. + val onNextButtonClick: View.OnClickListener = + View.OnClickListener { Log.d(TAG, "OnNextClicked") } + + val layout: GlifLayout = requireActivity().findViewById(R.id.setup_wizard_layout) + footerBarMixin = layout.getMixin(FooterBarMixin::class.java) + footerBarMixin.primaryButton = + FooterButton.Builder(requireActivity()) + .setText(R.string.security_settings_face_enroll_introduction_more) + .setListener(onNextButtonClick) + .setButtonType(FooterButton.ButtonType.OPT_IN) + .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) + .build() + footerBarMixin.setSecondaryButton( + FooterButton.Builder(requireActivity()) + .setText(textModel.negativeButton) + .setListener({ Log.d(TAG, "prevClicked") }) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) + .build(), + true /* usePrimaryStyle */ + ) + + val primaryButton = footerBarMixin.primaryButton + val secondaryButton = footerBarMixin.secondaryButton + + secondaryButton.visibility = View.INVISIBLE + + val requireScrollMixin = layout.getMixin(RequireScrollMixin::class.java) + requireScrollMixin.requireScrollWithButton( + requireActivity(), + footerBarMixin.primaryButton, + R.string.security_settings_face_enroll_introduction_more, + onNextButtonClick + ) + + requireScrollMixin.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean -> + // Show secondary button once scroll is completed. + if (!scrollNeeded) { + fingerprintScrollViewModel.userConsented() + } + } + + lifecycleScope.launch { + fingerprintScrollViewModel.hasReadConsentScreen.collect { consented -> + if (consented) { + primaryButton.setText( + requireContext(), + R.string.security_settings_fingerprint_enroll_introduction_agree + ) + secondaryButton.visibility = View.VISIBLE + } else { + secondaryButton.visibility = View.INVISIBLE + } + } + } + + footerBarMixin.getButtonContainer()?.setBackgroundColor(Color.TRANSPARENT) + + // I think I should remove this, and make the challenge a pre-requisite of launching + // the flow. For instance if someone launches the activity with an invalid challenge, it + // either 1) Fails or 2) Launched confirmDeviceCredential + primaryButton.isEnabled = false + lifecycleScope.launch { + gateKeeperViewModel.hasValidGatekeeperInfo.collect { primaryButton.isEnabled = it } + } + } + + private fun getNormalTextModel() = + TextModel( + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_2, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_3, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_4, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_6, + R.string.security_settings_fingerprint_enroll_introduction_no_thanks, + R.string.security_settings_fingerprint_enroll_introduction_footer_title_1, + R.string.security_settings_fingerprint_enroll_introduction_footer_title_2, + R.string.security_settings_fingerprint_enroll_introduction_title, + R.string.security_settings_fingerprint_enroll_introduction_v3_message, + ) + + private fun getUnicornTextModel() = + TextModel( + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_2, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_3, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_4, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_5, + R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_consent_6, + R.string.security_settings_fingerprint_enroll_introduction_no_thanks, + R.string.security_settings_fingerprint_enroll_introduction_footer_title_consent_1, + R.string.security_settings_fingerprint_enroll_introduction_footer_title_2, + R.string.security_settings_fingerprint_enroll_consent_introduction_title, + R.string.security_settings_fingerprint_enroll_introduction_v3_message, + ) + + @NonNull + private fun getIconColorFilter(): PorterDuffColorFilter { + return PorterDuffColorFilter( + DynamicColorPalette.getColor(context, DynamicColorPalette.ColorType.ACCENT), + PorterDuff.Mode.SRC_IN + ) + } + + private fun getLayout(): GlifLayout? { + return requireView().findViewById(R.id.setup_wizard_layout) as GlifLayout? + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt new file mode 100644 index 00000000000..d074fdd5df5 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt @@ -0,0 +1,160 @@ +/* + * 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.enrollment.ui.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +const val TAG = "FingerprintEnrollmentNavigationViewModel" + +/** Interface to validate a gatekeeper hat */ +interface Validator { + fun validateGateKeeper(challenge: Long?): Boolean +} + +/** + * The [EnrollType] for fingerprint enrollment indicates information on how the flow should behave. + */ +sealed class EnrollType() + +/** The default enrollment experience, typically called from Settings */ +object Default : EnrollType() + +/** SetupWizard/Out of box experience (OOBE) enrollment type. */ +object SetupWizard : EnrollType() + +/** Unicorn enrollment type */ +object Unicorn : EnrollType() + +/** + * This class is responsible for sending a [NavigationStep] which indicates where the user is in the + * Fingerprint Enrollment flow + */ +class FingerprintEnrollmentNavigationViewModel( + private val dispatcher: CoroutineDispatcher, + private val validator: Validator, + private val fingerprintManagerInteractor: FingerprintManagerInteractor, + private val gatekeeperViewModel: FingerprintGatekeeperViewModel, + private val canSkipConfirm: Boolean +) : ViewModel() { + + private class InternalNavigationStep( + lastStep: NextStepViewModel, + nextStep: NextStepViewModel, + forward: Boolean, + var canNavigate: Boolean + ) : NavigationStep(lastStep, nextStep, forward) + + private var _enrollType = MutableStateFlow(Default) + + /** A flow that indicates the [EnrollType] */ + val enrollType: Flow = _enrollType.asStateFlow() + + private var navState = NavState(canSkipConfirm) + + private val _navigationStep = + MutableStateFlow( + InternalNavigationStep( + PlaceHolderState, + Start.next(navState), + forward = false, + canNavigate = true + ) + ) + + init { + viewModelScope.launch { + gatekeeperViewModel.credentialConfirmed.filterNotNull().collect { + if (_navigationStep.value.currStep is LaunchConfirmDeviceCredential) { + if (it) nextStep() else finish() + } + } + } + } + + /** + * A flow that contains the [NavigationStep] used to indicate where in the enrollment process the + * user is. + */ + val navigationViewModel: Flow = _navigationStep.asStateFlow() + + /** Used to start the next step of Fingerprint Enrollment. */ + fun nextStep() { + viewModelScope.launch { + val currStep = _navigationStep.value.currStep + val nextStep = currStep.next(navState) + Log.d(TAG, "nextStep(${currStep} -> $nextStep)") + _navigationStep.update { + InternalNavigationStep(currStep, nextStep, forward = true, canNavigate = false) + } + } + } + + /** Go back a step of fingerprint enrollment. */ + fun prevStep() { + viewModelScope.launch { + val currStep = _navigationStep.value.currStep + val nextStep = currStep.prev(navState) + _navigationStep.update { + InternalNavigationStep(currStep, nextStep, forward = false, canNavigate = false) + } + } + } + + private fun finish() { + _navigationStep.update { + InternalNavigationStep(Finish(null), Finish(null), forward = false, canNavigate = false) + } + } + + class FingerprintEnrollmentNavigationViewModelFactory( + private val backgroundDispatcher: CoroutineDispatcher, + private val fingerprintManagerInteractor: FingerprintManagerInteractor, + private val fingerprintGatekeeperViewModel: FingerprintGatekeeperViewModel, + private val canSkipConfirm: Boolean, + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + + return FingerprintEnrollmentNavigationViewModel( + backgroundDispatcher, + object : Validator { + override fun validateGateKeeper(challenge: Long?): Boolean { + return challenge != null + } + }, + fingerprintManagerInteractor, + fingerprintGatekeeperViewModel, + canSkipConfirm, + ) + as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintGatekeeperViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintGatekeeperViewModel.kt new file mode 100644 index 00000000000..8079f7a69c5 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintGatekeeperViewModel.kt @@ -0,0 +1,124 @@ +/* + * 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.enrollment.ui.viewmodel + +import android.os.CountDownTimer +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +sealed interface GatekeeperInfo { + object Invalid : GatekeeperInfo + object Timeout : GatekeeperInfo + data class GatekeeperPasswordInfo(val token: ByteArray?, val passwordHandle: Long?) : + GatekeeperInfo +} + +/** + * This class is responsible for maintaining the gatekeeper information including things like + * timeouts. + * + * Please note, that this class can't fully support timeouts of the gatekeeper password handle due + * to the fact that a handle may have been generated earlier in the settings enrollment and passed + * in as a parameter to this class. + */ +class FingerprintGatekeeperViewModel( + theGatekeeperInfo: GatekeeperInfo?, + private val fingerprintManagerInteractor: FingerprintManagerInteractor, +) : ViewModel() { + + private var _gatekeeperInfo: MutableStateFlow = + MutableStateFlow(theGatekeeperInfo) + + /** The gatekeeper info for fingerprint enrollment. */ + val gatekeeperInfo: Flow = _gatekeeperInfo.asStateFlow() + + /** Indicates if the gatekeeper info is valid. */ + val hasValidGatekeeperInfo: Flow = + gatekeeperInfo.map { it is GatekeeperInfo.GatekeeperPasswordInfo } + + private var _credentialConfirmed: MutableStateFlow = MutableStateFlow(null) + val credentialConfirmed: Flow = _credentialConfirmed.asStateFlow() + + private var countDownTimer: CountDownTimer? = null + + /** Timeout of 15 minutes for a generated challenge */ + private val TIMEOUT: Long = 15 * 60 * 1000 + + /** Called after a confirm device credential attempt has been made. */ + fun onConfirmDevice(wasSuccessful: Boolean, theGatekeeperPasswordHandle: Long?) { + if (!wasSuccessful) { + Log.d(TAG, "confirmDevice failed") + _gatekeeperInfo.update { GatekeeperInfo.Invalid } + _credentialConfirmed.update { false } + } else { + viewModelScope.launch { + val res = fingerprintManagerInteractor.generateChallenge(theGatekeeperPasswordHandle!!) + _gatekeeperInfo.update { GatekeeperInfo.GatekeeperPasswordInfo(res.second, res.first) } + _credentialConfirmed.update { true } + startTimeout() + } + } + } + + private fun startTimeout() { + countDownTimer?.cancel() + countDownTimer = + object : CountDownTimer(TIMEOUT, 1000) { + override fun onFinish() { + _gatekeeperInfo.update { GatekeeperInfo.Timeout } + } + + override fun onTick(millisUntilFinished: Long) {} + } + } + + companion object { + /** + * A function that checks if the challenge and token are valid, in which case a + * [GatekeeperInfo.GatekeeperPasswordInfo] is provided, else [GatekeeperInfo.Invalid] + */ + fun toGateKeeperInfo(challenge: Long?, token: ByteArray?): GatekeeperInfo { + Log.d(TAG, "toGateKeeperInfo(${challenge == null}, ${token == null})") + if (challenge == null || token == null) { + return GatekeeperInfo.Invalid + } + return GatekeeperInfo.GatekeeperPasswordInfo(token, challenge) + } + } + + class FingerprintGatekeeperViewModelFactory( + private val gatekeeperInfo: GatekeeperInfo?, + private val fingerprintManagerInteractor: FingerprintManagerInteractor, + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return FingerprintGatekeeperViewModel(gatekeeperInfo, fingerprintManagerInteractor) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintScrollViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintScrollViewModel.kt new file mode 100644 index 00000000000..ad90fc757b7 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintScrollViewModel.kt @@ -0,0 +1,47 @@ +/* + * 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.enrollment.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** This class is responsible for ensuring a users consent to use FingerprintEnrollment. */ +class FingerprintScrollViewModel : ViewModel() { + + private val _hasReadConsentScreen: MutableStateFlow = MutableStateFlow(false) + /** Indicates if a user has consented to FingerprintEnrollment */ + val hasReadConsentScreen: Flow = _hasReadConsentScreen.asStateFlow() + + /** Indicates that a user has consented to FingerprintEnrollment */ + fun userConsented() { + _hasReadConsentScreen.update { true } + } + + class FingerprintScrollViewModelFactory() : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return FingerprintScrollViewModel() as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintStateViewModel.kt new file mode 100644 index 00000000000..1acd15a032d --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/FingerprintStateViewModel.kt @@ -0,0 +1,90 @@ +/* + * 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.enrollment.ui.viewmodel + +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** Represents the fingerprint data nad the relevant state. */ +data class FingerprintStateViewModel( + val fingerprintViewModels: List, + val canEnroll: Boolean, + val maxFingerprints: Int, + val sensorProps: FingerprintSensorPropertiesInternal, +) + +/** Represents a fingerprint enrollment. */ +data class FingerEnrollmentViewModel( + val name: String, + val fingerId: Int, + val deviceId: Long, +) + +/** Represents all of the fingerprint information needed for fingerprint enrollment. */ +class FingerprintViewModel(fingerprintManagerInteractor: FingerprintManagerInteractor) : + ViewModel() { + + private val _fingerprintViewModel: MutableStateFlow = + MutableStateFlow(null) + + /** + * A flow that contains a [FingerprintStateViewModel] which contains the relevant information for + * enrollment + */ + val fingerprintStateViewModel: Flow = + _fingerprintViewModel.asStateFlow() + + init { + viewModelScope.launch { + val enrolledFingerprints = + fingerprintManagerInteractor.enrolledFingerprints.last().map { + FingerEnrollmentViewModel(it.name, it.fingerId, it.deviceId) + } + val sensorProps = fingerprintManagerInteractor.sensorPropertiesInternal().first() + val maxFingerprints = 5 + _fingerprintViewModel.update { + FingerprintStateViewModel( + enrolledFingerprints, + enrolledFingerprints.size < maxFingerprints, + maxFingerprints, + sensorProps, + ) + } + } + } + + class FingerprintViewModelFactory(val interactor: FingerprintManagerInteractor) : + ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + + return FingerprintViewModel(interactor) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/NextStepViewModel.kt new file mode 100644 index 00000000000..a8a8077db2d --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/enrollment/ui/viewmodel/NextStepViewModel.kt @@ -0,0 +1,104 @@ +/* + * 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.enrollment.ui.viewmodel + +/** + * A class that represents an action that the consumer should transition between lastStep and + * currStep and in what direction this transition is occurring (e.g. forward or backwards) + */ +open class NavigationStep( + val lastStep: NextStepViewModel, + val currStep: NextStepViewModel, + val forward: Boolean +) { + override fun toString(): String { + return "lastStep=$lastStep, currStep=$currStep, forward=$forward" + } +} + +/** The navigation state used by a [NavStep] to determine what the [NextStepViewModel] should be. */ +class NavState(val confirmedDevice: Boolean) + +interface NavStep { + fun next(state: NavState): T + fun prev(state: NavState): T +} + +/** + * A class to represent a high level step (I.E. EnrollmentIntroduction) for FingerprintEnrollment. + */ +sealed class NextStepViewModel : NavStep + +/** + * This is the initial state for the previous step, used to indicate that there have been no + * previous states. + */ +object PlaceHolderState : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Finish(null) + + override fun prev(state: NavState): NextStepViewModel = Finish(null) +} + +/** + * This state is the initial state for the current step, and will be used to determine if the user + * needs to [LaunchConfirmDeviceCredential] if not, it will go to [Intro] + */ +object Start : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = + if (state.confirmedDevice) Intro else LaunchConfirmDeviceCredential + + override fun prev(state: NavState): NextStepViewModel = Finish(null) +} + +/** State indicating enrollment has been completed */ +class Finish(val resultCode: Int?) : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Finish(resultCode) + override fun prev(state: NavState): NextStepViewModel = Finish(null) +} + +/** State for the FingerprintEnrollment introduction */ +object Intro : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Education + override fun prev(state: NavState): NextStepViewModel = Finish(null) +} + +/** State for the FingerprintEnrollment education */ +object Education : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Enrollment + override fun prev(state: NavState): NextStepViewModel = Intro +} + +/** State for the FingerprintEnrollment enrollment */ +object Enrollment : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Confirmation + override fun prev(state: NavState): NextStepViewModel = Education +} + +/** State for the FingerprintEnrollment confirmation */ +object Confirmation : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Finish(0) + override fun prev(state: NavState): NextStepViewModel = Intro +} + +/** + * State used to send the user to the ConfirmDeviceCredential activity. This activity can either + * confirm a users device credential, or have them create one. + */ +object LaunchConfirmDeviceCredential : NextStepViewModel() { + override fun next(state: NavState): NextStepViewModel = Intro + override fun prev(state: NavState): NextStepViewModel = Finish(0) +}