Skip to content

Commit ea78f1e

Browse files
author
Niharika Arora
committed
Create BiometricErrorUtils to handle biometric validation method used in 4 classes and updated strings as suggested
1 parent 6a043d3 commit ea78f1e

File tree

8 files changed

+140
-201
lines changed

8 files changed

+140
-201
lines changed

CredentialProvider/MyVault/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The app demonstrates how to:
1414
- Register as a `CredentialProviderService` so that users can store and retrieve passwords and passkeys using the app.
1515
- Save passwords/passkeys to the app. These are stored locally in a database for demonstration purposes only. In a real app this data should be sent to a server to allow the user's credentials to be synchronized across all their devices.
1616
- Retrieve credentials from the app to assist with user login in another app or website.
17-
- Implement your own biometrics prompt(single tap credential creation & sign-in)
17+
- Implement your own biometric prompts for single-tap credential creation and sign-in.
1818
- Delete passkeys or passwords.
1919

2020
# Requirements
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2025 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+
* https://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 com.example.android.authentication.myvault
17+
18+
import android.annotation.SuppressLint
19+
import android.content.Context
20+
import androidx.credentials.provider.BiometricPromptResult
21+
22+
/**
23+
* Utility class for handling biometric authentication errors.
24+
*
25+
* <p>This class provides a collection of static utility methods for managing
26+
* and processing errors that may occur during biometric authentication flows.
27+
* It encapsulates the logic for extracting error information from
28+
* {@link BiometricPromptResult} objects and constructing user-friendly error
29+
* messages.
30+
*
31+
* <p>The primary function of this class is to centralize the error-handling
32+
* logic related to biometric authentication, promoting code reuse and
33+
* maintainability.
34+
*/
35+
object BiometricErrorUtils {
36+
37+
/**
38+
* Checks if there was an error during the biometric authentication flow and returns an error message if so.
39+
*
40+
* <p>This method determines whether the biometric authentication flow resulted in
41+
* an error. It checks if the {@link BiometricPromptResult} is null or if the
42+
* authentication was successful. If neither of these conditions is met, it
43+
* extracts the error code and message from the {@link BiometricPromptResult},
44+
* constructs an error message, and returns it.
45+
*
46+
* <p>The error message is built using the following format:
47+
* "Biometric Error Code: [errorCode] [errorMessage] Other providers may be available."
48+
*
49+
* @param context The context used to retrieve string resources.
50+
* @param biometricPromptResult The result of the biometric authentication prompt.
51+
* @return An error message if there was an error during the biometric flow, or an empty string otherwise.
52+
*/
53+
@SuppressLint("StringFormatMatches")
54+
fun getBiometricErrorMessage(
55+
context: Context,
56+
biometricPromptResult: BiometricPromptResult?,
57+
): String {
58+
// If the biometricPromptResult is null, there was no error.
59+
if (biometricPromptResult == null) return context.getString(R.string.empty)
60+
61+
// If the biometricPromptResult indicates success, there was no error.
62+
if (biometricPromptResult.isSuccessful) return context.getString(R.string.empty)
63+
64+
// Initialize default values for the error code and message.
65+
var biometricAuthErrorCode = -1
66+
var biometricAuthErrorMsg = context.getString(R.string.unknown_failure)
67+
68+
// Check if there is an authentication error in the biometricPromptResult.
69+
if (biometricPromptResult.authenticationError != null) {
70+
// Extract the error code and message from the authentication error.
71+
biometricAuthErrorCode = biometricPromptResult.authenticationError!!.errorCode
72+
biometricAuthErrorMsg = biometricPromptResult.authenticationError!!.errorMsg.toString()
73+
}
74+
75+
// Build the error message to be sent to the client.
76+
val errorMessage = buildString {
77+
append(
78+
context.getString(
79+
R.string.biometric_error_code_with_message,
80+
biometricAuthErrorCode,
81+
),
82+
)
83+
append(biometricAuthErrorMsg)
84+
append(context.getString(R.string.other_providers_error_message))
85+
}
86+
87+
// Indicate that there was an error during the biometric flow.
88+
return errorMessage
89+
}
90+
}

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -186,26 +186,26 @@ class CredentialsRepository(
186186
/**
187187
* Configures a {@link PasswordCredentialEntry.Builder} for a given password item.
188188
*
189-
* @param passwordItemCurrent The {@link PasswordItem} containing the password details.
189+
* @param currentPasswordItem The {@link PasswordItem} containing the password details.
190190
* @param option The {@link BeginGetPasswordOption} containing the request parameters.
191191
* @return A {@link PasswordCredentialEntry.Builder} configured with the provided
192192
* password details and request options.
193193
*/
194194
private fun configurePasswordCredentialEntryBuilder(
195-
passwordItemCurrent: PasswordItem,
195+
currentPasswordItem: PasswordItem,
196196
option: BeginGetPasswordOption,
197197
): PasswordCredentialEntry.Builder {
198198
val entryBuilder = PasswordCredentialEntry.Builder(
199199
applicationContext,
200-
passwordItemCurrent.username,
200+
currentPasswordItem.username,
201201
createNewPendingIntent(
202-
passwordItemCurrent.username,
202+
currentPasswordItem.username,
203203
GET_PASSWORD_INTENT,
204204
),
205205
option,
206-
).setDisplayName("display-${passwordItemCurrent.username}")
206+
).setDisplayName("display-${currentPasswordItem.username}")
207207
.setIcon(AppDependencies.providerIcon!!)
208-
.setLastUsedTime(Instant.ofEpochMilli(passwordItemCurrent.lastUsedTimeMs))
208+
.setLastUsedTime(Instant.ofEpochMilli(currentPasswordItem.lastUsedTimeMs))
209209
return entryBuilder
210210
}
211211

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasskeyActivity.kt

