androidkotlinandroid-intentandroid-pendingintentandroid-biometric-prompt

Device credential (PIN) using BiometricPrompt destroys calling activity


we are currently facing a weird BiometricPrompt behaviour on some Android devices. The problem is brand/manufacturer independent because it works e.g. on some Samsung devices but on others doesn't.

The Issue

To approve some transaction made by the user to her account we require her to authenticate herself using her device credentials or biometry. Under the hood we are using BiometricPrompt. The problem we are facing is that immediately after calling the BiometricPrompt.authenticate() method the device credential screen is shown (specifically only PIN), our underlying activity gets destroyed (there are just some milliseconds in between), we enter the Pin-Code, click continue, the device credential screens disappears and we see the home screen of the device itself. The problem only occurs while the users has set up a PIN as device credential. Pattern and biometry are working fine. Also, PIN is working on some other Android devices.

Code

Here is the code we are calling:

suspend fun authenticate(
    activity: FragmentActivity
): AuthenticationResult = suspendCoroutine { continuation ->
    @Suppress("DEPRECATION") // We are aware of this. If possible we use the "Modern" approach.
    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle(activity.getString(R.string.biometric_prompt_title))
        .setDeviceCredentialAllowed(true)
        .build()
    activity.createBiometricPrompt(continuation).authenticate(promptInfo)
}

Our authenticate() method is called on the main thread using Dispatchers.Main.

Here is the code of the createBiometricPrompt() method:

fun FragmentActivity.createBiometricPrompt(continuation: Continuation<AuthenticationResult>): BiometricPrompt {
    return BiometricPrompt(
        this,
        object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                continuation.resume(AuthenticationResult.Error(errorCode, errString))
            }

            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                continuation.resume(AuthenticationResult.Succeeded(result))
            }

            override fun onAuthenticationFailed() {
                Timber.i("onAuthenticationFailed")
            }
        }
    )
}

Since we are sending a push to the device which is registered as the second factor we create a PendingIntent to present the transaction activity. We create the activity with the following code:

fun createPendingIntent(context: Context, transaction: Transaction): PendingIntent =
    Intent(context, TransactionActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_NO_HISTORY or
                Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
        putExtra(INTENT_EXTRA_TRANSACTION_APPROVAL, transaction)
    }.let { intent ->
        val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        PendingIntent.getActivity(context, 1484, intent, flags)
    }

We use the same code during login. There our code works without any issues on all devices. Therefore my guess that we might do something wrong creating the Intent/PendingIntent? 🤷

Does any of you has an idea what might cause our problem?

Already tried solutions


Solution

  • I just found a solution to my problem. Regarding the documentation here Start an Activity from a Notification they recommend using TaskStackBuilder to build your PendingIntent. Using TaskStackBuilder our activity isn't killed while the PIN screen gets/is shown.