How to set a fallback method if fingerprint does not work - android

I recently moved my project to AndroidX and while implementing fingerprint for the app I am using the Biometric for AndroidX.
implementation 'androidx.biometric:biometric:1.0.0-alpha03'
When a dialog is displayed to use fingerprint for authentication, the dialog has "Cancel" option set as the negative button.
final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Log into App")
.setSubtitle("Please touch the fingerprint sensor to log you in")
.setDescription("Touch Sensor")
.setNegativeButtonText("Cancel".toUpperCase())
.build();
As per the android documentation:
https://developer.android.com/reference/androidx/biometric/BiometricPrompt.PromptInfo.Builder.html#setNegativeButtonText(java.lang.CharSequence)
Required: Set the text for the negative button.
This would typically be used as a "Cancel" button, but may be also used
to show an alternative method for authentication,
such as screen that asks for a backup password.
So instead of "Cancel" button I can say "Use Password" to provide an alternative method incase fingerprint fails, and when user clicks on it I can show another popup dialog where I can let user enter the device password to help retrieve the app password from the Keystore. Is this correct ?
But, what happens if I do not have password set to unlock my phone instead I use a pattern ?
I see that if I use android.hardware.biometrics.BiometricPrompt.Builder instead of androidx.biometric.BiometricPrompt.PromptInfo.Builder, it has a method https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder.html#setDeviceCredentialAllowed(boolean)
for the same purpose, to let user authenticate using other means if fingerprint fails.
Can someone help me understand this ? How I could achieve this with AndroidX as my app is compatible from API 16 onwards. And why does AndroidX does not come back with this fallback method ?

The setDeviceCredentialAllowed API was recently added in beta01
See the release notes here
https://developer.android.com/jetpack/androidx/releases/biometric

On SDK version Q and above using BiometricPrompt with authentication callback otherwise using createConfirmDeviceCredentialsIntent.
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val biometricPrompt = BiometricPrompt.Builder(this)
.setTitle(getString(R.string.screen_lock_title))
.setDescription(getString(R.string.screen_lock_desc))
.setDeviceCredentialAllowed(true)
.build()
val cancellationSignal = CancellationSignal()
cancellationSignal.setOnCancelListener {
println("#Biometric cancellationSignal.setOnCancelListener")
//handle cancellation
}
val executors = mainExecutor
val authCallBack = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
print("SecuritySetupActivity.onAuthenticationError ")
println("#Biometric errorCode = [${errorCode}], errString = [${errString}]")
//handle authentication error
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)
print("SecuritySetupActivity.onAuthenticationSucceeded ")
println("#Biometric result = [${result}]")
//handle authentication success
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
super.onAuthenticationHelp(helpCode, helpString)
print("SecuritySetupActivity.onAuthenticationHelp ")
println("#Biometric helpCode = [${helpCode}], helpString = [${helpString}]")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
print("SecuritySetupActivity.onAuthenticationFailed ")
//handle authentication failed
}
}
biometricPrompt.authenticate(cancellationSignal, executors, authCallBack)
} else {
val i = km.createConfirmDeviceCredentialIntent(getString(R.string.screen_lock_title), getString(R.string.screen_lock_desc))
startActivityForResult(i, 100)
}

Try setDeviceCredentialAllowed(true) on BiometricPromopt.

androidx 1.0.0 allows you to setup a fallback with ease - like this:
// Allows user to authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
// Can't call setNegativeButtonText() and
// setAllowedAuthenticators(... or DEVICE_CREDENTIAL) at the same time.
// .setNegativeButtonText("Use account password")
.setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
.build()
see this

Related

BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS but KeyPairGenerator.initialize() => java.lang.IllegalStateException on API 29

