Following is my code to detect biometric functionality
class MainActivity : AppCompatActivity() {
private var executor: Executor? = null
private var biometricPrompt: BiometricPrompt? = null
private var promptInfo: BiometricPrompt.PromptInfo? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val bioMetricManager = BiometricManager.from(this)
when (bioMetricManager.canAuthenticate()) {
BiometricManager.BIOMETRIC_SUCCESS -> {
executor = ContextCompat.getMainExecutor(this)
promptInfo = BiometricPrompt.PromptInfo.Builder().setTitle("Biometric login for leaao")
.setSubtitle("Log in using biometric credentials").setDeviceCredentialAllowed(true).build()
biometricPrompt = BiometricPrompt(this,
executor!!, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.i("here4","Authentication error: $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.i("here5","Success")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.i("here6","Authentication failed: ")
}
})
biometricPrompt?.authenticate(promptInfo!!)
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Log.i("here","No biometric features available on this device.")
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Log.i("here2","Biometric features are currently unavailable.")
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
Log.i("here3","The user hasn't associated any biometric credentials with their account.")
}
}
}
}
I have added <uses-permission android:name="android.permission.USE_BIOMETRIC" /> to Manifest file and added implementation 'androidx.biometric:biometric:1.0.1' to gradle
Still my code fails to detect biometric prompt. I have enabled face recognition on my mobile but it does not work when i open my app. It goes to BIOMETRIC_ERROR_HW_UNAVAILABLE case in my code.
By default, the BiometricManager seeks BIOMETRIC_STRONG sensors on the device, unless you specify them in .setAllowedAutnenticators(). However, you should know that as of now, there are no devices with STRONG face/iris sensors. That means that only the fingerprint sensor is "seen" as STRONG and all of the face and iris sensors (with some exceptions, where they are not seen as a sensor at all) are "seen" as BIOMETRIC_WEAK. With "seen" I mean they were set as such by the manufacturer. So having said that, something like this should be able to fix your issue:
BiometricPrompt.PromptInfo.Builder()
.setTitle(...)
.setDescription(...)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.BIOMETRIC_WEAK
)
and when checking for available authentication:
when (BiometricManager.from(requireContext()).canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.BIOMETRIC_WEAK
)) { ... }
Related
I have this function :
fun createBiometricPrompt(
activity: AppCompatActivity,
processSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,
callback: BiometricCallback
): BiometricPrompt {
var nbFailure = 0
val executor = ContextCompat.getMainExecutor(activity)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errCode: Int, errString: CharSequence) {
super.onAuthenticationError(errCode, errString)
Log.d(TAG, "errCode is $errCode and errString is: $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d(TAG, "User biometric rejected.")
nbFailure++;
if(nbFailure == MAX_BIOMETRICS_FAILURE){
nbFailure = 0
callback.onFailure()
}
}
which is using a callback callback: BiometricCallback, it's only for now using it for notifying that a failure happened by sending callback.onFailure()
Callback is called in the calling Fragment by doing this:
Fragment.kt
biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(requireActivity() as AppCompatActivity, ::encryptAndStoreServerToken, object: BiometricCallback {
override fun onFailure() {
biometricPrompt.cancelAuthentication()
viewModel.loginError.set("Error Biometrics")
}
})
The onFailure is used then to cancel the process and display an error.
The Callback interface is defined as :
interface BiometricCallback {
fun onFailure()
}
I have been asked to use a suspend function instead of a callback but I have no clue how to do it properly. Should I use LiveData ? If any idea, please help
Thanks
you can use suspendCancellableCoroutine or suspendCoroutine to convert any callback based Api in direct style to make it more kotlin friendly plus it provides more encapsulation you will be just returned with the result all complexity is handled inside the function only
suspend fun authenticate():BiomatricPrompt.AuthenticationResult? {
return suspendCancelableCoroutine { continuation ->
biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int,
errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
continuation.resume(null,null)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
continuation.resume(result,null)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
continuation.resume(null,null)
}
})
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
.setNegativeButtonText("Use account password")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
usage
val authenticationResult = authenticate()
if(authenticationResult == null){
//authentication failed
}else{
//authenticated successfully
}
you can use custom object as result type to handle more use cases
more information
I have tried to setup fingerprint, pattern/password/pin, faceUnlock for my app. But Biometric doesn't works it always showing fingerprint with use pattern dialog. Is Still Biometrics not support FaceUnlock? If Not supported Biometric dependency means which library I should use in my app which contains fingerprint, pattern/password/pin and faceUnlock.
class LoginActivity : AppCompatActivity(){
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var executor: Executor
private lateinit var callBack: BiometricPrompt.AuthenticationCallback
lateinit var prompt: BiometricPrompt.PromptInfo
private var keyguardManager: KeyguardManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityLoginBinding>(this, R.layout.activity_login)
executor = ContextCompat.getMainExecutor(this)
biometricPrompt = BiometricPrompt(this, executor, object: BiometricPrompt.AuthenticationCallback(){
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
tvAuthStatus.text = "Authentication Failed"
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
startActivity(Intent(this#LoginActivity, MainActivity::class.java))
finish()
tvAuthStatus.text = "Authentication Success"
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
tvAuthStatus.text = "Error" + errString
}
})
prompt = BiometricPrompt.PromptInfo.Builder()
.setTitle("RoomDB FingerPrint Login")
.setNegativeButtonText("Login using fingerprint or face")
.setNegativeButtonText("Cancel")
.build()
btnAuth.setOnClickListener {
biometricPrompt.authenticate(prompt)
}
}
}
Maybe, in your case, the reason is your test device is released with the face authentication feature before the same API for face authentication appears in the official Android SDK. It makes third-party applications in your test device cannot use face authentication. Check this library, it said about this thing in README.md "I have a device that can be unlocked using Fingerprint/Face/Iris and(or) I can use this biometric type in pre-installed apps. But it doesn't work on 3rd party apps. Can you help?"
https://github.com/sergeykomlach/AdvancedBiometricPromptCompat#i-have-a-device-that-can-be-unlocked-using-fingerprintfaceiris-andor-i-can-use-this-biometric-type-in-pre-installed-apps-but-it-doesnt-work-on-3rd-party-apps-can--you-help
Hope it can help.
I am working on authenticating my my via BiometricPrompt
The UI shown by system varies
on Motorola (stock Android) I get BottomSheet
on OnePlus I get Full screen activity
How can I know what type of UI would be shown Or how can I control the ui
My problem
Facing a big issue in Activity transition
In BottomSheet UI I can see the my app screen UI elements which I don't want
I can't guess how GooglePay(Tez) is doing
MyCode
val executor = ContextCompat.getMainExecutor(activity)
val biometricPrompt = BiometricPrompt(activity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
if (errorCode == BiometricConstants.ERROR_USER_CANCELED) {
listener.onAuthCancelled()
} else {
listener.onAuthFailed()
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
listener.onAuthSucceeded()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
listener.onAuthFailed()
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Title")
.setSubtitle("Sub title ")
.setConfirmationRequired(false)
.setDeviceCredentialAllowed(true)
.build()
biometricPrompt.authenticate(promptInfo)
I am using androidx.biometric:biometric:1.0.1 everything works fine but when I have a device without a biometric sensor (or when the user didn't set his fingerprint or etc) and I try to use DeviceCredentials after doing authentication my function input data is not valid.
class MainActivity : AppCompatActivity() {
private val TAG = MainActivity::class.java.name
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.first).setOnClickListener {
authenticate(MyData(1, "first"))
}
findViewById<View>(R.id.second).setOnClickListener {
authenticate(MyData(2, "second"))
}
}
private fun authenticate(data: MyData) {
Log.e(TAG, "starting auth with $data")
val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.e(TAG, "auth done : $data")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(true)
.setTitle("title")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
data class MyData(
val id: Int,
val text: String
)
First I click on my first button, authenticate, then I click my second button and authenticate, then android logcat is like this:
E/com.test.biometrictest.MainActivity: starting auth with MyData(id=1, text=first)
E/com.test.biometrictest.MainActivity: auth done : MyData(id=1, text=first)
E/com.test.biometrictest.MainActivity: starting auth with MyData(id=2, text=second)
E/com.test.biometrictest.MainActivity: auth done : MyData(id=1, text=first)
as you see in last line MyData id and text is invalid! autneticate function input(data) is not the same when onAuthenticationSucceeded is called!
(if you try to test it be sure to use DeviceCredentials not biometrics, I mean pattern or password, unset your fingerprint)
Why data is not valid in callBack?
it works ok on android 10 or with fingerprint
I don`t want to use onSaveInstanceState.
When you create a new instance of BiometricPrompt class, it adds a LifecycleObserver to the activity and as I figured out it never removes it. So when you have multiple instances of BiometricPrompt in an activity, there are multiple LifecycleObserver at the same time that cause this issue.
For devices prior to Android Q, there is a transparent activity named DeviceCredentialHandlerActivity and a bridge class named DeviceCredentialHandlerBridge which support device credential authentication. BiometricPrompt manages the bridge in different states and finally calls the callback methods in the onResume state (when back to the activity after leaving credential window) if needed. When there are multiple LifecycleObserver, The first one will handle the result and reset the bridge, so there is nothing to do by other observers. This the reason that the first callback implementation calls twice in your code.
Solution:
You should remove LifecycleObserver from activity when you create a new instance of BiometricPrompt class. Since there is no direct access to the observer, you need use reflection here. I modified your code based on this solution as below:
class MainActivity : AppCompatActivity() {
private val TAG = MainActivity::class.java.name
private var lastLifecycleObserver: LifecycleObserver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.first).setOnClickListener {
authenticate(MyData(1, "first"))
}
findViewById<View>(R.id.second).setOnClickListener {
authenticate(MyData(2, "second"))
}
}
private fun authenticate(data: MyData) {
Log.e(TAG, "starting auth with $data")
lastLifecycleObserver?.let {
lifecycle.removeObserver(it)
lastLifecycleObserver = null
}
val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.e(TAG, "auth done : $data")
}
})
var field = BiometricPrompt::class.java.getDeclaredField("mLifecycleObserver")
field.isAccessible = true
lastLifecycleObserver = field.get(biometricPrompt) as LifecycleObserver
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(true)
.setTitle("title")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
data class MyData(
val id: Int,
val text: String
)
So it seems strange but I managed to get it working by introducing a parameter to MainActivity
here is the working code:
class MainActivity : AppCompatActivity() {
var dataParam : MyData? = null
companion object {
private val TAG = MainActivity::class.java.name
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.firstBtn).setOnClickListener {
authenticate(MyData(1, "first"))
}
findViewById<View>(R.id.secondBtn).setOnClickListener {
authenticate(MyData(2, "second"))
}
}
private fun authenticate(data: MyData) {
Log.e(TAG, "starting auth with $data")
dataParam = data
val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.e(TAG, "auth done : $dataParam")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(true)
.setTitle("title")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
data class MyData(
val id: Int,
val text: String
)
The output is now:
E/com.worldsnas.bioissuesample.MainActivity: starting auth with MyData(id=1, text=first)
E/com.worldsnas.bioissuesample.MainActivity: auth done : MyData(id=1, text=first)
E/com.worldsnas.bioissuesample.MainActivity: starting auth with MyData(id=2, text=second)
E/com.worldsnas.bioissuesample.MainActivity: auth done : MyData(id=2, text=second)
Since you are asking about setDeviceCredentialAllowed(true), it's safe to assume you aren't following the recommended implementation that uses CryptoObject. (Also check out this blog post.)
The setDeviceCredentialAllowed(true) functionality will only work on API 21+, but you have multiple options for handling it in your app depending on your minSdkVersion.
API 23+
if your app is targeting API 23+, then you can do
if (keyguardManager.isDeviceSecure()){
biometricPrompt.authenticate(promptInfo)
}
API 16 to pre-API 23
If your app must make the check pre API 23, you can use
if (keyguardManager.isKeyguardSecure) {
biometricPrompt.authenticate(promptInfo)
}
KeyguardManager.isKeyguardSecure() is equivalent to isDeviceSecure() unless the device is SIM-locked.
API 14 to API 16
If you are targeting lower than API 16 or SIM-lock is an issue, then you should simply rely on the error codes in the callback onAuthenticationError().
P.S.
You should replace private val TAG = MainActivity::class.java.name with private val TAG = "MainActivity".
It looks like BiometricPrompt 1.0.0 has a bug where it stays in invisible state throwing that exception
ill suggest a workaround in the answer
EDIT(thanks to #Isai Damier):
Way to reproduce:
open the BiometricPrompt
press back button - close the prompt
press back again and exit the app
return to the app - try to open the prompt again
this fix was introduces when biometric prompt version was 1.0.0. this issue dosent reproduces in 1.0.1
//-------------------------------------------------------------------------
i came up with a work around - call 'cancelAuthentication' on the prompt upon user cancelation.
full code is as follows (used in a react native app - thats why the promise are for):
private const val E_BIOMETRIC_ERR = "E_FINGER_PRINT_ERR"
private const val OPENED = "success"
private var bp: BiometricPrompt? = null
private var mAuthenticationPromise: Promise? = null
fun authenticate(activity: AppCompatActivity, title: String,
subTitle: String, description: String, authenticationPromise: Promise) {
mAuthenticationPromise = authenticationPromise
val executor = Executors.newSingleThreadExecutor()
bp?.cancelAuthentication()
bp = getBiometricPrompt(activity, executor)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subTitle)
.setDescription(description)
.setDeviceCredentialAllowed(true)
.build()
bp!!.authenticate(promptInfo)
}
private fun getBiometricPrompt(activity: AppCompatActivity, executor: Executor): BiometricPrompt {
return BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
mAuthenticationPromise?.resolve(OPENED)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
bp!!.cancelAuthentication()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
bp!!.cancelAuthentication()
}
})
}
I hope it helps anyone - cheers