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