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