Skip to content

[Bug] Action buttons fire on the wrong notification when 2+ are in the tray (PendingIntent request code collision) #69

Description

@nesho84

Pre-submission checklist

  • I have searched existing issues and this bug has not been reported
  • I have checked the "Bugs Fixed from Upstream Notifee" table in the README
  • I am using the latest version of react-native-notify-kit

react-native-notify-kit version

10.5.0

React Native version

0.86.2

Platform

Android

OS version

Android 16

Description

What happened:

With two or more notifications live in the tray, notification action buttons operate on the
wrong notification:

  1. Pressing one notification's action button delivers the event carrying a different
    notification's notification and pressAction bundles. In my app, pressing "✓ Prayed" on
    one prayer marked a different prayer as prayed.
  2. Swiping one notification away also removes an unrelated notification from the tray (the
    delete intent fires with another notification's payload). No groupId is set anywhere, so
    this is not Android group behaviour.
  3. Once this happens, the remaining notifications' buttons stop responding entirely — in the
    foreground as well as background. Only swipe-dismiss still works.

It is intermittent: it depends on whether the app process restarted between two notifications
being posted (see root cause below — the request code counter resets on process start).

A single notification in the tray always behaves correctly. Tapping the notification body
always works correctly too, even while its action buttons are broken — only action buttons and
delete intents are affected.

What I expected:

Pressing an action button delivers the event for the notification whose button was pressed,
and swiping a notification dismisses only that notification — regardless of how many
notifications are live or whether the app process restarted in between.

Minimal reproduction

The symptoms above were observed repeatedly in production in my own app (a prayer-times app
posting several notifications a day, each with three action buttons). The snippet below isolates
the conditions, derived from the ReceiverService code path.

The key requirement is that the two notifications are posted in different process lifetimes,
because the PendingIntent request code is a static counter that resets to 0 on process start.
Posting both within one app session will not reproduce it.

import notifee, { EventType } from 'react-native-notify-kit';

async function post(id, title) {
  const channelId = await notifee.createChannel({ id: 'repro', name: 'Repro' });
  await notifee.displayNotification({
    id,
    title,
    data: { which: id },
    android: {
      channelId,
      // No launchActivity / mainComponent => headless action
      // => routed to ReceiverService.ACTION_PRESS_INTENT
      actions: [{ title: 'Done', pressAction: { id: `done-${id}` } }],
    },
  });
}

notifee.onForegroundEvent(({ type, detail }) => {
  if (type === EventType.ACTION_PRESS) {
    console.log('pressed:', detail.pressAction?.id, '| payload:', detail.notification?.data?.which);
  }
});

// Wire post('notif-a', 'A') and post('notif-b', 'B') to two buttons.

Steps:

  1. Tap the button that calls post('notif-a', 'A') — notification A appears in the tray.
  2. Swipe the app away from recents so the process is killed. Leave A in the tray.
  3. Reopen the app (fresh process — the counter restarts at 0) and call post('notif-b', 'B').
    Both A and B are now live in the tray.
  4. Press A's "Done" button.

Expected log:

pressed: done-notif-a | payload: notif-a

Actual log:

pressed: done-notif-b | payload: notif-b

Swiping A away instead of pressing it removes B as well, via the same collision on the delete
intent.

Logs

No logs to attach — this failure produces no log output.

There is no crash, exception, or warning at any layer. Android resolves and delivers the
PendingIntent successfully; it simply carries another notification's extras. From logcat's point
of view the press is handled normally and the "wrong" payload is indistinguishable from a correct
one, so a capture would show a perfectly ordinary, successful action-press event.

The evidence is therefore in the source rather than the log stream — see the root cause in
Additional context, which identifies the exact lines and explains why the collision occurs.

Happy to attach a logcat capture if it would still help; just say what you'd like to see.

Additional context

Device: Samsung Galaxy S22 Ultra, One UI 8

Root cause

android/src/main/java/app/notifee/core/ReceiverService.java:

private static final AtomicInteger uniqueIds = new AtomicInteger(0);   // L49
...
int uniqueInt = uniqueIds.getAndIncrement();                            // L79
return PendingIntent.getService(
    context, uniqueInt, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);

uniqueIds is unique only within a single process lifetime. Android matches PendingIntents on
request code + Intent (action, component, data, categories) and ignores extras. Every intent
built here shares the same component (ReceiverService) and one of three constant actions, so
the request code is the only discriminator — while the notification id and pressAction.id
travel purely as extras.

So when the process restarts, the counter returns to 0 and newly posted notifications are handed
request codes still held by live tray notifications. FLAG_UPDATE_CURRENT then overwrites the
older PendingIntent's extras with the newer notification's bundles, and pressing the older
notification's button delivers the newer one's payload.

This is easy to hit with trigger notifications specifically, since they are often posted from a
fresh headless process (rehydrated from Room after an app kill), where the counter always starts
at 0.

Why the delete intent is affected too

setDeleteIntent has no branch — it always uses ReceiverService.createIntent
(NotificationManager.java L160-164), so every notification's delete intent is exposed
regardless of its pressAction configuration. That's the collateral-dismissal symptom.

Why tapping the body is unaffected

A body pressAction of { id: 'default', launchActivity: 'default' } routes to
NotificationPendingIntent.createIntent, which uses a collision-free request code:

int uniqueInt = UUID.randomUUID().hashCode();   // NotificationPendingIntent.java L78

(validateAndroidPressAction only defaults launchActivity when id === 'default', so ordinary
action buttons never receive launch metadata.)

Relationship to #38

#38 fixed headless action buttons foregrounding the app by reusing
shouldCreateLaunchActivityIntent(...) when selecting the action's PendingIntent implementation
(NotificationManager.java L455-475). That change is correct in intent, but it moved every
headless action button off the activity path (unique UUID request codes) and onto
ReceiverService (the resetting counter) — which is where this collision lives. So it can't be
addressed by reverting #38; ReceiverService needs request codes that are unique and stable in
their own right.

Suggested fix

Derive the request code from data already passed into createIntent — the notification id, plus
the pressAction id where present — instead of the counter:

StringBuilder key = new StringBuilder(action);
for (Bundle b : extraBundles) {
  if (b != null) key.append(':').append(b.getString("id"));
}
int uniqueInt = key.toString().hashCode();

Every call site already supplies these bundles ({"notification"} for the delete intent,
{"notification", "pressAction"} for press/action intents), and both carry an "id" key —
NotificationModel.getId() reads mNotificationBundle.getString("id"). This yields distinct,
restart-stable codes such as ACTION_PRESS_INTENT:notif-a:done vs ACTION_PRESS_INTENT:notif-b:done.

Stable is preferable to random here: it keeps FLAG_UPDATE_CURRENT doing what it is meant to do —
a re-posted notification (e.g. a DAILY repeat) updates its own PendingIntent rather than
hijacking a neighbour's or leaking a new one on every reschedule.

NotificationModel.getHashCode() (getId().hashCode()) already exists if you would prefer to
build on that.

Related

Upstream Notifee #196 reports the same
user-visible behaviour (multiple trigger notifications with quick actions; press delivers the
wrong one; explicitly intermittent).

Architecture

  • New Architecture (Fabric / TurboModules)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions