AndroidX Biometrics library integration tips after using it on production for 3 months

ยท

3 min read

Table of contents

No heading

No headings in the article.

Disclaimer: This post does not contain background information about biometrics on Android. If you need to quickly walk through the background information, here is the great post about it. And it does not contain information on how to implement Androidx Biometrics library. If you need it, here is the official guide

Prestory

I had a privilege to work on introducing the biometrics authentication feature for the payment solution (Just like GooglePay) which has many many millions of euros of weekly transactions at the time of writing this post. That means we had to make sure our feature is bug free as much as possible.

During the POC (prove of concept) we've tried out latest AndroidX Biometrics Library provided by Google

To turn the biometric API into a one-stop-shop for in-app user authentication

And ended up using AndroidX Biometrics Library as it made biometrics implementation much simpler and easier.

In general it was, easy to use and quick to implement. And UI components' look and feel were consistent across the different devices.

But apparently it can't solve everything as described in the documentation. As all of us know Android OS is widely used by many different phone makers and everyone tries to customize Android to make their brands distinguishable. And it comes with some costs in the end, making developers suffer trying to make their apps smooth and stable in as many devices as possible.

Note: Below solution was suggested by my great ex-colleague Rene de Groot. Thanks to him ๐Ÿ™ . My intention is to document it publicly so that future me can thank me. And if others benefit from it to develop more robust feature that's a big win for me.

Findings

In our payments solution, we let our user to choose if they want to use biometrics capability to authenticate their payment effortlessly without entering their PIN. That being said we have two steps essentially. One, to enroll biometrics. Two, to use enrolled biometrics to authorize the payment.

For the first part we had two functions below to basically check if there is biometrics sensors exists on the device and if there is any biometrics enrolled on the device.

val isBiometricsSupportedByDevice: Boolean
        get() = when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
            BiometricManager.BIOMETRIC_SUCCESS, BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> true
            else -> false
        }
val isBiometricsNotEnrolled: Boolean
        get() = when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> true
            else -> false
        }

Apparently, there are some devices with Android API version below 30 where above functions would not work. It would just simply return BiometricManager.BIOMETRIC_STATUS_UNKNOWN leading to else and ending up returning false even there is a biometrics sensor exist. If we check the source code, you will see below


    /**
     * Unable to determine whether the user can authenticate.
     *
     * <p>This status code may be returned on older Android versions due to partial incompatibility
     * with a newer API. Applications that wish to enable biometric authentication on affected
     * devices may still call {@code BiometricPrompt#authenticate()} after receiving this status
     * code but should be prepared to handle possible errors.
     */
    public static final int BIOMETRIC_STATUS_UNKNOWN = -1;

The best way to cover as many devices as possible is to use older (deprecated) version of BiometricManager implementation below Android API version 30 as shown in below example

private const val VERSION_CODE_R = 30

private val biometricManager by lazy { BiometricManager.from(context) }

private val fingerprintManager by lazy { FingerprintManagerCompat.from(context) }

private fun shouldUseSystemBiometrics() = Build.VERSION.SDK_INT < VERSION_CODE_R

val isBiometricsSupportedByDevice: Boolean
        get() =
            if (shouldUseSystemBiometrics()) {
                fingerprintManager.isHardwareDetected
            } else {
                when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
                    BiometricManager.BIOMETRIC_SUCCESS, BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> true
                    BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
                        analytics.trackEvent(R.string.mp__analytics_development_biometrics_status_unknown)
                        false
                    }
                    else -> false
                }
            }

val isBiometricsNotEnrolled: Boolean
        get() =
            if (shouldUseSystemBiometrics()) {
                fingerprintManager.hasEnrolledFingerprints().not()
            } else {
                when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
                    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> true
                    else -> false
            }
        }

Many Android developers would agree with me that one of the hardest challenges in Android development world is to minimize bugs in as many possible devices as possible from many different manufacturers. And above solution intents to do so.