Sorry for such a long question. I've tried to include all relevant information and it is quite a lot. I've been working on this issue for a few weeks now and am in desperate need of help.
General info
I am developing a flutter app that requires authentication with a CryptoObject for certain functionality. This means for Android that setUserAuthenticationRequired(true) needs to be set on the KeyGenParameterSpec that's used to create the key. On Android API >=30 this works fine and I can authenticate myself with either fingerprint or Device Credentials (PIN, pattern, password).
The problem
The problem is that I can't get Biometrics with setUserAuthenticationRequired(true) to work on emulators with API 29, even if they have fingerprints set up. I have not been able to test on emulators with an even lower API, so I don't know if that would work or not.
Calling BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS as below returns true. The else case is run since Build.VERSION_CODES.R = API 30. According to the documentation of BiometricPrompt.authenticate(), only BIOMETRIC_STRONG is allowed for devices with API <30.
fun canAuthenticate(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
BiometricManager.from(context)
.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
} else {
BiometricManager.from(context)
.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS // <----- this returns true!
}
}
However, even though a fingerprint is registered in the emulator and BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS, calling keyPairGenerator.initialize() throws java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use.
This is the code (restricted is true so setUserAuthenticationRequired(true) gets set):
private fun initializeKeyPairGenerator(withStrongBox: Boolean = true): KeyPairGenerator {
val keyPairGenerator = KeyPairGenerator.getInstance(keyGenAlgorithm, provider)
try {
val parameterSpec = createParameterSpec(withStrongBox)
keyPairGenerator.initialize(parameterSpec) // <-------- It throws the exception here
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && e is StrongBoxUnavailableException) {
val parameterSpec = createParameterSpec(false)
keyPairGenerator.initialize(parameterSpec)
} else {
throw Exception("Cannot create key", e)
}
}
return keyPairGenerator
}
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
return KeyGenParameterSpec.Builder(alias, purposes)
.run {
setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
setBlockModes(encryptionBlockMode)
setEncryptionPaddings(encryptionPadding)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setIsStrongBoxBacked(withStrongBox)
}
if (restricted) {
setUserAuthenticationRequired(true)
}
build()
}
}
The issue seems very related to this issue https://issuetracker.google.com/issues/147374428.
Some things I've tried and an ugly way to make it work with two biometric prompts
Setting setUserAuthenticationValidityDurationSeconds(10) on the KeyGenParameterSpec makes keyPairGenerator.initialize() not throw an exception.
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
return KeyGenParameterSpec.Builder(alias, purposes)
.run {
setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
setBlockModes(encryptionBlockMode)
setEncryptionPaddings(encryptionPadding)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setIsStrongBoxBacked(withStrongBox)
}
if (restricted) {
setUserAuthenticationRequired(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setUserAuthenticationParameters(
0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
)
}
else { // API <= Q
// parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
// parameter "-1" default to AUTH_BIOMETRIC_STRONG
// source: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
setUserAuthenticationValidityDurationSeconds(10) // <-- Allow device credential authentication
}
}
build()
}
}
However, it instead throws the following exception when calling initSign(privateKey): ((PlatformException(SIGNING_FAILED, User not authenticated, android.security.keystore.UserNotAuthenticatedException: User not authenticated, null)).
Here is the code:
val signature: Signature
get() = Signature.getInstance(signAlgorithm)
.apply {
val privateKey = asymmetricKeyPair.privateKey
initSign(privateKey) <--- Throws an exception
}
This behavior matches with the documentation of setUserAuthenticationValidityDurationSeconds():
Cryptographic operations involving keys which are authorized to be used for a duration of time after a successful user authentication event can only use secure lock screen authentication. These cryptographic operations will throw UserNotAuthenticatedException during initialization if the user needs to be authenticated to proceed.
The documentation continues with:
This situation can be resolved by the user unlocking the secure lock screen of the Android or by going through the confirm credential flow initiated by KeyguardManager.createConfirmDeviceCredentialIntent(CharSequence, CharSequence). Once resolved, initializing a new cryptographic operation using this key (or any other key which is authorized to be used for a fixed duration of time after user authentication) should succeed provided the user authentication flow completed successfully.
Following these instructions to show a biometric prompt and listening to the result before doing initSign(privateKey) makes initSign(privateKey) not throw an exception, if the user authenticates themself in the prompt by fingerprint.
The code:
private fun triggerBiometricPrompt() {
val bio = BiometricAuthenticator()
val intent = bio.createConfirmDeviceCredentialIntent(activity)
activity.startActivityForResult(intent, 0)
}
In the class FlutterFragmentActivity()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (IdNowMethodCallHandler.handler.onActivityResult(requestCode, resultCode, data)) {
return
}
if (resultCode == Activity.RESULT_OK) {
handler.signWithRestrictedKey(handler.methodCall, handler.methodResult) // <-- The result gets handled here
}
super.onActivityResult(requestCode, resultCode, data)
}
However, this means the user needs to authenticate themself twice, as a second prompt of course is shown when calling BiometricPrompt.authenticate().
The code:
private fun authenticate(
activity: FragmentActivity,
promptInfo: BiometricPrompt.PromptInfo = createPromptInfo(),
signature: Signature?,
onError: (Int, CharSequence) -> Unit,
onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,
) {
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onError(errorCode, errString)
override fun onAuthenticationFailed() {
// Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as belonging to the user.
// We want to omit it because the fingerprint maybe just failed to be read in which case the user retries.
// Also, after multiple attempts, the user can use credentials instead.
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess(result)
}
val executor = ContextCompat.getMainExecutor(activity)
val prompt = BiometricPrompt(activity, executor, callback)
if (signature == null) {
prompt.authenticate(promptInfo) // TODO: We never do this since signature is never null.
} else {
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature)) // <-- Another prompt is shown here to authenticate
}
}
fun createPromptInfo(
title: String = "Authorize",
subtitle: String = "Please, authorise yourself",
description: String = "This is needed to perform cryptographic operations.",
): BiometricPrompt.PromptInfo {
val builder = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setDescription(description)
.setConfirmationRequired(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder.apply {
setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
}
} else {
builder.setNegativeButtonText("Cancel")
}
return builder.build()
}
Needing the user to authenticate twice in a row with biometrics is of course a very poor user experience. It won't even work if the user authenticates with device credentials in the first prompt, and I've found no way to hide that option.
Questions
Why does KeyPairGenerator.initialize() throw the exception java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use on emulators with API 29 with fingerprint set up, even though BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS? Is this simply a bug in the Android system?
Is there a way to make keys with setUserAuthenticationRequired(true) (crypto-based authentication) work on API 29 (or APIs <30)?
I am deeply grateful for any help I can get.
Finally found a solution thanks to https://www.iedigital.com/resources/archive/create-rsa-key-pair-on-android/.
Basically, for API <30, the trick is to use keyGuardManager.createConfirmDeviceCredentialIntent() instead of using BiometricPrompt.authenticate(). The article explains it best but here are the basic steps with some code:
Do setUserAuthenticationValidityDurationSeconds(0) when creating the key.
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
val purposes = KeyProperties.PURPOSE_SIGN
return KeyGenParameterSpec.Builder(alias, purposes)
.run {
setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
setBlockModes(encryptionBlockMode)
setEncryptionPaddings(encryptionPadding)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setIsStrongBoxBacked(withStrongBox)
}
if (restricted) {
setUserAuthenticationRequired(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setUserAuthenticationParameters(
0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
)
}
else { // API <= Q
// parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
// parameter "-1" default to AUTH_BIOMETRIC_STRONG
// source: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
setUserAuthenticationValidityDurationSeconds(0)
}
}
build()
}
}
Use the deprecated keyGuardManager.createConfirmDeviceCredentialIntent() to display a prompt and let the user authenticate themselves.
fun createConfirmDeviceCredentialIntent(context: Context): Intent {
val keyGuardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
return keyGuardManager.createConfirmDeviceCredentialIntent(
"Authorize", // TODO: Add and use Phrase string https://jimplan.atlassian.net/browse/FS-946
"Please, authorise yourself", // TODO: Add and use Phrase string https://jimplan.atlassian.net/browse/FS-946
)
}
Sign the data
fun sign(signature: Signature, data: ByteArray): ByteArray {
val signedData = signature.run {
update(data)
sign()
}
return signedData
}
Annoying that it doesn't work for all APIs to use BiometricPrompt.authenticate(). I wish this was clearer in the documentation!

