fix: [SDK-4746] register Android click listener lazily to replay cold-start clicks#881
Conversation
|
@claude review once |
There was a problem hiding this comment.
LGTM — small, well-scoped fix that mirrors the iOS lazy-registration pattern to unblock cold-start click replay.
Extended reasoning...
Overview
Single-file change in com.onesignal.unity.android/Runtime/AndroidNotificationsManager.cs. The native Android addClickListener call is moved out of Initialize() and into the Clicked event's add accessor, guarded by a _clickNativeListenerSet flag so registration happens exactly once. The event field is backed by a private _clicked delegate, and the internal click handler dispatches through it. ForegroundWillDisplay and PermissionChanged are untouched.
Security risks
None. This is a JNI registration ordering change — no auth, crypto, permissions, network, or data-handling surface is touched. Public API shape is unchanged.
Level of scrutiny
Low. The fix is a mechanical reordering that adopts a pattern already proven on the iOS side, with a clear explanation in both the code comment and the PR description. Cold-start click replay is a documented native-SDK behavior, so the reasoning is verifiable. No new abstractions, no changes to threading semantics beyond what the existing event dispatch already assumes.
Other factors
- The bug hunting system found no issues.
- Manual verification on an Android emulator is documented (cold-start tap now fires
ClickedwithAdditionalData; warm-start and foreground paths unchanged). - Non-thread-safe check on
_clickNativeListenerSetis acceptable here because Unity event subscription conventionally occurs on the main thread; this matches the surrounding code's assumptions. - If
Clickednever gets a subscriber, the native listener is never registered — this is intentional and matches iOS.
|
Had to do: |
|
Does seem to fix it though I do some small changes.
|
|
Addressed both points in e2db6d9:
The iOS bridge has the same pattern but I can address it in a follow-up PR. |
fadi-george
left a comment
There was a problem hiding this comment.
Nit: if addClickListener throws, _clicked += value has already succeeded even though the native registration did not. Retrying the same subscription could add the same handler twice.
Could wrap the native registration and roll back _clicked -= value before rethrowing, so failed subscriptions don’t leave partial managed state behind.
|
Applied the nit in f3d5d48 — the native registration is now wrapped in a try/catch that rolls back |
Description
One Line Summary
Defer registering the native Android notification click listener until the first C#
Clickedsubscriber, so cold-start notification clicks are replayed instead of silently dropped.Details
Motivation
Fixes #873. When an Android app is opened by tapping a push notification from a killed state (cold start), the click event — and its Additional Data — never reached the app.
The Android bridge registered the native click listener eagerly inside
OneSignal.Initialize(). The native Android SDK queues a cold-start click and replays it the moment a listener is added, but at that point no C#Clickedhandler was subscribed yet (apps subscribe afterInitialize(), as our samples do), so the bridge discarded the replayed event.This PR adopts the same lazy-registration pattern the iOS bridge already uses: the native
addClickListenercall now happens on the first C#Clickedsubscription, after the handler is attached, so the native SDK's queued cold-start click replays into a live handler.Scope
Android only, notification click events only.
ForegroundWillDisplayandPermissionChangedregistration is unchanged. No public API changes —OneSignal.Notifications.Clickedbehaves the same, except cold-start clicks now fire. iOS is untouched (it already worked this way).Testing
Unit testing
None added — the repo has no unit test infrastructure, and this change is a thin JNI-registration ordering change.
Manual testing
Ran a before-and-after comparison with the demo app (
examples/demo), built with Unity 6000.3.6f1 (IL2CPP/ARM64) and installed on a Pixel 9 emulator (API 37, Google Play image). "Before" =main, "After" = this branch. To simulate real apps that subscribe to SDK events later thanInitialize()(the stock demo subscribes ~100ms after, a window small enough that the replayed event usually sneaks through), a temporary 5-second delay was added beforeRegisterSdkListeners()in both builds.Reproduction steps
Simulate late subscription. In
examples/demo/Assets/Scripts/AppBootstrapper.cs, add a delay before listener registration inStart()(this mimics apps that subscribe toClickedin a later scene/frame, like the reporter's):Optionally, log the click payload in
OnNotificationClickedviaNewtonsoft.Json.JsonConvert.SerializeObject(e.Notification.AdditionalData)(note:JsonUtility.ToJsonwon't showAdditionalData— it can't serialize dictionaries).Build and install on a booted emulator (Google Play image required for FCM):
Subscribe the device. Launch the app, accept the notification permission prompt, and wait for the push subscription to appear. Grab the subscription ID from the app UI (Push ID) or from logcat (
adb logcat | grep -o 'OneSignal-Subscription-Id=\[[a-f0-9-]*\]').Kill the app process (kill only —
force-stopwould put the app in stopped state, which blocks FCM delivery):Send a push with Additional Data to the subscription from step 3:
Tap the notification from the shade (this cold-starts the app) and watch logcat:
main:addClickListenerlogs duringInitialize(), then the listener registration log 5s later — butNotification clickednever appears. The event is dropped.addClickListenerlogs right after the 5s delay (firstClicked +=), immediately followed byNotification clickedwith theadditionalDatapayload.Results
Before (bug reproduced) — the native listener registers during
Initialize(), the queued cold-start click replays into a null C# handler and is silently discarded:After (fixed) — the native listener registers on first
Clicked +=, and the queued cold-start click replays immediately with Additional Data intact:Regression checks on the fixed build (all passing):
ForegroundWillDisplayfired and the notification displayed.Clickedfired immediately withadditionalData: {"test_key":"foreground"}.Affected code checklist
Checklist
Overview
Testing
Final pass