All About PendingIntents
PendingIntent
s are an important part of the Android framework, but most of the available developer resources focus on their implementation details — a “reference to a token maintained by the system” — rather than their usage.
Since Android 12 includes important changes to pending intents, including a change that requires explicitly deciding when a PendingIntent
is mutable or immutable, I thought it would be helpful to talk more what pending intents do, how the system uses them, and why you might occasionally want a mutable PendingIntent
.
What is a PendingIntent?
A PendingIntent
object wraps the functionality of an Intent
object while allowing your app to specify something that another app should do, on your app’s behalf, in response to a future action. For example, the wrapped intent might be invoked when an alarm goes off, or when the user taps on a notification.
A key aspect of pending intents is that another app invokes the intent on your app’s behalf. That is, the other app uses your app’s identity when invoking the intent.
In order for the PendingIntent
to have the same behavior as if it were a normal Intent
, the system triggers the PendingIntent
with the same identity as it was created with. In most situations, such as the alarm and notifications, this is the identity of the app itself.
Let’s take a look at the different ways our apps can work with PendingIntent
s and why we might want to use them in these ways.
Common case
The most common, and most basic, way to use a PendingIntent
is as the action associated with a notification:
val intent = Intent(applicationContext, MainActivity::class.java).apply {
action = NOTIFICATION_ACTION
data = deepLink
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(
applicationContext,
NOTIFICATION_CHANNEL
).apply {
// ...
setContentIntent(pendingIntent)
// ...
}.build()
notificationManager.notify(
NOTIFICATION_TAG,
NOTIFICATION_ID,
notification
)
We can see that we’re constructing a standard type of Intent
that will open our app, and then simply wrapping that in a PendingIntent
before adding it to our notification.
In this case, since we have an exact action we know we want to perform, we construct a PendingIntent
that cannot be modified by the app we pass it to by utilizing a flag called FLAG_IMMUTABLE
.
After we call NotificationManagerCompat.notify()
we’re done. The system will display the notification, and, when the user clicks on it, call PendingIntent.send()
on our PendingIntent
, starting our app.
Updating an immutable PendingIntent
You might think that if an app needs to update a PendingIntent
that it needs to be mutable, but that’s not always the case! The app that creates a PendingIntent
can always update it by passing the flag FLAG_UPDATE_CURRENT
:
val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {
action = NOTIFICATION_ACTION
data = differentDeepLink
}
// Because we're passing `FLAG_UPDATE_CURRENT`, this updates
// the existing PendingIntent with the changes we made above.
val updatedPendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
updatedIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// The PendingIntent has been updated.
We’ll talk about why one may want to make a PendingIntent
mutable in a bit.
Inter-app APIs
The common case isn’t only useful for interacting with the system. While it is most common to use startActivityForResult()
and onActivityResult()
to receive a callback after performing an action, it’s not the only way.
Imagine an online ordering app that provides an API to allow apps to integrate with it. It might accept a PendingIntent
as an extra
of its own Intent
that’s used to start the process of ordering food. The order app only starts the PendingIntent
once the order has been delivered.
In this case the ordering app uses a PendingIntent
rather than sending an activity result because it could take a significant amount of time for the order to be delivered, and it doesn’t make sense to force the user to wait while this is happening.
We want to create an immutable PendingIntent
because we don’t want the online ordering app to change anything about our Intent
. We just want them to send it, exactly as it is, when the order’s arrived.
Mutable PendingIntents
But what if we were the developers for the ordering app, and we wanted to add a feature that would allow the user to type a message that would be sent back to the app that called it? Perhaps to allow the calling app to show something like, “It’s PIZZA TIME!”
The answer to this is to use a mutable PendingIntent
.
Since a PendingIntent
is essentially a wrapper around an Intent
, one might think that there would be a method called PendingIntent.getIntent()
that one could call to get and update the wrapped Intent
, but that’s not the case. So how does it work?
In addition to the send()
method on PendingIntent
that doesn’t take any parameters, there are a few other versions, including this version, which accepts an Intent
:
fun PendingIntent.send(
context: Context!,
code: Int,
intent: Intent?
)
This intent
parameter doesn’t replace the Intent
that’s contained in the PendingIntent
, but rather it is used to fill in parameters from the wrapped Intent
that weren’t provided when the PendingIntent
was created.
Let’s look at an example.
val orderDeliveredIntent = Intent(applicationContext, OrderDeliveredActivity::class.java).apply {
action = ACTION_ORDER_DELIVERED
}
val mutablePendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
orderDeliveredIntent,
PendingIntent.FLAG_MUTABLE
)
This PendingIntent
could be handed over to our online order app. After the delivery is completed, the order app could take a customerMessage
and send that back as an intent extra like this:
val intentWithExtrasToFill = Intent().apply {
putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
}
mutablePendingIntent.send(
applicationContext,
PENDING_INTENT_CODE,
intentWithExtrasToFill
)
The calling app will then see the extra EXTRA_CUSTOMER_MESSAGE
in its Intent
and be able to display the message.
Important considerations when declaring pending intent mutability
⚠️ When creating a mutable PendingIntent
ALWAYS explicitly set the component that will be started in the Intent
. This can be done the way we’ve done it above, by explicitly setting the exact class that will receive it, but it can also be done by calling Intent.setComponent()
.
Your app might have a use case where it seems easier to call Intent.setPackage()
. Be very careful of the possibility to match multiple components if you do this. It’s better to specify a specific component to receive the Intent
if at all possible.
⚠️ If you attempt to override the values in a PendingIntent
that was created with FLAG_IMMUTABLE
will fail silently, delivering the original wrapped Intent
unmodified.
Remember that an app can always update its own PendingIntent
, even when they are immutable. The only reason to make a PendingIntent
mutable is if another app has to be able to update the wrapped Intent
in some way.
Details on flags
We’ve talked a bit about a few of the flags that can be used when creating a PendingIntent
, but there are a few others to cover as well.
FLAG_IMMUTABLE
: Indicates the Intent inside the PendingIntent
cannot be modified by other apps that pass an Intent
to PendingIntent.send()
. An app can always use FLAG_UPDATE_CURRENT
to modify its own PendingIntents
Prior to Android 12, a PendingIntent
created without this flag was mutable by default.
⚠️ On Android versions prior to Android 6 (API 23), PendingIntent
s are always mutable.
🆕 FLAG_MUTABLE
: Indicates the Intent inside the PendingIntent
should allow its contents to be updated by an app by merging values from the intent parameter of PendingIntent.send()
.
⚠️ Always fill in the ComponentName
of the wrapped Intent
of any PendingIntent
that is mutable. Failing to do so can lead to security vulnerabilities!
This flag was added in Android 12. Prior to Android 12, any PendingIntent
s created without the FLAG_IMMUTABLE
flag were implicitly mutable.
FLAG_UPDATE_CURRENT
: Requests that the system update the existing PendingIntent
with the new extra data, rather than storing a new PendingIntent
. If the PendingIntent
isn’t registered, then this one will be.
FLAG_ONE_SHOT
: Only allows the PendingIntent to be sent once (via PendingIntent.send()
). This can be important when passing a PendingIntent
to another app if the Intent
inside it should only be able to be sent a single time. This may be related to convenience, or to prevent the app from performing some action multiple times.
🔐 Utilizing FLAG_ONE_SHOT
prevents issues such as “replay attacks”.
FLAG_CANCEL_CURRENT
: Cancels an existing PendingIntent
, if one exists, before registering this new one. This can be important if a particular PendingIntent
was sent to one app, and you’d like to send it to a different app, potentially updating the data. By using FLAG_CANCEL_CURRENT
, the first app would no longer be able to call send on it, but the second app would be.
Receiving PendingIntents
Sometimes the system or other frameworks will provide a PendingIntent
as a return from an API call. One example is the method MediaStore.createWriteRequest()
that was added in Android 11.
static fun MediaStore.createWriteRequest(
resolver: ContentResolver,
uris: MutableCollection<Uri>
): PendingIntent
Just as a PendingIntent
created by our app is run with our app’s identity, a PendingIntent
created by the system is run with the system’s identity. In the case of this API, this allows our app to start an Activity
that can grant write permission to a collection of Uri
s to our app.
Summary
We’ve talked about how a PendingIntent
can be thought of as a wrapper around an Intent
that allows the system, or another app, to start an Intent
that one app created, as that app, at some time in the future.
We also talked about how a PendingIntent
s should usually be immutable and that doing so doesn’t prevent the app from updating its own PendingIntent
objects. The way it can do so is by using the FLAG_UPDATE_CURRENT
flag in addition to FLAG_IMMUTABLE
.
We’ve also talked about the precautions we should make — ensuring to fill in the ComponentName
of the wrapped Intent
— if a PendingIntent
is necessary to be mutable.
Finally, we talked about how sometimes the system, or frameworks, may provide PendingIntent
s to our app so that we can decide how and when to run them.
Updates to PendingIntent
were just one of the features in Android 12 designed to improve app security. Read about all the changes in the preview here.
Want to do even more? We encourage you to test your apps on the new developer preview of the OS and provide us feedback on your experiences!