Skip to content

fix(live-notifications): recover the opt-in set in a cold process - #803

Open
mahmoud-elmorabea wants to merge 1 commit into
feature/live-notificationsfrom
live-notifications-cold-start-types
Open

fix(live-notifications): recover the opt-in set in a cold process#803
mahmoud-elmorabea wants to merge 1 commit into
feature/live-notificationsfrom
live-notifications-cold-start-types

Conversation

@mahmoud-elmorabea

@mahmoud-elmorabea mahmoud-elmorabea commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Live notifications delivered after ordinary process death were dropped, because the enabled-types gate could not distinguish an app that never opted in from a process that had not loaded the opt-in yet.

Why

Android starts a process solely to deliver an FCM push. No app code runs there — CustomerIOFirebaseMessagingService only calls SDKComponent.setupAndroidComponent(context) — so ModuleMessagingPushFCM is never registered and SDKComponent.pushModuleConfig falls back to MessagingPushModuleConfig.default(), whose type set is empty. The gate in LiveNotificationHandler read that as "not enabled" and returned.

A dropped end was worse than a missed render: the matching start was posted by an earlier live process as an ongoing, non-dismissible notification, so it stayed on screen with no way for the user to clear it.

What

  • LiveNotificationStore persists the app-wide opt-in set, replacing on each write and clearing when empty, so disabling a type does not leave a stale opt-in behind. The key is deliberately unprefixed to stay clear of the per-activity reclamation sweeps.
  • ModuleMessagingPushFCM.initialize() records the set on every initialization, empty included.
  • The gate prefers the live config and falls back to the persisted set.

A populated config always wins, so the opt-in guarantee still holds for apps that never enabled the feature, and a newly disabled type takes effect immediately rather than waiting for the persisted copy to be rewritten.

Custom types additionally need a render callback, which cannot be persisted; that half is tracked separately. Their end is already improved here, since the handler cancels a stranded notification when there is nothing to render.

Verification

  • messagingpush unit suite: 279 tests, 0 failures, including 9 new ones covering the cold-process gate both ways, end cancelling a stranded notification, config-wins-over-stale-persisted, replace/clear semantics, and survival of the reclamation sweeps.
  • ktlint clean; apiCheck passes with no .api changes (all touched code is internal).

Not device-tested.


Note

Medium Risk
Changes push delivery gating and SharedPreferences opt-in state; behavior is well-covered by new tests but affects notification lifecycle after process death.

Overview
Fixes live notifications being dropped when FCM wakes a cold process after the app was killed. In that case pushModuleConfig.liveNotificationTypes is empty, so the handler treated opted-in apps the same as apps that never enabled the feature—including end events, which could leave a non-dismissible ongoing notification stuck on screen.

Persistence: LiveNotificationStore now saves the app-wide enabled activity-type set (replace on write, clear when empty). ModuleMessagingPushFCM.initialize() writes that set on every init, including when empty, so disabling types does not leave a stale opt-in.

Gating: LiveNotificationHandler uses enabledActivityTypes()—live config when non-empty, otherwise the persisted set—so cold FCM delivery can still honor the opt-in while apps that never enabled live notifications stay ignored.

Reviewed by Cursor Bugbot for commit c73d8cc. Bugbot is set up for automated code reviews on this repo. Configure here.

Android starts a process solely to deliver an FCM push, and no app code runs
there: the FCM service wires up the context, so the push module is never
registered and its config reads back empty. The enabled-types gate then treated
that empty set as "this app never opted in" and dropped the notification, which
happened for every live notification delivered after ordinary process death. A
dropped `end` was worse than a no-op, stranding the ongoing notification a
previous process posted with no way for the user to clear it.

Persist the opt-in set at initialization and fall back to it at the gate, so a
fresh process can tell "never enabled" from "not loaded yet". A populated config
still wins, keeping the opt-in guarantee intact for apps that never enabled the
feature and letting a disabled type take effect immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Sample app builds 📱

Below you will find the list of the latest versions of the sample apps. It's recommended to always download the latest builds of the sample apps to accurately test the pull request.


@mahmoud-elmorabea
mahmoud-elmorabea marked this pull request as ready for review July 29, 2026 10:04
@mahmoud-elmorabea
mahmoud-elmorabea requested a review from a team as a code owner July 29, 2026 10:04
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.59%. Comparing base (fd69467) to head (c73d8cc).

Additional details and impacted files
@@                       Coverage Diff                        @@
##             feature/live-notifications     #803      +/-   ##
================================================================
+ Coverage                         68.40%   68.59%   +0.19%     
- Complexity                         1373     1388      +15     
================================================================
  Files                               229      229              
  Lines                              7542     7550       +8     
  Branches                           1127     1132       +5     
================================================================
+ Hits                               5159     5179      +20     
+ Misses                             1985     1976       -9     
+ Partials                            398      395       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
  • java_layout: live-notifications-cold-start-types (1785319459)

@github-actions

Copy link
Copy Markdown

Build available to test
Version: live-notifications-cold-start-types-SNAPSHOT
Repository: https://central.sonatype.com/repository/maven-snapshots/

@github-actions

Copy link
Copy Markdown
  • kotlin_compose: live-notifications-cold-start-types (1785319469)

@github-actions

Copy link
Copy Markdown

📏 SDK Binary Size Comparison Report

Module Last Recorded Size Current Size Change in Size
core 45.65 KB 45.65 KB ✅ No Change
datapipelines 42.68 KB 42.68 KB ✅ No Change
messagingpush 71.44 KB 71.57 KB ⬆️ +0.13KB
messaginginapp 123.88 KB 123.88 KB ✅ No Change
tracking-migration 22.89 KB 22.89 KB ✅ No Change
location 18.93 KB 18.93 KB ✅ No Change
geofence 78.55 KB 78.55 KB ✅ No Change

@Shahroz16 Shahroz16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I traced this through the killed-process FCM path, and the saved type set is replaced/cleared correctly, with the live config taking over once it exists. That fixes the practical drop for the bundled templates, including cold end delivery.

One known follow-up, not something I think should hold this PR: a wrapper cold process still does not have the runtime callback or branding config, so custom start/update cannot render and bundled templates use fallback styling. A custom end is improved here because it can now clear the stranded notification. This was already outside this minimal fix, so I think we can handle it separately after release.

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