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.
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.