Skip to content

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

Open
akashdh11 wants to merge 1 commit into
firebase:mainfrom
akashdh11:fix/messaging-ignore-dismissed-notifications
Open

akashdh11 wants to merge 1 commit into
firebase:mainfrom
akashdh11:fix/messaging-ignore-dismissed-notifications

Conversation

@akashdh11

@akashdh11 akashdh11 commented Nov 3, 2026 •

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 here: the Dart unit tests (firebase_messaging 22, its platform interface 111, firebase_messaging_web 2 in Chrome), the Android unit tests (11), and the whole e2e suite (integration_test/e2e_test.dart) on an Android 17 emulator, once on main and once on this branch.
    • Every test that passes on main passes here, plus the new one (12 passed against 11).
    • The 9 e2e tests that fail here fail the same way on main. Six need the real Firebase project (tokens, FIDs, topics); this run had the example's dummy configuration. Three permission tests reach the host's adb server, which needs exactly one device attached.
  • 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

@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 introduces logic to correctly identify and ignore notification dismissal events in the Firebase Messaging plugin, preventing them from being incorrectly processed as messages. The changes include a new helper method in the receiver, updated integration tests, and a new unit test suite. The reviewer provided actionable feedback to improve maintainability by using a package-private constant for the intent extra key, which is a valuable improvement.


public class FlutterFirebaseMessagingReceiver extends BroadcastReceiver {
private static final String TAG = "FLTFireMsgReceiver";
private static final String EXTRA_WRAPPED_INTENT = "wrapped_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

The constant EXTRA_WRAPPED_INTENT can be made package-private (by removing the private modifier) so that it can be reused in the corresponding unit test FlutterFirebaseMessagingReceiverTest instead of hardcoding the string literal "wrapped_intent" there.

Suggested change
private static final String EXTRA_WRAPPED_INTENT = "wrapped_intent";
static final String EXTRA_WRAPPED_INTENT = "wrapped_intent";

@Override
@SuppressWarnings({"deprecation", "unchecked"})
public <T extends Parcelable> T getParcelableExtra(String name) {
return "wrapped_intent".equals(name) ? (T) wrapped : null;

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

Reuse the package-private constant EXTRA_WRAPPED_INTENT from FlutterFirebaseMessagingReceiver instead of hardcoding the string literal "wrapped_intent" to improve maintainability.

Suggested change
return "wrapped_intent".equals(name) ? (T) wrapped : null;
return FlutterFirebaseMessagingReceiver.EXTRA_WRAPPED_INTENT.equals(name) ? (T) wrapped : null;

This branch has not been deployed

No deployments
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