Skip to content

[firebase_messaging][Android] Behavior of onMessageOpenedApp on terminated launch — question about docs vs. observed behavior #18490

Description

@YuuWoods

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

Messaging

Which platforms are affected?

Android

Description

I'm trying to understand whether the following behavior is intentional or a bug, since it appears to differ from what the official documentation describes. I'd appreciate any clarification.

Observed behavior (Android, terminated launch)

When the app is opened from a terminated state by tapping a notification, both of the following fire for the same message:

  1. FirebaseMessaging.onMessageOpenedApp stream — emits the message
  2. FirebaseMessaging.instance.getInitialMessage() — returns the same message

If an app follows the pattern shown in the official docs (registering an onMessageOpenedApp listener AND calling getInitialMessage()), the handler ends up being invoked twice for a single notification tap.

What the docs say

The Handling Interaction page assigns each app state to a single API:

  • TerminatedgetInitialMessage()
  • BackgroundonMessageOpenedApp
  • ForegroundonMessage

Reading this, I expected a terminated launch to surface the message through getInitialMessage() only, and onMessageOpenedApp to stay silent in that case.

What I noticed in the Android plugin source

Looking at FlutterFirebaseMessagingPlugin.java in v16.4.3, I noticed:

  • onAttachedToActivity (lines 101–111) manually invokes onNewIntent(mainActivity.getIntent()) at attach time if the launching intent has extras.
  • onNewIntent (lines 545–590) does two things: it stores the message so getInitialMessage() can return it later (initialMessage = remoteMessage;) and it invokes Messaging#onMessageOpenedApp on the method channel (channel.invokeMethod("Messaging#onMessageOpenedApp", message);).

That would explain why both APIs surface the same message on a terminated launch. I wanted to check whether this dual-surfacing is intentional (in which case maybe the docs could mention it), or whether the channel.invokeMethod("Messaging#onMessageOpenedApp", ...) in this specific attach-time path is unintended.

Not caused by double-registered listeners

I added logging around every .listen() call and confirmed that FirebaseMessaging.onMessageOpenedApp.listen(...) is registered exactly once per app launch. The duplication does not come from multiple subscriptions on the app side.

Local experiment (for reference)

I tried a local patch on the pub-cache copy of FlutterFirebaseMessagingPlugin.java that comments out the manual onNewIntent(mainActivity.getIntent()) call in onAttachedToActivity (line 108). With this patch:

  • getInitialMessage() still returns the correct message via the Intent-based fallback path (lines 272–340).
  • onMessageOpenedApp stream no longer fires on terminated launch.
  • Verified 3/3 times: exactly one handler invocation per notification tap, via getInitialMessage().

This makes me wonder whether the channel.invokeMethod("Messaging#onMessageOpenedApp", ...) on terminated launch might be unnecessary, since the Intent-based fallback inside getInitialMessage() already covers that case. Is that understanding correct?

Reproducing the issue

  1. Follow the official Handling Interaction sample:
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
    final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
    if (initialMessage != null) _handleMessage(initialMessage);
  2. Send a notification to an Android device.
  3. Force-stop the app (terminated state).
  4. Tap the notification to launch the app.
  5. _handleMessage is called twice for the single tap (once via the stream, once via getInitialMessage()).

Reproduced consistently (6 out of 6 attempts across firebase_messaging 16.2.2 and 16.4.3) on a physical device.

Firebase Core version

4.12.1

Flutter Version

3.41.9

Relevant Log Output

[SETUP_START] setupRemoteNotification called count=1
[LISTEN_BEFORE] before onMessage/onMessageOpenedApp listen count=1
[LISTEN_AFTER] after onMessage/onMessageOpenedApp listen count=1
[C_ROUTE] onMessageOpenedApp handler entered (via onMessageOpenedApp stream) push=<id>
  stackHead: onMessageOpenedApp handler | _rootRunUnary | _CustomZone.runUnary
[SETUP_AFTER_GET_INITIAL] getInitialMessage returned push=<id> isNull=false
[D_ROUTE] calling onMessageOpenedApp handler via getInitialMessage push=<id>
[D_ROUTE] onMessageOpenedApp handler entered (via getInitialMessage) push=<id>
  stackHead: onMessageOpenedApp handler | setupRemoteNotification (via getInitialMessage)

([C_ROUTE] and [D_ROUTE] are custom log markers I added to distinguish the two entry paths. The stack traces confirm that the two calls originate from different call sites: one from the stream listener, one from the explicit getInitialMessage() result. Same push id in both.)

Flutter dependencies

Not included by user preference.

Additional context and comments

Question

Is the current Android behavior (both onMessageOpenedApp stream and getInitialMessage() surfacing the same message on terminated launch) intended?

  • If yes, would it be possible to mention this in the docs so users can add message-id deduplication?
  • If not, would the fix be to skip the channel.invokeMethod("Messaging#onMessageOpenedApp", ...) in the attach-time onNewIntent path (i.e. equivalent to removing the manual onNewIntent(getIntent()) call in onAttachedToActivity)?

Thanks for your time.

Environment

  • firebase_messaging: 16.4.3
  • Device: Pixel 5a
  • Android version: 14

Metadata

Metadata

Assignees

Labels

blocked: customer-responseWaiting for customer response, e.g. more information was requested.platform: androidIssues / PRs which are specifically for Android.plugin: messagingtype: bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions