Skip to main content

PendingIntent identity, mutability, and security

A PendingIntent is a system-held capability: another process may trigger an operation as your app, even when your process is gone. Treat it like a scoped authorization token.

Prefer explicit and immutable​

val intent = Intent(context, MainActivity::class.java).apply {
action = ACTION_OPEN_ORDER
data = "https://shop.example.com/orders/$orderId".toUri()
}

val contentIntent = PendingIntent.getActivity(
context,
orderId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)

FLAG_IMMUTABLE prevents the holder from filling in/changing intent semantics. The creator can still update its token using FLAG_UPDATE_CURRENT.

Use FLAG_MUTABLE only when the platform must supply values—for example inline reply. Even then, make the underlying intent explicit and tightly validate received values.

Identity surprises​

The system compares the operation type, request code, and the Intent’s filter identity (action, data, type, component, categories). Extras do not distinguish two PendingIntents.

// ❌ Same requestCode and same filter identity: second may update/reuse first token.
PendingIntent.getActivity(context, 0, base.putExtra("orderId", "A"), flags)
PendingIntent.getActivity(context, 0, base.putExtra("orderId", "B"), flags)

// ✅ Give each destination stable identity.
Intent(context, MainActivity::class.java).apply {
data = "https://shop.example.com/orders/$orderId".toUri()
}

This explains the classic bug where tapping notification A opens order B: all notifications shared one token and only extras changed.

Update, cancel, and one-shot​

  • FLAG_UPDATE_CURRENT: creator updates extras on an existing matching token.
  • FLAG_CANCEL_CURRENT: invalidate matching old token and create another.
  • FLAG_ONE_SHOT: token can be sent only once; useful when replay is unsafe.
  • FLAG_NO_CREATE: look up without creating.

One-shot limits token replay, but the receiving operation still needs domain-level idempotency and authorization.

Inline reply​

val replyIntent = Intent(context, ReplyReceiver::class.java).apply {
action = ACTION_REPLY
data = "app://conversation/$conversationId".toUri()
}
val replyPendingIntent = PendingIntent.getBroadcast(
context,
conversationId.hashCode(),
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)

Validate conversation access after the receiver starts. Never trust an ID merely because it arrived through your PendingIntent.

Authority is checked at execution time

A notification created while Alice was signed in might be tapped after Bob signs in. Resolve current session and authorize access before showing or changing the resource.

Sources​

Interview question​

Why do different extras not create different PendingIntents? Extras are excluded from Intent filter equality. Use distinct request codes or stable action/data/component identity, based on the operation’s semantics.