Android add hook into firebase email auth to replace "sign-in email sent" screen

Am using Firebase Auth in Kotlin to trigger email link sign-in. It's all working fine, but I would like to change the "Sign-in email sent" screen with a custom one but cant find an event/callback that is triggered when this UI is displayed so I can detect this and replace it.
Excerpt of relevant code
private val signInActivity = registerForActivityResult(FirebaseAuthUIActivityResultContract()) { result ->
}
private fun emailSignIn() {
signInActivity.launch(FirebaseSignInHelper.createSignInForEmailIntent().build())
}
class FirebaseSignInHelper {
companion object {
fun createSignInForEmailIntent() = createSignInIntent(emailProvider())
private fun createSignInIntent(providers: List<AuthUI.IdpConfig>) =
AuthUI.getInstance()
.createSignInIntentBuilder()
.setIsSmartLockEnabled(!BuildConfig.DEBUG, true)
.setAvailableProviders(providers)
.setLogo(R.drawable.black_crown_with_name)
.enableAnonymousUsersAutoUpgrade()
.setTheme(R.style.GreenTheme)
private fun actionCodeSettings() =
ActionCodeSettings.newBuilder()
.setAndroidPackageName("<domain>", false, null)
.setHandleCodeInApp(true) // This must be set to true
.setUrl("<url>") // This URL needs to be whitelisted
.build();
fun emailProvider() =
arrayListOf(
AuthUI.IdpConfig.EmailBuilder()
.enableEmailLinkSignIn()
.setActionCodeSettings(actionCodeSettings())
.build()
)
}
}
I have tried Subclassing FirebaseAuthUIActivityResultContract and implementing available overrides and as it all runs in its own activity I can't find a way to hook into activity life cycle or callback events, unless someone knows how to do this? Nothing seems to work, the app is just left on this screen with nothing in code to tell me this.

