From 1aba3ad7642576da4909a3c3806ae3dfc55689ab Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Tue, 8 Sep 2026 12:11:24 +0000 Subject: [PATCH] fix(messaging,android): skip onMessageOpenedApp for terminated notification taps Terminated launches already store the message for getInitialMessage(); also invoking the stream handled the same tap twice. --- .../FlutterFirebaseMessagingPlugin.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingPlugin.java b/packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingPlugin.java index 8587fcd98e45..14282cfc5c63 100644 --- a/packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingPlugin.java +++ b/packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingPlugin.java @@ -124,7 +124,9 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { // The notification tap created this Activity, so the Messaging SDK has already logged // `notification_open` from FcmLifecycleCallbacks#onActivityCreated. Handle the intent // without logging it a second time. - handleNotificationIntent(mainActivity.getIntent()); + // Terminated launch: getInitialMessage() owns this tap. Do not also fire + // onMessageOpenedApp (firebase/flutterfire#18661). + handleNotificationIntent(mainActivity.getIntent(), /* shouldNotifyStream= */ false); } } } @@ -630,7 +632,8 @@ public boolean onNewIntent(@NonNull Intent intent) { // FcmLifecycleCallbacks#onActivityCreated for this intent and `notification_open` was not // logged. Log it here before handling the intent. logNotificationOpen(intent); - return handleNotificationIntent(intent); + // Background resume: onMessageOpenedApp owns this tap. + return handleNotificationIntent(intent, /* shouldNotifyStream= */ true); } /** @@ -700,7 +703,17 @@ private static String getMessageId(@NonNull Bundle extras) { return messageId; } - private boolean handleNotificationIntent(@NonNull Intent intent) { + /** + * Handles a notification-tap intent. + * + *

{@code shouldNotifyStream} is {@code false} when the tap created this Activity (terminated + * launch). In that case the message is stored for {@code getInitialMessage()} only, matching iOS + * and the Dart API docs. It is {@code true} when the Activity already existed ({@link + * #onNewIntent}), which is a resume from background and should fire {@code onMessageOpenedApp}. + * + * @see #18661 + */ + private boolean handleNotificationIntent(@NonNull Intent intent, boolean shouldNotifyStream) { if (intent.getExtras() == null) { return false; } @@ -740,7 +753,10 @@ private boolean handleNotificationIntent(@NonNull Intent intent) { message.put("notification", initialMessageNotification); } - channel.invokeMethod("Messaging#onMessageOpenedApp", message); + // On a terminated launch, getInitialMessage() owns this message. + if (shouldNotifyStream) { + channel.invokeMethod("Messaging#onMessageOpenedApp", message); + } mainActivity.setIntent(intent); return true; }