Biometrics

Biometrics – BiometricManager

Android has supported fingerprint sensors since API 23 and we previously covered the APIs for handling user authentication on Styling Android. However the FingerprintManager class which those tutorials rely upon were deprecated in API 28 (Pie). In this series we’ll look at the new APIs which were introduced in Pie to replace this.

Image: free icons from pngtree.com

Previously we look at the BiometricPrompt API which has been introduced to provide a more generic biometric authentication mechanism that the older FingerprintManager. While we got it all working there was once aspect of the implementation that I wasn’t altogether happy with – how we handle the case where no biometric hardware is available. Although we fail gracefully when this condition is met, I wasn’t altogether comfortable with making a system call to BiometricPrompt#authenticate which would return an error status if the device lack biometric hardware. We really do not know how heavy an operation it is to check this, and it would be nicer if we were able to determine this in advance and only make an authentication request if we know that hardware is available.

With FingerprintManager this was simple enough using its isHardwareDetected() method. However BiometricPrompt does not have an equivalent which is why I did not implement these checks. This is a little frustrating. Fortunately there is a new API introduced in Android Q which enables us to perform a similar check using a new class: BiomentricManager . We obtain an instance of BiometricManager in much the same was as FingerprintManager because it is a system service. Let’s wrap this behaviour inside a class:

@TargetApi(Build.VERSION_CODES.Q)
private class QBiometricChecker(
    private val biometricManager: BiometricManager
) : BiometricChecker() {

    private val availableCodes = listOf(
        BiometricManager.BIOMETRIC_SUCCESS,
        BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
    )

    override val hasBiometrics: Boolean
        get() = availableCodes.contains(biometricManager.canAuthenticate())

    companion object {

            fun getInstance(context: Context): QBiometricChecker? =
                context.getSystemService(BiometricManager::class.java)?.let {
                    QBiometricChecker(it)
                }
    }
}

The getInstance() method creates an instance of this class and obtains the BiometricManager instance. The hasBiometrics getter performs a check using biometricManager.canAuthenticate(). This actually returns a status code indicating the state of the hardware – BIOMETRIC_SUCCESS indicates that the hardware is present and biometrics have bee enrolled (i.e. the user has enrolled one of more fingerprints). In some cases we should not be accepting BIOMETRIC_ERROR_NONE_ENROLLED as a valid state because it indicates that although the biometric hardware is present, it is not available for authentication because no biometrics have been enrolled. I have included it here simply to show how we can implement a check to indicate merely that biometric hardware is present. This will provide compatibility with what actually follows.

This is fine if we’re on a Q device, but simply won’t work on older devices. The app is actually minSdkVersion = 28, so this is clearly a problem. However we can provide a backward compatible approach using a sealed class:

sealed class BiometricChecker {

    abstract val hasBiometrics: Boolean

    @TargetApi(Build.VERSION_CODES.Q)
    private class QBiometricChecker(
        private val biometricManager: BiometricManager
    ) : BiometricChecker() {
        .
        .
        .
    }

    @Suppress("DEPRECATION")
    private class LegacyBiometricChecker(
        private val fingerprintManager: android.hardware.fingerprint.FingerprintManager
    ) : BiometricChecker() {

        override val hasBiometrics: Boolean
            @SuppressLint("MissingPermission")
            get() = fingerprintManager.isHardwareDetected

        companion object {

            fun getInstance(context: Context): LegacyBiometricChecker? =
                    context.getSystemService(
                            android.hardware.fingerprint.FingerprintManager::class.java
                    )?.let {
                        LegacyBiometricChecker(it)
                    }
        }
    }

    private class DefaultBiometricChecker : BiometricChecker() {

        override val hasBiometrics: Boolean = false
    }

    companion object {

        @SuppressLint("ObsoleteSdkInt")
        fun getInstance(context: Context): BiometricChecker {
            return when {
                // We should change this when Android Q is fully released
                // as this BuildCompat lookup will be deprecated
                BuildCompat.isAtLeastQ() ->
                    QBiometricChecker.getInstance(context)
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
                    LegacyBiometricChecker.getInstance(context)
                else -> null
            } ?: DefaultBiometricChecker()
        }
    }
}

As well as QBiometricChecker that we just looked at, we now have LegacyBiometricChecker which uses FingerprintManager to determine hardware availability, and there is also DefaultBiometricChecker which is for SDK levels which don’t even support FingerprintManager. Although we really don’t require this for this particular project, I have included it to demonstrate how we could achieve a compat wrapper that works for all SDK levels.

The more observant may have spotted that although BiometricManager was not introduced until Android Q, FingerprintManager was actually deprecated in Pie (API 28) so on a Pie device we will actually be using a deprecated class. Although deprecation means that this should not be used, there really is no alternative here so we don’t really have a lot of choice. That said, in Pie only fingerprint hardware is widely available, so it seems reasonably safe to use it. Of course the alternative approach would be to simply use the error state of a call to BiometricManager.authenticate() to determine lack of hardware.

BiometricChecker#getInstance() is where all of the logic for determining the correct implementation for BiometricChecker gets returned. Also interesting to note is that all three of the concrete instances of BiometricChecker have private visibility, so any clients of BiometricChecker only have a contract with the sealed class. This is a nice way of providing different behaviours in a discrete way so that any consumers remain completely agnostic of the implementation.

If we wanted to make the minSdkVersion of this app lower, then we could use a similar approach to wrap BiometricPrompt and FingerprintManager inside a similar abstraction.

Adding this check in to our Authenticator code is pretty straightforward:

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.

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.