1111import android .content .Intent ;
1212import android .content .pm .PackageManager ;
1313import android .os .Build ;
14+ import android .os .Bundle ;
15+ import android .util .Log ;
1416import androidx .annotation .NonNull ;
1517import androidx .annotation .Nullable ;
1618import androidx .annotation .RequiresApi ;
19+ import androidx .annotation .VisibleForTesting ;
1720import androidx .core .app .NotificationManagerCompat ;
1821import androidx .lifecycle .LiveData ;
1922import androidx .lifecycle .Observer ;
2023import com .google .android .gms .tasks .Task ;
2124import com .google .android .gms .tasks .TaskCompletionSource ;
2225import com .google .android .gms .tasks .Tasks ;
2326import com .google .firebase .FirebaseApp ;
27+ import com .google .firebase .messaging .Constants ;
2428import com .google .firebase .messaging .FirebaseMessaging ;
29+ import com .google .firebase .messaging .MessagingAnalytics ;
2530import com .google .firebase .messaging .RemoteMessage ;
2631import io .flutter .embedding .engine .plugins .FlutterPlugin ;
2732import io .flutter .embedding .engine .plugins .activity .ActivityAware ;
3338import io .flutter .plugin .common .MethodChannel .Result ;
3439import io .flutter .plugin .common .PluginRegistry .NewIntentListener ;
3540import io .flutter .plugins .firebase .core .FlutterFirebasePlugin ;
41+ import java .util .ArrayDeque ;
3642import java .util .HashMap ;
3743import java .util .Map ;
3844import java .util .Objects ;
45+ import java .util .Queue ;
3946
4047/** FlutterFirebaseMessagingPlugin */
4148public class FlutterFirebaseMessagingPlugin
@@ -45,7 +52,14 @@ public class FlutterFirebaseMessagingPlugin
4552 FlutterPlugin ,
4653 ActivityAware {
4754
55+ private static final String TAG = "FLTFireMsgPlugin" ;
56+
57+ /** Mirrors the de-duplication window the Messaging SDK keeps in FcmLifecycleCallbacks. */
58+ private static final int RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE = 10 ;
59+
4860 private final HashMap <String , Boolean > consumedInitialMessages = new HashMap <>();
61+ private final Queue <String > recentlyLoggedMessageIds =
62+ new ArrayDeque <>(RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE );
4963 private MethodChannel channel ;
5064 private Activity mainActivity ;
5165
@@ -104,7 +118,10 @@ public void onAttachedToActivity(ActivityPluginBinding binding) {
104118 if (mainActivity .getIntent () != null && mainActivity .getIntent ().getExtras () != null ) {
105119 if ((mainActivity .getIntent ().getFlags () & Intent .FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY )
106120 != Intent .FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY ) {
107- onNewIntent (mainActivity .getIntent ());
121+ // The notification tap created this Activity, so the Messaging SDK has already logged
122+ // `notification_open` from FcmLifecycleCallbacks#onActivityCreated. Handle the intent
123+ // without logging it a second time.
124+ handleNotificationIntent (mainActivity .getIntent ());
108125 }
109126 }
110127 }
@@ -533,13 +550,86 @@ private Map<String, Object> getExceptionDetails(@Nullable Exception exception) {
533550
534551 @ Override
535552 public boolean onNewIntent (@ NonNull Intent intent ) {
553+ // The Activity already existed, so the Messaging SDK never ran
554+ // FcmLifecycleCallbacks#onActivityCreated for this intent and `notification_open` was not
555+ // logged. Log it here before handling the intent.
556+ logNotificationOpen (intent );
557+ return handleNotificationIntent (intent );
558+ }
559+
560+ /**
561+ * Logs the Analytics {@code notification_open} event for a notification tap that did not create
562+ * the Activity.
563+ *
564+ * <p>The Messaging SDK only logs this event from {@code FcmLifecycleCallbacks#onActivityCreated},
565+ * so it is missed whenever the app was merely backgrounded (for example with the Home button) and
566+ * the Activity is reused. See firebase/flutterfire#17072 and firebase/firebase-android-sdk#3799.
567+ *
568+ * <p>This mirrors the SDK's own implementation, including de-duplication by message id, so a
569+ * message is never counted twice.
570+ */
571+ private void logNotificationOpen (@ NonNull Intent intent ) {
572+ Bundle analyticsData ;
573+ try {
574+ Bundle extras = intent .getExtras ();
575+ if (extras == null ) {
576+ return ;
577+ }
578+
579+ if (!markNotificationOpenAsLogged (getMessageId (extras ))) {
580+ return ;
581+ }
582+
583+ analyticsData = extras .getBundle (Constants .MessageNotificationKeys .ANALYTICS_DATA );
584+ } catch (RuntimeException e ) {
585+ // The intent can come from anywhere and may be malformed, so never crash the host app while
586+ // reading analytics data out of it.
587+ Log .w (TAG , "Failed to get analytics data from notification intent extras." , e );
588+ return ;
589+ }
590+
591+ if (MessagingAnalytics .shouldUploadScionMetrics (analyticsData )) {
592+ MessagingAnalytics .logNotificationOpen (analyticsData );
593+ }
594+ }
595+
596+ /**
597+ * Records {@code messageId} as having had its {@code notification_open} event logged, and returns
598+ * whether the caller should log it.
599+ *
600+ * <p>Returns {@code false} when this message id was logged recently, so that a message tapped
601+ * more than once (or delivered through both the create and the new-intent path) is only counted
602+ * once. A {@code null} id cannot be de-duplicated and is always logged, matching the SDK.
603+ */
604+ @ VisibleForTesting
605+ boolean markNotificationOpenAsLogged (@ Nullable String messageId ) {
606+ if (messageId == null ) {
607+ return true ;
608+ }
609+ if (recentlyLoggedMessageIds .contains (messageId )) {
610+ return false ;
611+ }
612+ if (recentlyLoggedMessageIds .size () >= RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE ) {
613+ recentlyLoggedMessageIds .poll ();
614+ }
615+ recentlyLoggedMessageIds .add (messageId );
616+ return true ;
617+ }
618+
619+ /** Remote Message ID can be either one of the following... */
620+ @ Nullable
621+ private static String getMessageId (@ NonNull Bundle extras ) {
622+ String messageId = extras .getString ("google.message_id" );
623+ if (messageId == null ) messageId = extras .getString ("message_id" );
624+ return messageId ;
625+ }
626+
627+ private boolean handleNotificationIntent (@ NonNull Intent intent ) {
536628 if (intent .getExtras () == null ) {
537629 return false ;
538630 }
539631
540- // Remote Message ID can be either one of the following...
541- String messageId = intent .getExtras ().getString ("google.message_id" );
542- if (messageId == null ) messageId = intent .getExtras ().getString ("message_id" );
632+ String messageId = getMessageId (intent .getExtras ());
543633 if (messageId == null ) {
544634 return false ;
545635 }
0 commit comments