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: