1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.biometrics.fingerprint2.ui.settings.binder 18 19 import android.hardware.fingerprint.FingerprintManager 20 import android.util.Log 21 import androidx.lifecycle.LifecycleCoroutineScope 22 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel 23 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData 24 import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder.FingerprintView 25 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollAdditionalFingerprint 26 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollFirstFingerprint 27 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel 28 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel 29 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FinishSettings 30 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FinishSettingsWithResult 31 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.LaunchConfirmDeviceCredential 32 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.LaunchedActivity 33 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.PreferenceViewModel 34 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.ShowSettings 35 import kotlinx.coroutines.Dispatchers 36 import kotlinx.coroutines.Job 37 import kotlinx.coroutines.flow.collectLatest 38 import kotlinx.coroutines.flow.filterNotNull 39 import kotlinx.coroutines.launch 40 41 private const val TAG = "FingerprintSettingsViewBinder" 42 43 /** Binds a [FingerprintSettingsViewModel] to a [FingerprintView] */ 44 object FingerprintSettingsViewBinder { 45 46 interface FingerprintView { 47 /** 48 * Helper function to launch fingerprint enrollment(This should be the default behavior when a 49 * user enters their PIN/PATTERN/PASS and no fingerprints are enrolled). 50 */ 51 fun launchFullFingerprintEnrollment( 52 userId: Int, 53 gateKeeperPasswordHandle: Long?, 54 challenge: Long?, 55 challengeToken: ByteArray?, 56 ) 57 58 /** Helper to launch an add fingerprint request */ 59 fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) 60 61 /** 62 * Helper function that will try and launch confirm lock, if that fails we will prompt user to 63 * choose a PIN/PATTERN/PASS. 64 */ 65 fun launchConfirmOrChooseLock(userId: Int) 66 67 /** Used to indicate that FingerprintSettings is finished. */ 68 fun finish() 69 70 /** Indicates what result should be set for the returning callee */ 71 fun setResultExternal(resultCode: Int) 72 73 /** Indicates the settings UI should be shown */ 74 fun showSettings(enrolledFingerprints: List<FingerprintData>) 75 76 /** Updates the add fingerprints preference */ 77 fun updateAddFingerprintsPreference(canEnroll: Boolean, maxFingerprints: Int) 78 79 /** Updates the sfps fingerprints preference */ 80 fun updateSfpsPreference(isSfpsPrefVisible: Boolean) 81 82 /** Indicates that a user has been locked out */ 83 fun userLockout(authAttemptViewModel: FingerprintAuthAttemptModel.Error) 84 85 /** Indicates a fingerprint preference should be highlighted */ 86 suspend fun highlightPref(fingerId: Int) 87 88 /** Indicates a user should be prompted to delete a fingerprint */ 89 suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintData): Boolean 90 91 /** Indicates a user should be asked to renae ma dialog */ 92 suspend fun askUserToRenameDialog( 93 fingerprintViewModel: FingerprintData 94 ): Pair<FingerprintData, String>? 95 } 96 97 fun bind( 98 view: FingerprintView, 99 viewModel: FingerprintSettingsViewModel, 100 navigationViewModel: FingerprintSettingsNavigationViewModel, 101 lifecycleScope: LifecycleCoroutineScope, 102 ) { 103 104 /** Result listener for launching enrollments **after** a user has reached the settings page. */ 105 106 // Settings display flow 107 lifecycleScope.launch { viewModel.enrolledFingerprints.collect { view.showSettings(it) } } 108 lifecycleScope.launch { 109 viewModel.addFingerprintPrefInfo.collect { (enablePref, maxFingerprints) -> 110 view.updateAddFingerprintsPreference(enablePref, maxFingerprints) 111 } 112 } 113 lifecycleScope.launch { viewModel.isSfpsPrefVisible.collect { view.updateSfpsPreference(it) } } 114 115 // Dialog flow 116 lifecycleScope.launch { 117 viewModel.isShowingDialog.collectLatest { 118 if (it == null) { 119 return@collectLatest 120 } 121 when (it) { 122 is PreferenceViewModel.RenameDialog -> { 123 val willRename = view.askUserToRenameDialog(it.fingerprintViewModel) 124 if (willRename != null) { 125 Log.d(TAG, "renaming fingerprint $it") 126 viewModel.renameFingerprint(willRename.first, willRename.second) 127 } 128 viewModel.onRenameDialogFinished() 129 } 130 is PreferenceViewModel.DeleteDialog -> { 131 if (view.askUserToDeleteDialog(it.fingerprintViewModel)) { 132 Log.d(TAG, "deleting fingerprint $it") 133 viewModel.deleteFingerprint(it.fingerprintViewModel) 134 } 135 viewModel.onDeleteDialogFinished() 136 } 137 } 138 } 139 } 140 141 // Auth flow 142 lifecycleScope.launch { 143 viewModel.authFlow.filterNotNull().collect { 144 when (it) { 145 is FingerprintAuthAttemptModel.Success -> { 146 view.highlightPref(it.fingerId) 147 } 148 is FingerprintAuthAttemptModel.Error -> { 149 if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) { 150 view.userLockout(it) 151 } 152 } 153 } 154 } 155 } 156 157 // Launch this on Dispatchers.Default and not main. 158 // Otherwise it takes too long for state transitions such as PIN/PATTERN/PASS 159 // to enrollment, which makes gives the user a janky experience. 160 lifecycleScope.launch(Dispatchers.Default) { 161 var settingsShowingJob: Job? = null 162 navigationViewModel.nextStep.filterNotNull().collect { nextStep -> 163 settingsShowingJob?.cancel() 164 settingsShowingJob = null 165 Log.d(TAG, "next step = $nextStep") 166 when (nextStep) { 167 is EnrollFirstFingerprint -> 168 view.launchFullFingerprintEnrollment( 169 nextStep.userId, 170 nextStep.gateKeeperPasswordHandle, 171 nextStep.challenge, 172 nextStep.challengeToken, 173 ) 174 is EnrollAdditionalFingerprint -> 175 view.launchAddFingerprint(nextStep.userId, nextStep.challengeToken) 176 is LaunchConfirmDeviceCredential -> view.launchConfirmOrChooseLock(nextStep.userId) 177 is FinishSettings -> { 178 Log.d(TAG, "Finishing due to ${nextStep.reason}") 179 view.finish() 180 } 181 is FinishSettingsWithResult -> { 182 Log.d(TAG, "Finishing with result ${nextStep.result} due to ${nextStep.reason}") 183 view.setResultExternal(nextStep.result) 184 view.finish() 185 } 186 is ShowSettings -> Log.d(TAG, "Showing settings") 187 is LaunchedActivity -> Log.d(TAG, "Launched activity, awaiting result") 188 } 189 } 190 } 191 } 192 } 193