镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(in_app_messaging): re-add Android event listeners after pause and report unheard display messages - #18842

Draft
Lyokone wants to merge 2 commits into
mainfrom
fix/in-app-messaging-event-delivery
Draft

Lyokone wants to merge 2 commits into
mainfrom
fix/in-app-messaging-event-delivery

Conversation

@Lyokone

@Lyokone Lyokone commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes two event-delivery bugs in the lifecycle events (#18584) and custom display (#18586) that were added for 0.10.0.

1. Android: the lifecycle listeners were removed after the first activity pause. firebase-inappmessaging-display's FirebaseInAppMessagingDisplay.onActivityPaused calls FirebaseInAppMessaging.removeAllListeners() on every pause, which clears all four listener maps in DeveloperListenerManager (checked in the 23.0.0 AARs from BoM 35). The plugin added its listeners once, and Dart calls addEventListeners only once, so after the app was backgrounded, or a campaign action opened a URL or another activity, onMessageClicked, onMessageImpression, onMessageDismissed and onMessageDisplayError stayed silent for the rest of the process.

The plugin now tracks whether Dart wants the listeners separately from whether they are registered (EventListenerRegistration), and adds them again in onActivityResumed. The activity lifecycle callbacks are now registered when either the listeners or custom display are in use (they were only registered for custom display). addEventListeners now runs on the main thread, where the SDK removes the listeners; the SDK keeps them in plain HashMaps.

Reproduced on an Android emulator (Pixel 9a) with a test-only harness in the example app (not committed): it reads the SDK's registered listeners by reflection, delivers an impression through DeveloperListenerManager.impressionDetected, and starts an activity that finishes itself so MainActivity pauses and resumes.

before after
listeners registered after onMessageImpression.listen 4 4
listeners registered after one pause / resume 0 4
impression emitted on onMessageImpression after the pause no yes (3 of 3 runs)

Events the SDK dispatches while no activity is resumed (between one activity pausing and the next resuming) are still lost, since the SDK has no listeners then.

2. Messages sent to onMessageDisplay were dropped when nothing listened. After setCustomDisplayEnabled(true), native sends each campaign to a broadcast StreamController, which drops events that have no listener. Nothing then reported an impression, click, dismiss or error, and native kept waiting:

  • iOS: FIRIAMDisplayExecutor sets isMsgBeingDisplayed = YES when it hands a message to the display component, and resets it only in the click / dismiss / display error callbacks. Until then it shows no other campaign (I-IAM400002 / I-IAM400008).
  • Android has no such lock. The entry in pendingDisplays was never removed.

A message that arrives while nothing listens is now held and delivered to the first listener. If no listener subscribes within 5 seconds, or another campaign replaces it:

  • iOS reports it as a display error, which resets isMsgBeingDisplayed and records no impression, so native moves on. A dismissal would record one (recordValidImpression). onMessageDisplayError fires for it. For test messages, iOS shows its "fail to load a test message" alert with the reason.
  • Android only drops it in Dart. There DisplayCallbacksImpl.displayErrorEncountered (23.0.0) runs logToImpressionStore() then updateWasImpressed(): it stores a CampaignImpression, so the campaign is filtered out as seen and reported to the server, and for on_foreground campaigns it increments APP_FOREGROUND_ONE_PER_DAY_LIMITER_KEY, which would block every app open campaign for 24 h. Without a report, the campaign stays eligible and triggers again, as before this PR.

A held message replaced by the same campaign is not reported, because native keeps one entry per campaign. A held message is dropped when custom display is disabled, since native dismisses it then. The docs now say to listen to onMessageDisplay before enabling custom display, and which of this is iOS only.

3. Android: disabling custom display when it was already off rebinds the default display. With the lifecycle callbacks now registered for listener-only apps, a redundant setCustomDisplayEnabled(false), or onDetachedFromEngine, would pause and resume firebase-inappmessaging-display on the live activity: a flicker, restarted timers for a default message on screen, and removeAllListeners() dropping other engines' listeners. It now returns early when custom display is already off.

Tests

  • Dart (firebase_in_app_messaging_platform_interface, method_channel_firebase_in_app_messaging_test.dart), run for Android and iOS with debugDefaultTargetPlatformOverride: a message sent before the first listener is delivered to it; one replaced by the same campaign is not reported; disabling custom display drops it. On iOS, one nobody listens to, or one another campaign replaces, is reported as a display error and not dismissed. On Android neither is reported (both Android tests fail if Android reports).
  • Android JUnit:
    • FirebaseInAppMessagingPluginTest drives the plugin's real lifecycle wiring against a fake Application and a fake SDK (an internal InAppMessagingSdk seam over FirebaseInAppMessaging and the default display): addEventListeners adds the listeners, they come back after the SDK removes them on pause, detaching stops that. listenersAreAddedAgainWhenAnActivityResumes fails when updateLifecycleCallbacks() is removed from addEventListeners, onRemovedBySdk() from onActivityPaused, or restore() from onActivityResumed. Two tests check a redundant disable and a detach leave the default display, and another engine's listeners, alone; both fail without the early return.
    • EventListenerRegistrationTest covers the state machine. No test needs unitTests.returnDefaultValues, so it is not set.

CI

firebase_in_app_messaging is not a dependency of the aggregate tests app, so android_unit_tests.yaml gets a step that runs :firebase_in_app_messaging:testDebugUnitTest from the plugin's example app, and its reports are uploaded on failure.

Notes

Related Issues

Follow-up to #18584 and #18586.

Checklist

  • 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.
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (melos run analyze) does not report any problems on my PR.
  • 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

  • No, this is not a breaking change.

… report unheard display messages

firebase-inappmessaging-display removes all listeners on every activity
pause, so the lifecycle streams went silent on Android after the app was
backgrounded or a campaign opened another activity. The listeners are now
added again when an activity resumes.

A campaign sent to onMessageDisplay while nothing listened was dropped and
never reported, which on iOS blocks every later campaign. It is now held
for the first listener and reported as a display error if none subscribes
within 5 seconds.
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

A message that arrives on `onMessageDisplay` while nothing listens is
still held for the first listener for a few seconds. When none arrives:

- iOS reports it as a display error, as before, so the SDK releases its
  display lock and can show the next campaign. `onMessageDisplayError`
  fires for it.
- Android now only drops it. `displayErrorEncountered` stores a campaign
  impression there, and for app open campaigns uses up the one a day app
  open limit, for a message nobody saw. Android has no display lock, so
  the campaign stays eligible and triggers again.

On Android, disabling custom display while it is already off, which
`onDetachedFromEngine` also does, no longer pauses and resumes the
default display on the current activity. That restarted a default
message on screen and dropped the listeners of other engines.

The plugin's SDK calls now go through an internal seam, so JUnit tests
drive its activity lifecycle wiring against a fake `Application`.

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

Development

Successfully merging this pull request may close these issues.

1 participant