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