1 /*
2  * Copyright 2024 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 package androidx.credentials.provider
17 
18 import android.hardware.biometrics.BiometricPrompt
19 import android.util.Log
20 import androidx.annotation.RestrictTo
21 import java.util.Objects
22 import org.jetbrains.annotations.VisibleForTesting
23 
24 /**
25  * Error returned from the Biometric Prompt flow that is executed by
26  * [androidx.credentials.CredentialManager] after the user makes a selection on the Credential
27  * Manager account selector.
28  *
29  * @property errorCode the error code denoting what kind of error was encountered while the
30  *   biometric prompt flow failed, must be one of the error codes defined in
31  *   [androidx.biometric.BiometricPrompt] such as
32  *   [androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE] or
33  *   [androidx.biometric.BiometricPrompt.ERROR_TIMEOUT]
34  * @property errorMsg the message associated with the [errorCode] in the form that can be displayed
35  *   on a UI.
36  * @see AuthenticationErrorTypes
37  */
38 class AuthenticationError
39 @JvmOverloads
40 constructor(
41     val errorCode: @AuthenticationErrorTypes Int,
42     val errorMsg: CharSequence? = null,
43 ) {
44     internal companion object {
45         internal val TAG = "AuthenticationError"
46         @VisibleForTesting
47         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
48         const val EXTRA_BIOMETRIC_AUTH_ERROR =
49             "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_CODE"
50         @VisibleForTesting
51         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
52         const val EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK = "BIOMETRIC_AUTH_ERROR_CODE"
53         @VisibleForTesting
54         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
55         const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE =
56             "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_MESSAGE"
57         @VisibleForTesting
58         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
59         const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK = "BIOMETRIC_AUTH_ERROR_MESSAGE"
60         // The majority of this is unexpected to be sent, or the values are equal,
61         // but should it arrive for any reason, is handled properly. This way
62         // providers can be confident the Jetpack codes alone are enough.
63         @VisibleForTesting
64         internal val biometricFrameworkToJetpackErrorMap =
65             linkedMapOf(
66                 BiometricPrompt.BIOMETRIC_ERROR_CANCELED to
67                     androidx.biometric.BiometricPrompt.ERROR_CANCELED,
68                 BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT to
69                     androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT,
70                 BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE to
71                     androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE,
72                 BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT to
73                     androidx.biometric.BiometricPrompt.ERROR_LOCKOUT,
74                 BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to
75                     androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT,
76                 BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS to
77                     androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS,
78                 BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to
79                     androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,
80                 BiometricPrompt.BIOMETRIC_ERROR_NO_SPACE to
81                     androidx.biometric.BiometricPrompt.ERROR_NO_SPACE,
82                 BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to
83                     androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED,
84                 BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT to
85                     androidx.biometric.BiometricPrompt.ERROR_TIMEOUT,
86                 BiometricPrompt.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to
87                     androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
88                 BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED to
89                     androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED,
90                 BiometricPrompt.BIOMETRIC_ERROR_VENDOR to
91                     androidx.biometric.BiometricPrompt.ERROR_VENDOR
92                 // TODO(b/340334264) : Add NEGATIVE_BUTTON from FW once avail, or wrap this in
93                 // a credential manager specific error.
94             )
95 
convertFrameworkBiometricErrorToJetpacknull96         internal fun convertFrameworkBiometricErrorToJetpack(frameworkCode: Int): Int {
97             // Ignoring getOrDefault to allow this object down to API 21
98             return if (biometricFrameworkToJetpackErrorMap.containsKey(frameworkCode)) {
99                 biometricFrameworkToJetpackErrorMap[frameworkCode]!!
100             } else {
101                 Log.i(TAG, "Unexpected error code, $frameworkCode, ")
102                 frameworkCode
103             }
104         }
105 
106         /**
107          * Generates an instance of this class, to be called by an UI consumer that calls
108          * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
109          * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
110          * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
111          * [isFrameworkBiometricPrompt].
112          *
113          * @param uiErrorCode the error code used to create this error instance, typically using the
114          *   [androidx.biometric.BiometricPrompt]'s constants if conversion isn't desired, or
115          *   [android.hardware.biometrics.BiometricPrompt]'s constants if conversion *is* desired.
116          * @param uiErrorMessage the message associated with the [uiErrorCode] in the form that can
117          *   be displayed on a UI.
118          * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
119          *   requires conversion or not, set to true by default
120          * @return an authentication error that has properly handled conversion of the err code
121          */
122         @JvmOverloads
123         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
createFromnull124         internal fun createFrom(
125             uiErrorCode: Int,
126             uiErrorMessage: CharSequence,
127             isFrameworkBiometricPrompt: Boolean = true,
128         ): AuthenticationError =
129             AuthenticationError(
130                 errorCode =
131                     if (isFrameworkBiometricPrompt)
132                         convertFrameworkBiometricErrorToJetpack(uiErrorCode)
133                     else uiErrorCode,
134                 errorMsg = uiErrorMessage,
135             )
136     }
137 
138     override fun equals(other: Any?): Boolean {
139         if (this === other) {
140             return true
141         }
142         if (other is AuthenticationError) {
143             return this.errorCode == other.errorCode && this.errorMsg == other.errorMsg
144         }
145         return false
146     }
147 
hashCodenull148     override fun hashCode(): Int {
149         return Objects.hash(this.errorCode, this.errorMsg)
150     }
151 }
152