1 /*
2  * 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.viewmodel
18 
19 import android.hardware.fingerprint.FingerprintManager
20 import androidx.lifecycle.ViewModel
21 import androidx.lifecycle.ViewModelProvider
22 import androidx.lifecycle.viewModelScope
23 import com.android.settings.biometrics.BiometricEnrollBase
24 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
25 import kotlinx.coroutines.CoroutineDispatcher
26 import kotlinx.coroutines.flow.MutableStateFlow
27 import kotlinx.coroutines.flow.StateFlow
28 import kotlinx.coroutines.flow.asStateFlow
29 import kotlinx.coroutines.flow.last
30 import kotlinx.coroutines.flow.update
31 import kotlinx.coroutines.launch
32 
33 /** A Viewmodel that represents the navigation of the FingerprintSettings activity. */
34 class FingerprintSettingsNavigationViewModel(
35   private val userId: Int,
36   private val fingerprintManagerInteractor: FingerprintManagerInteractor,
37   private val backgroundDispatcher: CoroutineDispatcher,
38   tokenInit: ByteArray?,
39   challengeInit: Long?,
40 ) : ViewModel() {
41 
42   private var token = tokenInit
43   private var challenge = challengeInit
44 
45   private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
46 
47   /** This flow represents the high level state for the FingerprintSettingsV2Fragment. */
48   val nextStep: StateFlow<NextStepViewModel?> = _nextStep.asStateFlow()
49 
50   init {
51     if (challengeInit == null || tokenInit == null) {
<lambda>null52       _nextStep.update { LaunchConfirmDeviceCredential(userId) }
53     } else {
<lambda>null54       viewModelScope.launch {
55         if (fingerprintManagerInteractor.enrolledFingerprints.last()?.isEmpty() == true) {
56           _nextStep.update { EnrollFirstFingerprint(userId, null, challenge, token) }
57         } else {
58           showSettingsHelper()
59         }
60       }
61     }
62   }
63 
64   /** Used to indicate that FingerprintSettings is complete. */
finishnull65   fun finish() {
66     _nextStep.update { null }
67   }
68 
69   /** Used to finish settings in certain cases. */
maybeFinishActivitynull70   fun maybeFinishActivity(changingConfig: Boolean) {
71     val isConfirmingOrEnrolling =
72       _nextStep.value is LaunchConfirmDeviceCredential ||
73         _nextStep.value is EnrollAdditionalFingerprint ||
74         _nextStep.value is EnrollFirstFingerprint ||
75         _nextStep.value is LaunchedActivity
76     if (!isConfirmingOrEnrolling && !changingConfig)
77       _nextStep.update {
78         FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings")
79       }
80   }
81 
82   /** Used to indicate that we have launched another activity and we should await its result. */
setStepToLaunchednull83   fun setStepToLaunched() {
84     _nextStep.update { LaunchedActivity }
85   }
86 
87   /** Indicates a successful enroll has occurred */
onEnrollSuccessnull88   fun onEnrollSuccess() {
89     showSettingsHelper()
90   }
91 
92   /** Add fingerprint clicked */
onAddFingerprintClickednull93   fun onAddFingerprintClicked() {
94     _nextStep.update { EnrollAdditionalFingerprint(userId, token) }
95   }
96 
97   /** Enrolling of an additional fingerprint failed */
onEnrollAdditionalFailurenull98   fun onEnrollAdditionalFailure() {
99     launchFinishSettings("Failed to enroll additional fingerprint")
100   }
101 
102   /** The first fingerprint enrollment failed */
onEnrollFirstFailurenull103   fun onEnrollFirstFailure(reason: String) {
104     launchFinishSettings(reason)
105   }
106 
107   /** The first fingerprint enrollment failed with a result code */
onEnrollFirstFailurenull108   fun onEnrollFirstFailure(reason: String, resultCode: Int) {
109     launchFinishSettings(reason, resultCode)
110   }
111 
112   /** Notifies that a users first enrollment succeeded. */
onEnrollFirstnull113   fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) {
114     if (theToken == null) {
115       launchFinishSettings("Error, empty token")
116       return
117     }
118     if (theChallenge == null) {
119       launchFinishSettings("Error, empty keyChallenge")
120       return
121     }
122     token = theToken
123     challenge = theChallenge
124 
125     showSettingsHelper()
126   }
127 
128   /**
129    * Indicates to the view model that a confirm device credential action has been completed with a
130    * [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as
131    * [FingerprintManager.enroll].
132    */
onConfirmDevicenull133   suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
134     if (!wasSuccessful) {
135       launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
136       return
137     }
138     if (theGateKeeperPasswordHandle == null) {
139       launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
140       return
141     }
142 
143     launchEnrollNextStep(theGateKeeperPasswordHandle)
144   }
145 
showSettingsHelpernull146   private fun showSettingsHelper() {
147     _nextStep.update { ShowSettings }
148   }
149 
launchEnrollNextStepnull150   private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) {
151     fingerprintManagerInteractor.enrolledFingerprints.collect {
152       if (it?.isEmpty() == true) {
153         _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) }
154       } else {
155         viewModelScope.launch(backgroundDispatcher) {
156           val challengePair =
157             fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!)
158           challenge = challengePair.first
159           token = challengePair.second
160 
161           showSettingsHelper()
162         }
163       }
164     }
165   }
166 
launchFinishSettingsnull167   private fun launchFinishSettings(reason: String) {
168     _nextStep.update { FinishSettings(reason) }
169   }
170 
launchFinishSettingsnull171   private fun launchFinishSettings(reason: String, errorCode: Int) {
172     _nextStep.update { FinishSettingsWithResult(errorCode, reason) }
173   }
174 
175   class FingerprintSettingsNavigationModelFactory(
176     private val userId: Int,
177     private val interactor: FingerprintManagerInteractor,
178     private val backgroundDispatcher: CoroutineDispatcher,
179     private val token: ByteArray?,
180     private val challenge: Long?,
181   ) : ViewModelProvider.Factory {
182 
183     @Suppress("UNCHECKED_CAST")
createnull184     override fun <T : ViewModel> create(modelClass: Class<T>): T {
185 
186       return FingerprintSettingsNavigationViewModel(
187         userId,
188         interactor,
189         backgroundDispatcher,
190         token,
191         challenge,
192       )
193         as T
194     }
195   }
196 }
197