All About PendingIntents

Nicole Borrelli
Android Developers
Published in
7 min readMar 23, 2021
Illustration by Molly Hensley

PendingIntents 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 PendingIntents 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), PendingIntents 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 PendingIntents 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 Uris 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 PendingIntents 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 PendingIntents 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!

--

--