Skip to content

fix(messaging,android): ignore notification events in the messaging receiver - #18898

Closed
akashdh11 wants to merge 1 commit into
firebase:mainfrom
akashdh11:fix/messaging-android-dismiss-broadcast
Closed

akashdh11 wants to merge 1 commit into
firebase:mainfrom
akashdh11:fix/messaging-android-dismiss-broadcast

Conversation

@akashdh11

Copy link
Copy Markdown

Description

On Android, when the user dismisses a notification that the Firebase SDK displayed (swipes it away, or taps "Clear all"), the app receives an empty RemoteMessage: no message ID, no notification, no data. It reaches onMessage while the app is in the foreground, and onBackgroundMessage otherwise. An app that draws its own notification for foreground messages shows a blank one. Steps to reproduce are in #18897.

Cause. firebase-messaging (25.1.3, and 26.0.1 from BoM 35.1.0) gives a notification it displays with analytics enabled (google.c.a.e) a delete intent: a broadcast with the com.google.android.c2dm.intent.RECEIVE action, sent to the app's package, with the real com.google.firebase.messaging.NOTIFICATION_DISMISS intent in the wrapped_intent extra (CommonNotificationBuilder.createDeleteIntent). In our tests, messages sent through the HTTP v1 API had google.c.a.e whether or not they set fcm_options.analytics_label. Since firebase/firebase-android-sdk#5410 (October 2023), that broadcast is addressed by action and package instead of to FirebaseInstanceIdReceiver by name, so it reaches every receiver for that action, FlutterFirebaseMessagingReceiver included.

Firebase's own receiver, CloudMessagingReceiver (the base of FirebaseInstanceIdReceiver), checks wrapped_intent first and handles such a broadcast as a notification event (it logs the notification_dismiss analytics event), never as a message. FlutterFirebaseMessagingReceiver has no such check, so it builds new RemoteMessage(intent.getExtras()) from the dismissal.

Change. FlutterFirebaseMessagingReceiver.onReceive ignores a broadcast whose wrapped_intent extra is an Intent, the same check CloudMessagingReceiver makes. Firebase's own handling is unchanged: FirebaseInstanceIdReceiver still receives the broadcast and logs the dismissal.

Tests.

  • Unit, FlutterFirebaseMessagingReceiverTest: a dismissal is a notification event; a message, and a wrapped_intent that is not an Intent, are not.
  • Integration: the example's tests/remote_notifications channel can now hand the receiver the dismissal broadcast before the messages (dismissalFirst), and a dismissed notification does not reach onMessage checks that only the message arrives. Before the change it failed:
    Expected: ['after-dismissal-1793742379013929']
      Actual: [null, 'after-dismissal-1793742379013929']
    
  • Ran: ./gradlew :firebase_messaging:testDebugUnitTest from tests/android (11 passed), and integration_test/remote_notification_delivery_test.dart on an Android 17 emulator (the 4 Android tests passed; the iOS ones are skipped there).
  • In a real app, with firebase_messaging 17.0.1 plus this change (BoM 35.1.0, firebase-messaging 26.0.1): two notifications displayed by Firebase, then "Clear all" with the app in the foreground, in the background, and with its process killed. Each dismissal logged broadcast received for a notification event, not a message. Ignored., nothing reached onMessage, and the "no onBackgroundMessage handler has been registered" warnings stopped. Without the change, each state got one empty message per dismissed notification.

Prior art. React Native Firebase has the same receiver and had the same bug (invertase/react-native-firebase#8040, closed as stale). invertase/react-native-firebase#9105 (July 2026) fixed it by ignoring broadcasts without google.message_id, or with a message_type other than gcm. This PR uses the narrower check from CloudMessagingReceiver, so it only drops what Firebase itself never treats as a message.

When the app's process is not running, Android still starts it to deliver the dismissal broadcast. The plugin can't avoid that; only addressing the delete intent to Firebase's receiver by name again would.

Related Issues

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]).
This will ensure a smooth and quick review process. Updating the pubspec.yaml and changelogs is not required.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • My PR includes unit or integration tests for all changed/updated/fixed behaviors (See Contributor Guide).
  • All existing and new tests are passing. (Ran the Android unit tests and the delivery integration tests above; the rest of the e2e suite needs the project's Firebase configuration.)
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (melos run analyze) does not report any problems on my PR. (dart analyze packages/firebase_messaging/firebase_messaging: no issues.)
  • I read and followed the Flutter Style Guide.
  • I signed the CLA.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

🤖 Generated with Claude Code

…eceiver

firebase-messaging reports the dismissal of a notification it displayed
with a broadcast that has the action messages arrive with
(com.google.android.c2dm.intent.RECEIVE), its NOTIFICATION_DISMISS intent
wrapped in the "wrapped_intent" extra. FlutterFirebaseMessagingReceiver
read that broadcast as a message, so every dismissal reached onMessage, or
onBackgroundMessage, as an empty RemoteMessage.

The receiver now ignores a broadcast whose "wrapped_intent" extra is an
Intent, the check Firebase's own CloudMessagingReceiver makes before it
handles a broadcast as a message.

Fixes firebase#18897

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@google-cla

google-cla Bot commented Nov 3, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request prevents dismissed notification events from being incorrectly processed as incoming data messages by the FlutterFirebaseMessagingReceiver. It introduces a helper method isNotificationEvent to detect these events and ignore them, accompanied by unit and integration tests. The feedback suggests adding a null check for the intent parameter in isNotificationEvent to ensure robust defensive programming and prevent potential NullPointerExceptions.

Comment on lines +114 to +116
static boolean isNotificationEvent(Intent intent) {
return intent.getParcelableExtra(EXTRA_WRAPPED_INTENT) instanceof Intent;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

To ensure robust defensive programming, consider adding a null check for the intent parameter before calling getParcelableExtra. Although intent is typically non-null when called from onReceive, adding a guard clause prevents potential NullPointerExceptions if the method is called directly in other contexts (such as unit tests).

  static boolean isNotificationEvent(Intent intent) {
    if (intent == null) {
      return false;
    }
    return intent.getParcelableExtra(EXTRA_WRAPPED_INTENT) instanceof Intent;
  }
References
  1. Enforce defensive programming by ensuring appropriate null checks exist before object property accesses for languages that support nullable references.

@akashdh11

Copy link
Copy Markdown
Author

Superseded by #18899: the same change, opened again because this PR's commit listed a co-author who can't sign the CLA.

@akashdh11 akashdh11 closed this Nov 3, 2026
@akashdh11
akashdh11 deleted the fix/messaging-android-dismiss-broadcast branch November 3, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant