Pendingintents are a very important part of the Android framework, but most of the current developer resources on the topic focus on the implementation details, that is, “PendingIntents are token references maintained by the system,” rather than their purpose.

Because Android 12 has a significant update to PendingIntents.Including the need to explicitly determine whether pendingintentsare mutable, I thought it was worth talking more about what pendingintentsdo and how the system uses them. And why you need a mutable PendingIntent.

What is a PendingIntent?

PendingIntent objects encapsulate the Intent’s functionality and specify what actions other applications are allowed to perform in the name of your application in response to future actions that the user will perform. For example, encapsulated intents might be triggered when an alarm is turned off or when the user clicks on a notification.

The key to pendingIntents is that other applications trigger the intent in your application’s name. In other words, other applications use your application’s identity to trigger intents.

In order for a PendingIntent to function like a normal Intent, the system triggers it using the same identity as when it was created. In most cases, such as alarms and notifications, the identity used is the app itself.

Let’s look at the different ways that applications use PendingIntents, and why we use them.

Regular use

The most common and basic use of pendingIntents is as an action to do 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
)
Copy the code

You can see that we build a standard Intent type to open our application, and then simply wrap it around a PendingIntent before adding it to the notification.

In this case, we use the FLAG_IMMUTABLE flag to build a PendingIntent that cannot be modified because we know exactly what operations will be performed in the future.

Call NotificationManagerCompat. Notify () after work is completed. When the system displays a notification and the user clicks on the notification, we call pendingIntent.send () on our PendingIntent to launch our application.

Update the immutable PendingIntent

You might think that if an application needs to update a PendingIntent, it needs to be mutable, but it’s not. Pendingintents created by an application can be updated with the FLAG_UPDATE_CURRENT flag.

val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {
   action = NOTIFICATION_ACTION
   data = differentDeepLink
}

// Since we used the FLAG_UPDATE_CURRENT flag, we can update what we created above
// PendingIntent
val updatedPendingIntent = PendingIntent.getActivity(
   applicationContext,
   NOTIFICATION_REQUEST_CODE,
   updatedIntent,
   PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// The PendingIntent has been updated
Copy the code

In the following section we’ll explain why we set the PendingIntent to a mutable type.

Across application API

Common usage is not limited to interacting with systems. While it is very common to use startActivityForResult() and onActivityResult() to receive a callback after some operation, it is not the only use.

Imagine an online ordering application that provides an API to integrate with other applications. Once the Intent has started the food ordering process, applications can access the PendingIntent as the Intent’s extra. Once the order has been delivered, the ordering application only needs to launch the PendingIntent once.

In this case, the ordering application uses the PendingIntent instead of sending the activity result directly, because it might take longer for the order to be submitted, and it wouldn’t make sense for the user to wait in the process.

We want to create an immutable PendingIntent because we don’t want an online ordering application to modify our Intent. When the order takes effect, we just want other apps to send it and leave it as it is.

Variable PendingIntent

But what if we, as ordering app developers, want to add a feature that allows users to send messages back to the app that invoked the ordering feature? For example, you can have the calling app prompt, “It’s pizza time!”

The variable PendingIntent is used to achieve this effect.

Since a PendingIntent is essentially a wrapper for an Intent, one might think that it is possible to obtain the wrapped Intent with a pendingIntent.getintent () method. But the answer is no. So how do you do that?

In addition to the send() method that takes no parameters, there are other versions of the send() method in pendingIntents, including this one that accepts the Intent as an argument:

fun PendingIntent.send(
   context: Context! , code:Int,
   intent: Intent?).
Copy the code

Instead of replacing the Intent wrapped by the PendingIntent, the Intent arguments are populated with the Intent that was wrapped when the Intent was created.

Let’s look at the following 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
)
Copy the code

The PendingIntent here will be passed to our online ordering application. When the delivery is complete, the application can get a customerMessage and send it back as extra for the intent, as shown in the following example:

val intentWithExtrasToFill = Intent().apply {
  putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
}
mutablePendingIntent.send(
  applicationContext,
  PENDING_INTENT_CODE,
  intentWithExtrasToFill
)
Copy the code

The calling application gets EXTRA_CUSTOMER_MESSAGE extra in its Intent and displays the message.

Special considerations when declaring mutable PendingIntents

⚠️ When creating mutable PendingIntents, always explicitly set the Component of the Intent to be launched. This can be done the way we implemented it above, explicitly setting the exact class name to receive, but it can also be done with intent.setComponent ().

Your application might call intent.setpackage () in some scenarios to make it easier. Be aware, however, that it is possible to match more than one component. Specify a specific component if you can.

⚠️ If you attempt to override a value in a PendingIntent created using FLAG_IMMUTABLE, the operation will fail without prompting, passing the Intent that is raw and unmodified.

Remember that an application can always update its own PendingIntent, even with immutable types. The only reason a PendingIntent is mutable is because other applications need to update the Intent in some way.

Details on marking

We’ve covered a few of the tags that can be used to create pendingIntents, and we’ll cover a few more.

FLAG_IMMUTABLE: Indicates that intents sent by other applications to a PendingIntent.send() cannot be modified. An application can always modify its own PendingIntent with the FLAG_UPDATE_CURRENT flag.

Prior to Android 12, pendingIntents created without this tag were mutable by default.

⚠️ Before Android 6 (API 23), pendingIntents were mutable.

🆕FLAG_MUTABLE: Indicates that an intent passed by pendingIntent.send () can be merged into a PendingIntent by an application.

⚠️ For any mutable PendingIntent, always set ComponentName for the Intent wrapped in it. Failure to perform this operation may cause security risks.

This tag was added in the Android 12 release. Prior to Android 12, any PendingIntent created without FLAG_IMMUTABLE was implicitly mutable.

FLAG_UPDATE_CURRENT: Initiates a request to the system to update an existing PendingIntent with new extra data instead of saving the new intent. If the PendingIntent is not registered, it is registered.

FLAG_ONE_SHOT: Allows a PendingIntent (via pendingIntent.send ()) to be sent only once. When passing a PendingIntent, it is important that the Intent inside the Intent can only be sent once. This mechanism may be convenient or prevent the application from performing an operation more than once.

🔐 uses FLAG_ONE_SHOT to avoid problems such as “replay attacks”.

FLAG_CANCEL_CURRENT: FLAG_CANCEL_CURRENT: Cancels an existing PendingIntent before registering a new one. This flag is used when a PendingIntent is sent to an application and you want to forward it to another application and update the data in it. With FLAG_CANCEL_CURRENT, previous applications will no longer be able to call the send method, but future applications will be able to.

Receive PendingIntent

In some cases, systems or other frameworks will return a PendingIntent as an API call. A typical example is method MediaStore createWriteRequest (), it is new in Android 11.

static fun MediaStore.createWriteRequest(
   resolver: ContentResolver,
   uris: MutableCollection<Uri>): PendingIntent
Copy the code

Just as our application creates a PendingIntent that runs as our application, the system creates a PendingIntent that runs as the system. In the case of the API here, it allows an application to start an Activity and give write permission to our application Uri collection.

conclusion

In this article, we describe how pendingIntents can be used as an Intent wrapper to enable a system or other application to launch the Intent created by that application at some point in the future.

We also explained why PendingIntents need to be set to immutable, and how doing so does not affect the application’s ability to modify the PendingIntents it creates. You can do this by FLAG_UPDATE_CURRENT plus FLAG_IMMUTABLE.

We also covered the precautions you need to take if a PendingIntent is mutable — make sure that the Intent is ComponentName.

Finally, we showed how sometimes a system or framework provides PendingIntent to an application so that we can decide how and when to run them.

These updates to PendingIntent are in line with Android 12’s improved application security. For more, check out our previous tweet about the arrival of the first Developer Preview for Android 12.

To learn more, feel free to test your app with the Android 12 Developer Preview and tell us about your experience.