Android Protected Confirmation - Unexpected Behavior, Edge Cases and Considerations

Jan 7, 2021

Today was the first time I played around with Android Protected Confirmation and I discovered some unexpected behavior that I thought is worth documenting.

What is Android Protected Confirmation?

You can use Android Protected Confirmation (or just ConfirmationPrompt) to really really make sure the user sees and confirms a specific message. It doesn’t just open a new Dialog or Activity, but overwrites the screen content from very deep down on the hardware level. This makes sure that nothing can pop up over the message. Additionally, the user has to confirm the message using physical hardware buttons (not just by tapping on the screen). The Android feature was added in API 28 (Android 9).

An Android Protected Confirmation looks like this:

(Note: This is a photo because an Android Protected Confirmation cannot be screenshotted)

This kind of confirmation is intended to authorize critical tasks like a financial transaction or steering medical equipment.

It’s also possible to link the confirmation to a Key in the KeyStore and only make it usable when the user confirmed the Protected Confirmation (see setUserConfirmationRequired(true)).

How to Use

The usage is quite straight-forward:

import android.content.Context
import android.security.ConfirmationCallback
import android.security.ConfirmationPrompt
import java.util.concurrent.Executor


val promptText = "My first ConfirmationPrompt!"
val extraData: ByteArray = byteArrayOf() // any additional data

val threadReceivingCallback = Executor { runnable -> runnable.run() }
val callback = object : ConfirmationCallback() {
    override fun onConfirmed(dataThatWasConfirmed: ByteArray) {
        super.onConfirmed(dataThatWasConfirmed)
        // success!
        // do something here
    }

    override fun onDismissed() {
        super.onDismissed()
        // do something here
    }

    override fun onCanceled() {
        super.onCanceled()
        // do something here
    }

    override fun onError(e: Throwable?) {
        super.onError(e)
        // do something here
    }
}

val dialog = ConfirmationPrompt.Builder(context)
    .setPromptText(promptText)
    .setExtraData(extraData)
    .build()

dialog.presentPrompt(threadReceivingCallback, callback)

(go to a runnable code example)

Considerations

The following sections discuss unexpected behavior, edge cases, and other things to consider.

Lacking Support

I only got Android Protected Confirmations working on a Google Pixel 3a (Android 11). It doesn’t work on Samsung Galaxy A50 (Android 10) and it also doesn’t work on the Android Emulator (Android 11). On the not supported devices, the presentPrompt call throws a ConfirmationNotAvailableException without a message set.

Because it only works on a Google phone and not on Samsung, I assume that currently, only very few devices support Android Protected Confirmation. If you want to use Android Protected Confirmation in production, make sure that the users only use supported phones or provide a fallback. However, a software-implemented fallback doesn’t provide the same guarantees as Android Protected Confirmation.

Incompatible with AccessibilityServices

If an AccessibilityService is running, the Android Protected Confirmation cannot be used and presentPrompt throws a ConfirmationNotAvailableException. That’s unfortunate for people that rely on Accessibility Services. And it’s not possible to use the exception to distinguish if Android Protected Confirmation is not supported or disabled because of a running AccessibilityService.

It’s possible to check for support by calling ConfirmationPrompt.isSupported(context). What’s weird is that according to the docs it’s still possible that the exception is thrown, even if isSupported returned true. I couldn’t reproduce that behavior. For me isSupported returned false when an AccessibilityService was enabled, but since it’s stated in the docs, it safer to always expect a ConfirmationNotAvailableException.

No Formatting

promptText expects a CharSequence, so you could pass a formatted Spannable to it. However, internally the promptText is converted into a simple String, so no custom formatting is supported on Protected Confirmations, just plain text.

No Line Breaks, No Emojis, and Not Too Long

When promptText contains line breaks (\n) or emojis or other unsupported characters, promptText throws an IllegalArgumentException. Again, without further information in the exception’s message. To learn more about the cause you can lookout in the Logcat for:

W/ConfirmationPrompt: Unexpected responseCode=… from presentConfirmationPrompt() call.

I could find these error codes (source 1, source 2):

Error Codes Meaning
4 CONFIRMATIONUI_IGNORED
5 CONFIRMATIONUI_SYSTEM_ERROR
6 CONFIRMATIONUI_UNIMPLEMENTED
7 CONFIRMATIONUI_UNEXPECTED
65536 CONFIRMATIONUI_UIERROR
65537 CONFIRMATIONUI_UIERROR_MISSING_GLYPH
65538 CONFIRMATIONUI_UIERROR_MESSAGE_TOO_LONG
65539 CONFIRMATIONUI_UIERROR_MALFORMED_UTF8_ENCODING

That being said, for too long messages I always get 65536 (CONFIRMATIONUI_UIERROR) or even 5 (CONFIRMATIONUI_SYSTEM_ERROR) instead of the expected 65538 (CONFIRMATIONUI_UIERROR_MESSAGE_TOO_LONG). I don’t know why.

So when using Android Protected Confirmation, be very careful about the text you show. Avoid showing dynamically created texts or showing parts of user inputs. If you show user input, make sure that the user didn’t use unsupported characters and that the message doesn’t become too long. Also depending on the user’s language, unsupported characters could become a big problem. Unfortunately, it’s not documented which characters are allowed, or what’s the length limit.

Just Doesn’t Work Sometimes

I also experienced that presentPrompt just throws an IllegalArgumentException for no apparent reason. This happened especially after I tried to test out the maximum promptText length. So maybe exceeding the promptText max length destroys some internal state. If you receive IllegalArgumentException and you don’t know why it’s time to reboot the device.

No Empty Strings

A null or empty promptText is not allowed. This makes sense. But in contrast to all the other errors, for empty strings already the ConfirmationPrompt.Builder’s build() call throws an IllegalArgumentException, not the presentPrompt call.

Concurrent Confirmations

One core idea of Android Protected Confirmations is that nothing can pop up on top. For this reason, it’s not possible to show an Android Protected Confirmations when already another Confirmation is shown. When trying to do so, presentPrompt will throw a ConfirmationAlreadyPresentingException.

onPause and onStop Don’t Get Called

When the Android Protected Confirmation is shown, the currently active Activity stays in the onResume state. Neither onStop nor onPause gets called which might be unexpected. This can be explained by the Android Protected Confirmation not being an Activity, but a hardware-overwrite on the screen. So the App and probably even the OS doesn’t know that something is painting over it. This also explains why Android Protected Confirmations are not visible in screenshots.

Crashes

If the app crashes while an Android Protected Confirmation is shown, the Confirmation is automatically closed, which is good.

Phone Calls

If the device receives a phone call while an Android Protected Confirmation is shown, the device rings and vibrates but there is no visible indication on the screen. Just the Android Protected Confirmation is shown. After it is confirmed or dismissed, the call notification is shown as usual.

Conclusion

It was fun playing around with Android Protected Confirmation, but I wouldn’t use it in production. The main reasons are the lacking support and missing documentation. I spent a lot of time trying things out and browsing through the Android source code. When the support of non-Google manufacturers isn’t improved, I don’t see any future of this feature, since almost no phones will support it. If this post helped you, let me know!

If you want to try it yourself, you can use my ConfirmationPrompt-Playground on GitHub:

You might also like