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