+11-49
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package com.example.android.authentication.myvault.ui
1717

18-
import android.annotation.SuppressLint
1918
import android.app.Activity
2019
import android.content.Intent
2120
import android.content.pm.SigningInfo
@@ -29,12 +28,12 @@ import androidx.biometric.BiometricPrompt.PromptInfo.Builder
2928
import androidx.credentials.CreatePublicKeyCredentialRequest
3029
import androidx.credentials.CreatePublicKeyCredentialResponse
3130
import androidx.credentials.exceptions.GetCredentialUnknownException
32-
import androidx.credentials.provider.BiometricPromptResult
3331
import androidx.credentials.provider.CallingAppInfo
3432
import androidx.credentials.provider.PendingIntentHandler
3533
import androidx.credentials.provider.ProviderCreateCredentialRequest
3634
import androidx.fragment.app.FragmentActivity
3735
import com.example.android.authentication.myvault.AppDependencies
36+
import com.example.android.authentication.myvault.BiometricErrorUtils
3837
import com.example.android.authentication.myvault.R
3938
import com.example.android.authentication.myvault.data.PasskeyMetadata
4039
import com.example.android.authentication.myvault.fido.AssetLinkVerifier
@@ -102,11 +101,18 @@ class CreatePasskeyActivity : FragmentActivity() {
102101
private fun handleCreatePublicKeyCredentialRequest(request: ProviderCreateCredentialRequest) {
103102
val accountId = intent.getStringExtra(KEY_ACCOUNT_ID)
104103

105-
// Retrieve the biometric prompt result from the request.
104+
// Retrieve the BiometricPromptResult from the request.
106105
val biometricPromptResult = request.biometricPromptResult
107106

108-
// Check if there was an error during the biometric flow. If so, handle the error and return.
109-
if (isValidBiometricFlowError(biometricPromptResult)) return
107+
// Validate the error message in biometric result
108+
val biometricErrorMessage =
109+
BiometricErrorUtils.getBiometricErrorMessage(this, biometricPromptResult)
110+
111+
// If there's valid biometric error, set up the failure response and finish.
112+
if (biometricErrorMessage.isNotEmpty()) {
113+
setUpFailureResponseAndFinish(biometricErrorMessage)
114+
return
115+
}
110116

111117
// access the associated intent and pass it into the PendingIntentHandler class to get the ProviderCreateCredentialRequest.
112118
if (request.callingRequest is CreatePublicKeyCredentialRequest) {
@@ -138,50 +144,6 @@ class CreatePasskeyActivity : FragmentActivity() {
138144
}
139145
}
140146

141-
/**
142-
* Checks if there was an error during the biometric authentication flow.
143-
*
144-
* This method determines whether the biometric authentication flow resulted in
145-
* an error. It checks if the {@link BiometricPromptResult} is null or if the
146-
* authentication was successful. If neither of these conditions is met, it
147-
* extracts the error code and message from the {@link BiometricPromptResult}
148-
* and sets up a failure response to be sent to the client.
149-
*
150-
* @param biometricPromptResult The result of the biometric authentication prompt.
151-
* @return True if there was an error during the biometric flow, false otherwise.
152-
*/
153-
@SuppressLint("StringFormatMatches")
154-
private fun isValidBiometricFlowError(biometricPromptResult: BiometricPromptResult?): Boolean {
155-
// If the biometricPromptResult is null, there was no error.
156-
if (biometricPromptResult == null) return false
157-
if (biometricPromptResult.isSuccessful) return false
158-
159-
// Initialize default values for the error code and message.
160-
var biometricAuthErrorCode = -1
161-
var biometricAuthErrorMsg = getString(R.string.unknown_failure)
162-
163-
// Check if there is an authentication error in the biometricPromptResult.
164-
if (biometricPromptResult.authenticationError != null) {
165-
biometricAuthErrorCode = biometricPromptResult.authenticationError!!.errorCode
166-
biometricAuthErrorMsg = biometricPromptResult.authenticationError!!.errorMsg.toString()
167-
}
168-
169-
// Build the error message to be sent to the client.
170-
setUpFailureResponseAndFinish(
171-
buildString {
172-
append(
173-
getString(
174-
R.string.biometric_error_code_with_message,
175-
biometricAuthErrorCode,
176-
),
177-
)
178-
append(biometricAuthErrorMsg)
179-
append(getString(R.string.other_providers_error_message))
180-
},
181-
)
182-
return true
183-
}
184-
185147
/**
186148
* This method validates the digital asset linking to verify the app identity,
187149
* surface biometric prompt and sends back response to client app for passkey created

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasswordActivity.kt

+10-31
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package com.example.android.authentication.myvault.ui
1717

18-
import android.annotation.SuppressLint
1918
import android.content.Intent
2019
import android.os.Bundle
2120
import androidx.activity.ComponentActivity
@@ -24,11 +23,11 @@ import androidx.activity.enableEdgeToEdge
2423
import androidx.compose.runtime.rememberCoroutineScope
2524
import androidx.credentials.CreatePasswordRequest
2625
import androidx.credentials.CreatePasswordResponse
27-
import androidx.credentials.provider.BiometricPromptResult
2826
import androidx.credentials.provider.PendingIntentHandler
2927
import androidx.credentials.provider.ProviderCreateCredentialRequest
3028
import androidx.lifecycle.lifecycleScope
3129
import com.example.android.authentication.myvault.AppDependencies
30+
import com.example.android.authentication.myvault.BiometricErrorUtils
3231
import com.example.android.authentication.myvault.data.PasswordMetaData
3332
import com.example.android.authentication.myvault.ui.password.PasswordScreen
3433
import kotlinx.coroutines.launch
@@ -61,11 +60,17 @@ class CreatePasswordActivity : ComponentActivity() {
6160
accountId: String?,
6261
) {
6362
if (createRequest != null) {
64-
// Retrieve the biometric prompt result from the createRequest.
63+
// Retrieve the BiometricPromptResult from the request.
6564
val biometricPromptResult = createRequest.biometricPromptResult
6665

67-
// Check if there was an error during the biometric flow. If so, handle the error and return.
68-
if (isValidBiometricFlowError(biometricPromptResult)) return
66+
// Validate the error message in biometric result
67+
val biometricErrorMessage =
68+
BiometricErrorUtils.getBiometricErrorMessage(this, biometricPromptResult)
69+
70+
// If there's valid biometric error, set up the failure response and finish.
71+
if (biometricErrorMessage.isNotEmpty()) {
72+
return
73+
}
6974

7075
if (createRequest.callingRequest is CreatePasswordRequest) {
7176
val request: CreatePasswordRequest =
@@ -148,32 +153,6 @@ class CreatePasswordActivity : ComponentActivity() {
148153
}
149154
}
150155

151-
/**
152-
* Checks if there was an error during the biometric authentication flow.
153-
*
154-
* This method determines whether the biometric authentication flow resulted in
155-
* an error. It checks if the {@link BiometricPromptResult} is null or if the
156-
* authentication was successful. If neither of these conditions is met, it
157-
* returns true, indicating an error. Otherwise, it returns false.
158-
*
159-
* Note: This method does not handle the error itself. It only determines
160-
* if an error occurred. The error handling is expected to be done elsewhere.
161-
*
162-
* @param biometricPromptResult The result of the biometric authentication prompt.
163-
* @return True if there was an error during the biometric flow, false otherwise.
164-
*/
165-
@SuppressLint("StringFormatMatches")
166-
private fun isValidBiometricFlowError(biometricPromptResult: BiometricPromptResult?): Boolean {
167-
// If the biometricPromptResult is null, there was no error.
168-
if (biometricPromptResult == null) return false
169-
170-
// If the biometricPromptResult indicates success, there was no error.
171-
if (biometricPromptResult.isSuccessful) return false
172-
173-
// If we reach this point, there was an error during the biometric flow.
174-
return true
175-
}
176-
177156
/**
178157
* Saves the password and sets the response back to the calling app.
179158
*

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasskeyActivity.kt

+10-57
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package com.example.android.authentication.myvault.ui
1717

18-
import android.annotation.SuppressLint
1918
import android.app.Activity
2019
import android.content.Intent
2120
import android.content.pm.SigningInfo
@@ -37,6 +36,7 @@ import androidx.credentials.provider.PendingIntentHandler
3736
import androidx.credentials.provider.ProviderGetCredentialRequest
3837
import androidx.fragment.app.FragmentActivity
3938
import com.example.android.authentication.myvault.AppDependencies
39+
import com.example.android.authentication.myvault.BiometricErrorUtils
4040
import com.example.android.authentication.myvault.R
4141
import com.example.android.authentication.myvault.data.PasskeyItem
4242
import com.example.android.authentication.myvault.fido.AssetLinkVerifier
@@ -118,8 +118,15 @@ class GetPasskeyActivity : FragmentActivity() {
118118
// Retrieve the BiometricPromptResult from the request.
119119
val biometricPromptResult = request.biometricPromptResult
120120

121-
// Check if there was an error in the biometric flow.
122-
if (isValidBiometricFlowError(biometricPromptResult)) return
121+
// Validate the error message in biometric result
122+
val biometricErrorMessage =
123+
BiometricErrorUtils.getBiometricErrorMessage(this, biometricPromptResult)
124+
125+
// If there's valid biometric error, set up the failure response and finish.
126+
if (biometricErrorMessage.isNotEmpty()) {
127+
setUpFailureResponseAndFinish(biometricErrorMessage)
128+
return
129+
}
123130

124131
// Configure the passkey assertion.
125132
configurePasskeyAssertion(
@@ -222,60 +229,6 @@ class GetPasskeyActivity : FragmentActivity() {
222229
}
223230
}
224231

225-
/**
226-
* Checks if there was an error during the biometric authentication flow.
227-
*
228-
* <p>This method determines whether the biometric authentication flow resulted in
229-
* an error. It checks if the {@link BiometricPromptResult} is null or if the
230-
* authentication was successful. If neither of these conditions is met, it
231-
* extracts the error code and message from the {@link BiometricPromptResult},
232-
* constructs an error message, and sets up a failure response to be sent to
233-
* the client.
234-
*
235-
* <p>The error message is built using the following format:
236-
* "Biometric Error Code: [errorCode] [errorMessage] Other providers may be available."
237-
*
238-
* @param biometricPromptResult The result of the biometric authentication prompt.
239-
* @return True if there was an error during the biometric flow, false otherwise.
240-
*/
241-
@SuppressLint("StringFormatMatches")
242-
private fun isValidBiometricFlowError(biometricPromptResult: BiometricPromptResult?): Boolean {
243-
// If the biometricPromptResult is null, there was no error.
244-
if (biometricPromptResult == null) return false
245-
246-
// If the biometricPromptResult indicates success, there was no error.
247-
if (biometricPromptResult.isSuccessful) return false
248-
249-
// Initialize default values for the error code and message.
250-
var biometricAuthErrorCode = -1
251-
var biometricAuthErrorMsg = getString(R.string.unknown_failure)
252-
253-
// Check if there is an authentication error in the biometricPromptResult.
254-
if (biometricPromptResult.authenticationError != null) {
255-
// Extract the error code and message from the authentication error.
256-
biometricAuthErrorCode = biometricPromptResult.authenticationError!!.errorCode
257-
biometricAuthErrorMsg = biometricPromptResult.authenticationError!!.errorMsg.toString()
258-
}
259-
260-
// Build the error message to be sent to the client.
261-
val errorMessage = buildString {
262-
append(
263-
getString(
264-
R.string.biometric_error_code_with_message,
265-
biometricAuthErrorCode,
266-
),
267-
)
268-
append(biometricAuthErrorMsg)
269-
append(getString(R.string.other_providers_error_message))
270-
}
271-
272-
// Set up the failure response and finish the flow with the constructed error message.
273-
setUpFailureResponseAndFinish(errorMessage)
274-
275-
// Indicate that there was an error during the biometric flow.
276-
return true
277-
}
278-
279232
/**
280233
* This method helps check the asset linking to verify client app idenity
281234
* @param rpId : Relying party identifier

0 commit comments

Comments
 (0)