Biometrics

Biometrics – AndroidX

Android has supported fingerprint sensors since API 23 and previously we looked at the new APIs to handle them using BiometricPrompt and BiometricManager. For the latter we looked at how to create a compat wrapper so that this works back to API 23. But we didn’t implement one for BiometricPrompt. Since writing those articles, I have discovered that an AndroidX implementation of BiometricPrompt exists. In this article we’ll look at this and port the app that we created previously to use the AndroidX version, and be backwardly compatible to API 23 also.

Image: free icons from pngtree.com

BiometricPrompt is a mechanism provided by the Android Framework which greatly simplifies the process of performing biometric authentication. Typically it is used as in-app security forcing the user to authenticate before they are allowed to access more secure data within the app. The older version is FingerprintManager which performs the actual authentication but requires us to implement to UI for scanning ourselves – so BiometricPrompt simplifies this enormously. The AndroidX biometric library is a compat wrapper around both of these APIs, but included the necessary UI components for the legacy FingerprintManager implementation. It is also a slightly simplified version of the Android framework implementation of BiometricPrompt, so it really does make sense to use it.

The project that we used previously can be found here. We need to make only minor changes to get it backwardly compatible to API 23.

The first thing that we need to do is include the AndroidX biometric library which, at the time of writing, is 1.0.0-alpha04:

.
.
.
dependencies {
    implementation 'androidx.biometric:biometric:1.0.0-alpha04'
}
.
.
.

There are only two files which touch BiometricPrompt. The first is AuthenticationResult.kt for which we only need to change the import to use the AndroidX version of BiometricPrompt:

package com.stylingandroid.biometrics

import androidx.biometric.BiometricPrompt

internal sealed class AuthenticationResult {
    internal data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) :
        AuthenticationResult()
    internal data class RecoverableError(val code: Int, val message: CharSequence) :
        AuthenticationResult()
    internal data class UnrecoverableError(val code: Int, val message: CharSequence) :
        AuthenticationResult()
    internal object Failure : AuthenticationResult()
    internal object Cancelled : AuthenticationResult()
}

The majority of the changes are required in Authenticator.kt with the result actually being a smaller file – the previous version was a total of 74 lines, but after the changes it is 58 lines. We need to change the import as we did for AuthenticationResult – I won’t bother showing that here.

The Android framework implementation of BiometricPrompt uses a builder which is used to define the characteristics of the bottom sheet that gets displayed when prompting for a biometric authentication. However, because the AndroidX implementation provides its own UI for the legacy wrapper, this gets abstracted in to a separate class BiometricPrompt.PromptInfo which has its own builder class, and this is simplified because there is much less that we need to do:

    private val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle(fragmentActivity.getString(R.string.biometric_prompt_title))
        .setNegativeButtonText(fragmentActivity.getString(R.string.biometric_prompt_negative_text))
        .build()

The BiometricPrompt.AuthenticationCallback interface is simplified in the AndroidX implementation and we no longer need to implement the onAuthenticationHelp() method, but otherwise it is identical:

private val authCallback = object : BiometricPrompt.AuthenticationCallback() {

        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            super.onAuthenticationError(errorCode, errString)
            Timber.d("Error: $errorCode: $errString")
            callback(AuthenticationResult.UnrecoverableError(errorCode, errString))
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            Timber.d("Failed")
            callback(AuthenticationResult.Failure)
        }

        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            super.onAuthenticationSucceeded(result)
            Timber.d("Success")
            callback(AuthenticationResult.Success(result.cryptoObject))
        }
    }

Next we need to make a couple of small changes to the class itself as the AndroidX implementation of BiometricPrompt has some slightly different requirements:

internal class Authenticator(
    private val fragmentActivity: FragmentActivity,
    private val callback: (AuthenticationResult) -> Unit,
    private val biometricChecker: BiometricChecker = BiometricChecker.getInstance(fragmentActivity)
) {

    private val handler = Handler()
    .
    .
    .
}

We now have everything we need to create the AndroidX BiometricPrompt instance:

    private val biometricPrompt = BiometricPrompt(
        fragmentActivity,
        { runnable -> handler.post(runnable) },
        authCallback
    )

The final thing we need to change is the authenticate() method because the method signature of the AndroidX BiometricPrompt#authenticate method is slightly different:

    fun authenticate() {
        if (!biometricChecker.hasBiometrics) {
            callback(AuthenticationResult.UnrecoverableError(
                0,
                fragmentActivity.getString(R.string.biometric_prompt_no_hardware)
            ))
        } else {
            biometricPrompt.authenticate(promptInfo)
        }
    }

It may feel like we need to do some additional work because the constructor signature of Authenticator has changed but actually we don’t. The previous version required a more generic Context whereas this new version requires a FragmentActivity, but this is actually being called from a FragmentActivity instance which passes this as the first argument, so everything still works quite nicely.

I have made some other small changes to the project, either to update to newer versions of AndroidX libraries, or allow the app itself to work on API 23. For example: with minSdkVersion="28" we could assume Adaptive Icon support, but that no longer applies with minSdkVersion="23". I haven’t bothered detailing those changes here as they’re not relevant to the purpose of this post, but you can see them in the source.

When we run this, the behaviour on a device running Pie or later is identical to what we had before, but we get this on an Oreo emulator:

That shows most of the possible flows and the UI is slightly different from the Pie version. But it by no means a degradation in UX, just a slightly different UI which still works well.

Adapting the Android framework implementation of BiometricPrompt to the AndroidX one requires a work, but the advantages make that effort worthwhile. Adapting an older FingerprintManager implementation to the AndroidX BiometricManager may require a little more effort, the advantages would be much greater, and could potentially reduce your code size quite considerably because you no longer need to implement the UI yourself.

The source code for this article is available here.

© 2019, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Unfortunately, the androidx version of the biometrics has tons of bugs and absolutely unusable after rotation

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.