Skip to content

Harden realtime foreground service lifecycle - #112

Merged
in-jun merged 1 commit into
mainfrom
fix/syncforegroundservice
Jul 18, 2026
Merged

Harden realtime foreground service lifecycle#112
in-jun merged 1 commit into
mainfrom
fix/syncforegroundservice

Conversation

@in-jun

@in-jun in-jun commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Cleans up the realtime foreground service and periodic backstop worker around notifications, wake locks, and background start restrictions.

  • Skip the periodic worker's foreground promotion when the realtime service is already running. In REALTIME mode the service holds its own ongoing sync_status notification (1001), so the worker promoting itself posted a second, redundant "Syncing files" notification (1002) on every backstop pass; it now checks the realtime flag and only promotes in PERIODIC mode where the long-pass execution budget is actually needed.
  • Give the ongoing status notification a contentIntent. The persistent notification that REALTIME users always see in the shade was a dead tap target; it now deep-links into MainActivity, reusing the immutable PendingIntent pattern SyncAlertNotifier already uses.
  • Acquire a timeout-bounded PARTIAL_WAKE_LOCK around each realtime pass. A foreground service keeps the process and network alive but does not stop CPU suspend once the screen turns off, so an in-flight transfer could freeze mid-pass until the next wakeup; the wake lock (released in a finally) keeps the pass running. This also puts the already-declared WAKE_LOCK permission to use.
  • Guard startForegroundService() against ForegroundServiceStartNotAllowedException. apply() runs inside viewModelScope, which outlives the Activity, so a late awaitLoaded()/sync-lock wait could start the service from the background and crash on Android 12+; it now falls back to the WorkManager periodic backstop, matching the guard the boot path already has.

Fixes #103
Fixes #80
Fixes #79
Fixes #17

- Skip the periodic worker's foreground promotion when the realtime service is
  already running, so its backstop pass no longer posts a duplicate ongoing
  status notification
- Guard startForegroundService against ForegroundServiceStartNotAllowedException
  so a late viewModelScope apply() from the background falls back to the periodic
  backstop instead of crashing
- Give the ongoing status notification a contentIntent into MainActivity
- Hold a timeout-bounded partial wake lock around each realtime pass so an
  in-flight transfer does not stall when the screen turns off

@in-jun in-jun left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed against all four referenced issues and the surrounding lifecycle code. This is clean and well-scoped — each change maps to one issue and stays within app/sync.

#103 — duplicate notification. Gating the worker's setForeground() on !isRealtimeServiceEnabled() reads the same KEY_REALTIME flag apply() writes, so the redundant 1002 notification is suppressed in REALTIME while PERIODIC keeps its extended budget. Correct.

#80 — dead tap target. contentIntent on the shared status notification reuses the immutable PendingIntent pattern from SyncAlertNotifier; request code 1001 doesn't collide with the 2000+ alert codes, and the intents are equal anyway. Good that the worker's 1002 notification becomes tappable too.

#79 — wake lock. Timeout-bounded acquire after the network-gate early return, released in finally behind the isHeld guard so the timeout auto-release can't trigger an under-lock. Passes are serialized by the collect, and cancellation still runs the finally, so there's no leak on shutdown.

#17 — background FGS start. The guard swallows only ForegroundServiceStartNotAllowedException on API 31+ and rethrows everything else, with the SDK_INT check short-circuiting ahead of the is check (safe on older APIs). This also restores deletePair's forgetAncestors cleanup, which the uncaught throw used to skip.

One behavioral note, not blocking: in REALTIME mode where the service was refused a background start, the backstop worker now also skips promotion and runs a long pass under the default budget. That's an acceptable degradation given the engine is crash-safe and recordIfStoppedMidPass surfaces a persistently stopped pair.

LGTM.

@in-jun
in-jun merged commit 123399e into main Jul 18, 2026
1 check passed
@in-jun
in-jun deleted the fix/syncforegroundservice branch July 18, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment