1 package com.android.systemui.biometrics.ui.viewmodel
2 
3 import android.content.Context
4 import android.graphics.drawable.Drawable
5 import android.text.InputType
6 import com.android.internal.widget.LockPatternView
7 import com.android.systemui.R
8 import com.android.systemui.biometrics.Utils
9 import com.android.systemui.biometrics.domain.interactor.CredentialStatus
10 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
11 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
12 import com.android.systemui.biometrics.shared.model.BiometricUserInfo
13 import com.android.systemui.dagger.qualifiers.Application
14 import javax.inject.Inject
15 import kotlin.reflect.KClass
16 import kotlinx.coroutines.flow.Flow
17 import kotlinx.coroutines.flow.MutableSharedFlow
18 import kotlinx.coroutines.flow.MutableStateFlow
19 import kotlinx.coroutines.flow.asSharedFlow
20 import kotlinx.coroutines.flow.asStateFlow
21 import kotlinx.coroutines.flow.combine
22 import kotlinx.coroutines.flow.filterIsInstance
23 import kotlinx.coroutines.flow.map
24 
25 /** View-model for all CredentialViews within BiometricPrompt. */
26 class CredentialViewModel
27 @Inject
28 constructor(
29     @Application private val applicationContext: Context,
30     private val credentialInteractor: PromptCredentialInteractor,
31 ) {
32 
33     /** Top level information about the prompt. */
34     val header: Flow<CredentialHeaderViewModel> =
35         credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map {
36             request ->
37             BiometricPromptHeaderViewModelImpl(
38                 request,
39                 user = request.userInfo,
40                 title = request.title,
41                 subtitle = request.subtitle,
42                 description = request.description,
43                 icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId),
44             )
45         }
46 
47     /** Input flags for text based credential views */
48     val inputFlags: Flow<Int?> =
49         credentialInteractor.prompt.map {
50             when (it) {
51                 is BiometricPromptRequest.Credential.Pin ->
52                     InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
53                 else -> null
54             }
55         }
56 
57     /** If stealth mode is active (hide user credential input). */
58     val stealthMode: Flow<Boolean> =
59         credentialInteractor.prompt.map {
60             when (it) {
61                 is BiometricPromptRequest.Credential.Pattern -> it.stealthMode
62                 else -> false
63             }
64         }
65 
66     private val _animateContents: MutableStateFlow<Boolean> = MutableStateFlow(true)
67     /** If this view should be animated on transitions. */
68     val animateContents = _animateContents.asStateFlow()
69 
70     /** Error messages to show the user. */
71     val errorMessage: Flow<String> =
72         combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p ->
73             when (error) {
74                 is CredentialStatus.Fail.Error -> error.error
75                         ?: applicationContext.asBadCredentialErrorMessage(p)
76                 is CredentialStatus.Fail.Throttled -> error.error
77                 null -> ""
78             }
79         }
80 
81     private val _validatedAttestation: MutableSharedFlow<ByteArray?> = MutableSharedFlow()
82     /** Results of [checkPatternCredential]. A non-null attestation is supplied on success. */
83     val validatedAttestation: Flow<ByteArray?> = _validatedAttestation.asSharedFlow()
84 
85     private val _remainingAttempts: MutableStateFlow<RemainingAttempts> =
86         MutableStateFlow(RemainingAttempts())
87     /** If set, the number of remaining attempts before the user must stop. */
88     val remainingAttempts: Flow<RemainingAttempts> = _remainingAttempts.asStateFlow()
89 
90     /** Enable transition animations. */
91     fun setAnimateContents(animate: Boolean) {
92         _animateContents.value = animate
93     }
94 
95     /** Show an error message to inform the user the pattern is too short to attempt validation. */
96     fun showPatternTooShortError() {
97         credentialInteractor.setVerificationError(
98             CredentialStatus.Fail.Error(
99                 applicationContext.asBadCredentialErrorMessage(
100                     BiometricPromptRequest.Credential.Pattern::class
101                 )
102             )
103         )
104     }
105 
106     /** Reset the error message to an empty string. */
107     fun resetErrorMessage() {
108         credentialInteractor.resetVerificationError()
109     }
110 
111     /** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */
112     suspend fun checkCredential(text: CharSequence, header: CredentialHeaderViewModel) =
113         checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text))
114 
115     /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
116     suspend fun checkCredential(
117         pattern: List<LockPatternView.Cell>,
118         header: CredentialHeaderViewModel
119     ) = checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
120 
121     private suspend fun checkCredential(result: CredentialStatus) {
122         when (result) {
123             is CredentialStatus.Success.Verified -> {
124                 _validatedAttestation.emit(result.hat)
125                 _remainingAttempts.value = RemainingAttempts()
126             }
127             is CredentialStatus.Fail.Error -> {
128                 _validatedAttestation.emit(null)
129                 _remainingAttempts.value =
130                     RemainingAttempts(result.remainingAttempts, result.urgentMessage ?: "")
131             }
132             is CredentialStatus.Fail.Throttled -> {
133                 // required for completeness, but a throttled error cannot be the final result
134                 _validatedAttestation.emit(null)
135                 _remainingAttempts.value = RemainingAttempts()
136             }
137         }
138     }
139 }
140 
141 private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String =
142     asBadCredentialErrorMessage(
143         if (prompt != null) prompt::class else BiometricPromptRequest.Credential.Password::class
144     )
145 
146 private fun <T : BiometricPromptRequest> Context.asBadCredentialErrorMessage(
147     clazz: KClass<T>
148 ): String =
149     getString(
150         when (clazz) {
151             BiometricPromptRequest.Credential.Pin::class -> R.string.biometric_dialog_wrong_pin
152             BiometricPromptRequest.Credential.Password::class ->
153                 R.string.biometric_dialog_wrong_password
154             BiometricPromptRequest.Credential.Pattern::class ->
155                 R.string.biometric_dialog_wrong_pattern
156             else -> R.string.biometric_dialog_wrong_password
157         }
158     )
159 
160 private fun Context.asLockIcon(userId: Int): Drawable {
161     val id =
162         if (Utils.isManagedProfile(this, userId)) {
163             R.drawable.auth_dialog_enterprise
164         } else {
165             R.drawable.auth_dialog_lock
166         }
167     return resources.getDrawable(id, theme)
168 }
169 
170 private class BiometricPromptHeaderViewModelImpl(
171     val request: BiometricPromptRequest.Credential,
172     override val user: BiometricUserInfo,
173     override val title: String,
174     override val subtitle: String,
175     override val description: String,
176     override val icon: Drawable,
177 ) : CredentialHeaderViewModel
178 
179 private fun CredentialHeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
180     (this as BiometricPromptHeaderViewModelImpl).request
181