3

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.

feridok
  • 648
  • 7
  • 26
  • did you try to create a custom `BiometricPrompt.AuthenticationCallback` and create your class instead and pass your parameter into the constructor? or maybe keep the `MyData` param as Activity param and access that param instead? – seyed Jafari Jan 07 '20 at 10:51
  • Which Android version are you testing on? The androidx biometric library does slightly different things on Android Q compared to earlier Android versions. – Michael Jan 07 '20 at 11:05
  • @michael AVD Android P (9.0) – feridok Jan 07 '20 at 11:13
  • 1
    But I don't think the question is even related to Android! it can be any callback Am I missing anything? – seyed Jafari Jan 07 '20 at 11:36
  • I think it is related to android lifecycle somehow – feridok Jan 07 '20 at 11:43
  • @seyedJafari it didn't work: https://gist.github.com/faridfor/afb41424ef36a7197488efed9d2c07af – feridok Jan 07 '20 at 11:49
  • Hmmm... I've just copied your complete code and it worked on a google pixel 2 (Android 10). I've used pin input instead of fingerprint sensor. – Vall0n Jan 07 '20 at 15:47
  • no use API<29 its ok on Android 10 – feridok Jan 07 '20 at 19:53
  • I think this problem is happening because of the `FragmentManager` and the way this library is handling the `Callback` class – seyed Jafari Jan 07 '20 at 20:52
  • Did you try moving your biometricPrompt out of authenticate() and converting it to a field in the activity, so that you can reuse the same instance? – Venator85 Jan 12 '20 at 09:22

3 Answers3

6

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
)
Mir Milad Hosseiny
  • 2,769
  • 15
  • 19
  • Did you have any luck achieving this fix on a Fragment? I've tried fragment.lifecycle.removeObserver(it) and fragment.activity.lifecycle.removeObserver(it), but neither seem to fix the problem. – saltandpepper Mar 27 '20 at 18:03
0

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)
seyed Jafari
  • 1,235
  • 10
  • 20
  • thank for your answer. I want to find out a good solution, cause we can save data in plenty of places in android. – feridok Jan 08 '20 at 07:08
0

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

Isai Damier
  • 976
  • 6
  • 8