Skip to content

fix(android): [SDK-4407] deliver notification events when firebase_messaging is present (#1138)#1152

Open
fadi-george wants to merge 20 commits into
mainfrom
fadi/sdk-4407
Open

fix(android): [SDK-4407] deliver notification events when firebase_messaging is present (#1138)#1152
fadi-george wants to merge 20 commits into
mainfrom
fadi/sdk-4407

Conversation

@fadi-george
Copy link
Copy Markdown
Collaborator

One Line Summary

Fix Android notification click & foreground events being silently dropped when firebase_messaging (FlutterFire) is present in the same app.

Motivation

GitHub #1138: since 5.4.0, users running onesignal_flutter alongside firebase_messaging report OneSignal.Notifications.addClickListener never firing on Android background/killed notification taps.

Root cause: OneSignalNotifications owns a process-global static MethodChannel, but registerWith runs once per Flutter engine. FlutterFire spins up a headless background FlutterEngine for onBackgroundMessage, and GeneratedPluginRegistrant re-registers OneSignal against it — rebinding the shared channel to the background isolate, which never ran main() and has no listeners. Native click / willDisplay callbacks were then routed to that dead isolate and dropped. An additional engine-swap case (back out of MainActivity, then tap a notification) replayed queued clicks into a channel whose Dart end wasn't ready yet.

Scope

  • Android only. Plugin lifecycle / channel binding.
  • No public API change. Dart and iOS untouched.
  • Adds a firebase_messaging coexistence example (examples/demo_fm/).

Changes

  • OneSignalNotifications.registerWith: bind the shared channel only on the first engine; ignore later engines (e.g. FlutterFire's background engine) so they can't clobber it.
  • OneSignalNotifications now tracks whether Dart requested a click listener and toggles the native-SDK subscription across engine/activity lifecycles, so clicks delivered while the channel is detached get queued by the native SDK instead of dispatched into a dead JNI.
  • onAttachedToActivity(messenger): authoritatively rebind the channel to the activity-hosting engine's messenger. On a new engine, defer re-adding the native click listener and let Dart's registerClickListener drain the queue once the isolate is ready; on the same engine, re-add immediately.
  • OneSignalPlugin: wire the ActivityAware lifecycle (attach/detach + config-change variants) into the hooks above.
  • examples/demo_fm/: new example mirroring the affected setup (OneSignal + firebase_core + firebase_messaging), with a README documenting reproduction and direct-FCM testing.

Testing

Manual, on a real device + emulator, OneSignal push tapped in each app state:

State Before After
Foreground click fired click fired
Background tap dropped Notification clicked fires
Killed tap inconsistent Notification clicked fires
Back-out then tap (engine swap) dropped Notification clicked fires

Foreground willDisplay listener also confirmed firing with firebase_messaging present. Verified with runtime instrumentation (since removed) that the channel is bound to the UI isolate's messenger and that queued clicks drain after Dart re-registers.

Affected code checklist

  • Notifications
  • Outcomes
  • In App Messaging
  • Push Notifications
  • Examples

Checklist

  • Code compiles (flutter build apk --release)
  • Manually tested the reported scenarios
  • No public API changes

Made with Cursor

fadi-george and others added 8 commits May 28, 2026 13:24
…stence

demo_fm is a copy of the demo app with firebase_core + firebase_messaging
added and FCM listeners registered before OneSignal, matching the affected
users' setup in issue #1138. Initializing Firebase registers a
FirebaseMessagingService in the manifest, which is what triggers the FCM
hijack + click-listener routing problems the fix addresses.

Also gitignores google-services.json / GoogleService-Info.plist so per-dev
Firebase config is not committed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ssaging is present (#1138)

Two coexistence bugs caused OneSignal notification click / foreground
events to silently vanish on Android when an app also uses
firebase_messaging:

Bug A — FCM hijack. OneSignal's FCMBroadcastReceiver intent filter
requires a <category> matching the app package, a GCM-era convention
modern FCM no longer sets. When FlutterFire registers its own
FirebaseMessagingService, OneSignal's receiver never matches and pushes
are dropped before OneSignal sees them. Fixed by re-declaring the
receiver from the plugin manifest with tools:node="replace" and no
<category>, so OneSignal's existing receiver matches the modern FCM
broadcast.

Bug B — channel clobber. OneSignalNotifications is a process-global
singleton, but registerWith runs once per Flutter engine. FlutterFire
spins up a headless background FlutterEngine and GeneratedPluginRegistrant
re-registers us against it, rebinding the shared MethodChannel to the
background isolate (which never ran main() and has no listeners). Native
onClick/onWillDisplay callbacks were dispatched there and lost. Fixed by:
  - registerWith no longer rebinds an already-bound channel (ignores the
    extra background engine), and
  - onAttachedToActivity authoritatively (re)binds the channel to the
    engine that hosts the UI, so events always reach the isolate that
    registered the user's listeners (also covers killed-start where the
    background engine attaches first).

Verified on an emulator with firebase_messaging present: background-tap
notifications now fire addClickListener, and foreground pushes reach the
willDisplay listener. Repro app: examples/demo_fm.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george fadi-george requested a review from a team as a code owner May 28, 2026 22:16
Comment thread android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java
…detach

Address PR review:
- Apply the same idempotent registerWith guard + onAttachedToActivity rebind
  to OneSignalUser, OneSignalPushSubscription, and OneSignalInAppMessages, so
  a FlutterFire background engine can't clobber their channels and drop
  onUserStateChange / onPushSubscriptionChange / IAM lifecycle callbacks.
- Gate OneSignalNotifications.onDetachedFromEngine on the detaching engine's
  messenger so a background engine teardown no longer removes the click
  listener bound to the live UI engine.
- Tighten the #1138 lifecycle comments.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george
Copy link
Copy Markdown
Collaborator Author

@claude review

Comment thread android/src/main/java/com/onesignal/flutter/OneSignalInAppMessages.java Outdated
fadi-george and others added 2 commits May 28, 2026 16:34
Address PR review nit: extract the repeated registerWith guard and
onAttachedToActivity rebind logic into bindChannelIfUnbound and
rebindChannelToEngine on FlutterMessengerResponder, used by all four
singletons.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread .gitignore
Comment thread examples/demo_fm/lib/viewmodels/app_viewmodel.dart
fadi-george and others added 2 commits May 28, 2026 17:51
Add tools/send_fcm.sh to send non-OneSignal pushes via the FCM HTTP v1
API (notif/data modes) for exercising the FlutterFire path, document it
in the README, and gitignore the service-account key.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread android/src/main/java/com/onesignal/flutter/FlutterMessengerResponder.java Outdated
fadi-george and others added 2 commits May 28, 2026 19:51
Add an iOS setup/testing section (GoogleService-Info.plist, APNs auth key,
simulator caveats, log stream workflow for killed-state logs) and document
the new send_fcm.sh 'both' mode plus per-platform mode reliability.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george
Copy link
Copy Markdown
Collaborator Author

@claude review

bindChannelIfUnbound early-returned on later engines, so a FlutterFire
background isolate's messenger never got setMethodCallHandler, breaking
Dart->Native calls (MissingPluginException) from an FCM background handler.
Always register the incoming handler per engine while keeping the outgoing
channel pinned to the first engine (preserves the #1138 Native->Dart fix).

Also fix the .gitignore comment to reference examples/demo_fm.

Co-authored-by: Cursor <cursoragent@cursor.com>
onAttachedToActivity returned early on the new-engine branch without clearing
the process-global clickListenerRequested flag, leaving the prior engine's
state. A config change on the new engine before Dart re-registered would then
re-add the native click listener and drain queued clicks into an isolate with
empty _clickListeners, silently dropping them. Reset the flag so it tracks the
current engine's Dart state.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george
Copy link
Copy Markdown
Collaborator Author

@claude review

Comment thread lib/src/notifications.dart
…ndow

Only buffer clicks before the app's first click listener registers, and
cap the buffer size. Prevents unbounded growth when a listener is added
then removed without re-registering, since removeClickListener doesn't
stop native dispatch and _clickHandlerRegistered never resets.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george
Copy link
Copy Markdown
Collaborator Author

Would like to wait on OneSignal/OneSignal-Android-SDK#2655

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.

2 participants