From 00370f42231e026f513ae274b4cc04e775a1e492 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 6 Aug 2026 14:39:08 +0200 Subject: [PATCH 1/2] fix(messaging,android): fixing an issue with analytics being triggered twice --- .../firebase_messaging/android/build.gradle | 5 + .../FlutterFirebaseMessagingPlugin.java | 98 ++++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/firebase_messaging/firebase_messaging/android/build.gradle b/packages/firebase_messaging/firebase_messaging/android/build.gradle index 8a7291976765..ae05b563e1d7 100644 --- a/packages/firebase_messaging/firebase_messaging/android/build.gradle +++ b/packages/firebase_messaging/firebase_messaging/android/build.gradle @@ -59,12 +59,17 @@ android { disable 'InvalidPackage' } + testOptions { + unitTests.returnDefaultValues = true + } + dependencies { api firebaseCoreProject implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") implementation 'com.google.firebase:firebase-messaging' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation 'androidx.annotation:annotation:1.7.1' + testImplementation 'junit:junit:4.13.2' } } 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 92eb298b4097..44c7dad077bb 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 @@ -11,9 +11,12 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Bundle; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationManagerCompat; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; @@ -21,7 +24,9 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.messaging.Constants; import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.MessagingAnalytics; import com.google.firebase.messaging.RemoteMessage; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -33,9 +38,11 @@ import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.NewIntentListener; import io.flutter.plugins.firebase.core.FlutterFirebasePlugin; +import java.util.ArrayDeque; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Queue; /** FlutterFirebaseMessagingPlugin */ public class FlutterFirebaseMessagingPlugin @@ -45,7 +52,14 @@ public class FlutterFirebaseMessagingPlugin FlutterPlugin, ActivityAware { + private static final String TAG = "FLTFireMsgPlugin"; + + /** Mirrors the de-duplication window the Messaging SDK keeps in FcmLifecycleCallbacks. */ + private static final int RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE = 10; + private final HashMap consumedInitialMessages = new HashMap<>(); + private final Queue recentlyLoggedMessageIds = + new ArrayDeque<>(RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE); private MethodChannel channel; private Activity mainActivity; @@ -104,7 +118,10 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { if (mainActivity.getIntent() != null && mainActivity.getIntent().getExtras() != null) { if ((mainActivity.getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) { - onNewIntent(mainActivity.getIntent()); + // 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()); } } } @@ -533,13 +550,86 @@ private Map getExceptionDetails(@Nullable Exception exception) { @Override public boolean onNewIntent(@NonNull Intent intent) { + // The Activity already existed, so the Messaging SDK never ran + // FcmLifecycleCallbacks#onActivityCreated for this intent and `notification_open` was not + // logged. Log it here before handling the intent. + logNotificationOpen(intent); + return handleNotificationIntent(intent); + } + + /** + * Logs the Analytics {@code notification_open} event for a notification tap that did not create + * the Activity. + * + *

The Messaging SDK only logs this event from {@code FcmLifecycleCallbacks#onActivityCreated}, + * so it is missed whenever the app was merely backgrounded (for example with the Home button) and + * the Activity is reused. See firebase/flutterfire#17072 and firebase/firebase-android-sdk#3799. + * + *

This mirrors the SDK's own implementation, including de-duplication by message id, so a + * message is never counted twice. + */ + private void logNotificationOpen(@NonNull Intent intent) { + Bundle analyticsData; + try { + Bundle extras = intent.getExtras(); + if (extras == null) { + return; + } + + if (!markNotificationOpenAsLogged(getMessageId(extras))) { + return; + } + + analyticsData = extras.getBundle(Constants.MessageNotificationKeys.ANALYTICS_DATA); + } catch (RuntimeException e) { + // The intent can come from anywhere and may be malformed, so never crash the host app while + // reading analytics data out of it. + Log.w(TAG, "Failed to get analytics data from notification intent extras.", e); + return; + } + + if (MessagingAnalytics.shouldUploadScionMetrics(analyticsData)) { + MessagingAnalytics.logNotificationOpen(analyticsData); + } + } + + /** + * Records {@code messageId} as having had its {@code notification_open} event logged, and returns + * whether the caller should log it. + * + *

Returns {@code false} when this message id was logged recently, so that a message tapped + * more than once (or delivered through both the create and the new-intent path) is only counted + * once. A {@code null} id cannot be de-duplicated and is always logged, matching the SDK. + */ + @VisibleForTesting + boolean markNotificationOpenAsLogged(@Nullable String messageId) { + if (messageId == null) { + return true; + } + if (recentlyLoggedMessageIds.contains(messageId)) { + return false; + } + if (recentlyLoggedMessageIds.size() >= RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE) { + recentlyLoggedMessageIds.poll(); + } + recentlyLoggedMessageIds.add(messageId); + return true; + } + + /** Remote Message ID can be either one of the following... */ + @Nullable + private static String getMessageId(@NonNull Bundle extras) { + String messageId = extras.getString("google.message_id"); + if (messageId == null) messageId = extras.getString("message_id"); + return messageId; + } + + private boolean handleNotificationIntent(@NonNull Intent intent) { if (intent.getExtras() == null) { return false; } - // Remote Message ID can be either one of the following... - String messageId = intent.getExtras().getString("google.message_id"); - if (messageId == null) messageId = intent.getExtras().getString("message_id"); + String messageId = getMessageId(intent.getExtras()); if (messageId == null) { return false; } From 71fd38a8c7e989d0cc51cdf1404ff61b4b69e3d8 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 6 Aug 2026 14:58:47 +0200 Subject: [PATCH 2/2] clean --- .../firebase_messaging/android/build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/firebase_messaging/firebase_messaging/android/build.gradle b/packages/firebase_messaging/firebase_messaging/android/build.gradle index ae05b563e1d7..8a7291976765 100644 --- a/packages/firebase_messaging/firebase_messaging/android/build.gradle +++ b/packages/firebase_messaging/firebase_messaging/android/build.gradle @@ -59,17 +59,12 @@ android { disable 'InvalidPackage' } - testOptions { - unitTests.returnDefaultValues = true - } - dependencies { api firebaseCoreProject implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") implementation 'com.google.firebase:firebase-messaging' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation 'androidx.annotation:annotation:1.7.1' - testImplementation 'junit:junit:4.13.2' } }