Firebase Phone Authentication OTP not Auto Fill in EditText

Firebase Phone Authentication sent otp automatically not fill in edittext. otp is manually entered in edittext. how do automatically detect the incoming verification SMS and perform verification with user action?
Otp.kt
private fun sendVerificationCode(phone : String){
val options = PhoneAuthOptions.newBuilder(auth)
.setPhoneNumber(phone) // Phone number to verify
.setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
.setActivity(this) // Activity (for callback binding)
.setCallbacks(callbacks) // OnVerificationStateChangedCallbacks
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
}
private val callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks(){
override fun onVerificationCompleted(p0: PhoneAuthCredential) {
binding.btnOtpVerify.visible(true)
binding.progressbarVerify.visible(false)
val code = p0.smsCode
if(code != null){
binding.etOtpView.setText(code)
VerifyVerificationCode(code)
signInWithPhoneAuthCredential(p0)
}
}
override fun onVerificationFailed(p0: FirebaseException) {
binding.btnOtpVerify.visible(true)
binding.progressbarVerify.visible(false)
Toast.makeText(this#OtpVerification,p0.message,Toast.LENGTH_LONG).show()
}
override fun onCodeSent(p0: String, p1: PhoneAuthProvider.ForceResendingToken) {
super.onCodeSent(p0, p1)
binding.btnOtpVerify.visible(true)
binding.progressbarVerify.visible(false)
storedVerificationID = p0
resendingToken = p1
}
}
Make sure you have enabled SafetyNet thing this on google.
For the autofill editText work with the code inside the function onVerificationCompleted
As I understand it, you are using Firebase PhoneAuth to verify the phone number and send an OTP via the Firebase server, and you need a workflow to detect the incoming SMS containing the OTP and auto-fill your EditText with this OTP.
To enable automatic OTP detection, you can use the SmsRetriever API and the related SMS User Consent API.
Well-explained documentation can be found here. (Contains further links to a reference implementation that pretty much works out of the box, so I'm not including that code here.)
Be sure to choose the appropriate API for your use case.

Android user messaging platform isConsentFormAvailable returns false

I followed this guide https://developers.google.com/admob/ump/android/quick-start and my code looks like this:
private fun checkForConsent() {
/*
val debugSettings = ConsentDebugSettings.Builder(this)
.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA)
.addTestDeviceHashedId("69887E2CBBE5346EC3B54A3FD207AB41")
.build()
val params = ConsentRequestParameters.Builder()
.setConsentDebugSettings(debugSettings)
.build()
*/
Log.d("adstuff", "check Consent")
val params = ConsentRequestParameters.Builder().build()
// Set tag for under age of consent. Here false means users are not under age
//Log.d("adstuff", params.isTagForUnderAgeOfConsent.toString())
consentInformation = UserMessagingPlatform.getConsentInformation(this)
//consentInformation.reset();
consentInformation.requestConsentInfoUpdate(
this,
params,
object : ConsentInformation.OnConsentInfoUpdateSuccessListener {
override fun onConsentInfoUpdateSuccess() {
// The consent information state was updated.
// You are now ready to check if a form is available.
Log.d("adstuff", "Is consentform available")
Log.d("adstuff", consentInformation.isConsentFormAvailable.toString())
if (consentInformation.isConsentFormAvailable) {
Log.d("adstuff", "Consentform is available")
loadForm()
}
}
},
object : ConsentInformation.OnConsentInfoUpdateFailureListener {
override fun onConsentInfoUpdateFailure(formError: FormError) {
// Handle the error.
Log.d("adstuff", "content info update failure")
Log.d("adstuff", formError.toString())
}
})
}
However, consentInformation.isConsentFormAvailable returns false. I made an AdMob account and a Funding Choices account where I created and published a message of type Consent TCF. I have also added the ca-app-pub ID to the Android Manifest.
Thanks
Edit: If I set the location to the EEA using the debug settings, like in the commented code, the consent info update fails.
In your phone go to Settings > Privacy > Advanced > Ads and toggle off "Opt out of Ads Personalization"

Some Questions about BiometricPrompt on Android

I'm trying to figure out how to modify (if it's possible) the normal behavior of biometricPrompt, in particular i want to display Gandalf, when the authentication fails.
I'm currently displaying it with a custom alertDialog, but it remains in background, with the biometricPrompt fragment on foreground exactly like this, and it loses all of its dumbness...
The best solution would probably be to display both, alertDialog and biometricPrompt, on foreground, displaying the image only in the upper half of the screen, but at the moment I've no idea of how to do it, or better, I have no idea how to link layouts together to manage size / margins and everything else.
The other thing I was thinking is to remove the biometricPrompt, so the alert dialog will be put on foreground, but any solution I've tried has failed miserably.
Any type of help/ideas will be welcome.
Anyway, here's the code:
class BiometricPromptManager(private val activity: FragmentActivity) {
private val cryptoManager = CryptoManager(activity)
fun authenticateAndDecrypt(failedAction: () -> Unit, successAction: (String) -> Unit) {
// display biometric prompt, if the user is authenticated, the decryption will start
// if biometric related decryption gives positives results, the successAction will start services data decryption
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
cryptoManager.startDecrypt(failedAction,successAction)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
activity.runOnUiThread { failedAction() }
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
activity.runOnUiThread { failedAction() }
}
})
val promptInfo = biometricPromptInfo()
biometricPrompt.authenticate(promptInfo)
}
private fun biometricPromptInfo(): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle("Fingerprint Authenticator")
.setNegativeButtonText(activity.getString(android.R.string.cancel))
.build()
}
}
Open biometric auth from activity :
private fun openBiometricAuth(){
if(sharedPreferences.getBoolean("fingerPrintEnabled",false)) {
if (BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { // check for hardware/permission
biometric.visibility = View.VISIBLE
BiometricPromptManager(this).authenticateAndDecrypt(::failure, ::callDecryption)
}
}
}
What to do when the user is not recognized :
private fun failure(){
val view = layoutInflater.inflate(R.layout.gandalf, null)
val builder = AlertDialog.Builder(this)
builder.setView(view)
builder.setPositiveButton("Dismiss") { dialog: DialogInterface, id: Int -> dialog.cancel() }
val alertDialog = builder.create()
alertDialog.show()
}
The Biometric API itself handles failed attempts at authentication the following way:
For each failed attempt the onAuthenticationFailed() callback is invoked.
The user gets 5 tries, after the 5th attempt fails the onAuthenticationError() callback receives the error code ERROR_LOCKOUT and the user must wait for 30 seconds to try again.
Showing your dialog inside onAuthenticationFailed() may be too eager and may result in poor user experience. And so a good place might be after you get the ERROR_LOCKOUT. The way the AndroidX Biometric Library works is that it dismisses the BiometricPrompt when the error is sent. Therefore you should have little problem showing your own dialog at that point.
In any case -- i.e. beyond these opinions -- the more general approach is to call cancelAuthentication() to dismiss the prompt and then proceed to showing your own dialog that way.
Also please follow blogpost1 and blogpost2 for recommended design pattern for BiometricPrompt.

Categories

Resources