* docs: add plan for Android background sync via WorkManager Outlines the implementation plan for using WorkManager to periodically sync with SuperSync server in the background, cancelling stale reminders for tasks that were completed/deleted on other devices. https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * docs: refine plan based on multi-agent architecture review - Pin frontend hook to SyncProviderManager.currentProviderPrivateCfg$ - Add skipWhileApplyingRemoteOps() guard for selector-based effect - Add HRX (dismiss reminder) and deadlineRemindAt to parser - Add batch operation ds field handling - Add ProGuard rules step for release builds - Add error handling note for BootReceiver WorkManager enqueue https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * docs: improve plan after second review round Key changes: - Remove Step 5 (ReminderAlarmStore lookup) - hash-based cancellation is sufficient, no store lookup needed - Add BackgroundSyncProvider interface for extensibility to Dropbox/WebDAV - Per-account lastServerSeq keyed by baseUrl hash - Add SyncReminderScheduler helper to deduplicate scheduling logic - Document edge cases: account switching, token expiry, force-kill, gapDetected, notification race conditions - Add extensibility section explaining Dropbox feasibility and architecture https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * feat(android): add background sync to cancel stale reminders via WorkManager When a task is completed/deleted on desktop, Android reminders for that task would still fire because the mobile app didn't know about the change. This adds a WorkManager periodic job (every 15 min) that fetches operations from the SuperSync server and cancels AlarmManager alarms + notifications for tasks that are done, deleted, archived, or had their reminders dismissed. Architecture: - BackgroundSyncProvider interface for extensibility (Dropbox/WebDAV possible later) - SuperSyncBackgroundProvider: lightweight HTTP client + operation parser - SyncReminderWorker: CoroutineWorker that fetches ops and cancels reminders - SyncReminderScheduler: helper to schedule/cancel the WorkManager job - AndroidSyncBridgeEffects: mirrors SuperSync credentials to native SharedPreferences - Per-account lastServerSeq prevents account-switching bugs https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * chore: update package-lock.json after npm install https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * fix(android): address review issues and add tests for background sync Fixes from code review: - Fix distinctUntilChanged to properly handle provider switching (non-SuperSync emissions now treated as equal to prevent repeated clears) - Add null-safe ops array parsing in SuperSyncBackgroundProvider - Add write/call timeouts to OkHttpClient (30s write, 60s call) - Remove no-op `hash and hash` line in Kotlin hash function Tests: - Add cross-platform parity tests for notification ID hash function (pinned expected values that must match Kotlin implementation) - Add AndroidSyncBridgeEffects unit tests covering: - distinctUntilChanged comparator behavior - credential set/clear decision logic - observable filtering integration (dedup, null filtering) https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * fix(android): fix hash overflow and operation parser payload paths Two critical bugs found during final review: 1. Hash function abs(Int.MIN_VALUE) bug: In Kotlin, abs(Int.MIN_VALUE) returns Int.MIN_VALUE (still negative) due to 32-bit overflow. Fixed by converting to Long before abs(), matching JS behavior where Math.abs works on 64-bit floats. 2. Operation parser payload structure: The parser was looking at p.isDone and p.task.changes directly, but the actual compact operation format wraps everything in p.entityChanges[] and p.actionPayload. Rewrote to use p.entityChanges as primary source (consistent structure with entityType, entityId, changes fields) and p.actionPayload.task.changes as secondary source. https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * docs: add long-term plan for Android background sync improvements Covers three phases: fast app startup via cached sync seq, push-based notification cancellation via FCM, and extending background sync to Dropbox/WebDAV providers. https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe * fix(android): address review feedback for background reminder sync - Remove PLAN.md from repo root (C1) - Use EncryptedSharedPreferences for access token storage with fallback to standard SharedPreferences on broken KeyStore (C2) - Reset lastServerSeq when access token changes to prevent stale seq on account switch with same server URL (I1) - Add max iteration guard (100) to pagination loop to prevent infinite loops from server bugs (I2) - Use type predicate filter instead of non-null assertions (I3) - Add exponential backoff (5min) to WorkManager schedule (S1) - Add comments linking action type codes to frontend source (S2) - Extract distinctUntilChanged comparator to named function with JSDoc explaining the suppression semantics (S3) https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe --------- Co-authored-by: Claude <noreply@anthropic.com>
7.8 KiB
Android Background Sync Improvements
Status: Planned
Context
The current implementation (branch claude/fix-android-reminder-sync-TdwQY) uses Android WorkManager to poll the SuperSync server every ~15 minutes. When it detects that a task was completed, deleted, or had its reminder cleared on another device, it cancels the stale Android notification. This works but has limitations.
This document outlines two improvements:
- Fast app startup via cached sync state — use the background worker's progress to speed up the initial sync when the app opens
- Push-based notification cancellation via FCM — eliminate the 15-minute polling gap
Phase 1: Fast App Startup via Cached Sync State
Problem
When the user opens the app on Android, the sync layer starts from scratch — it doesn't know what the background worker has already seen. The worker has been tracking lastServerSeq in SharedPreferences, but that knowledge is wasted on app start.
Approach
Expose the worker's cached state to the TypeScript layer so the app can skip already-processed operations or use the seq as a sync hint.
Design
Option A: Seq Hint (Minimal)
- Add
getLastSyncSeq(): numbertoAndroidInterface - On app init,
SyncServicecallsandroidInterface.getLastSyncSeq()to get the worker's last-processed sequence number - The sync layer uses this as a starting point, only fetching operations newer than this seq
- Benefit: reduces initial sync payload from potentially thousands of ops to at most ~15 minutes' worth
Bridge addition:
// android-interface.ts
getLastSyncSeq?(): number;
// JavaScriptInterface.kt
@JavascriptInterface
fun getLastSyncSeq(): Long {
return credentialStore.getLastServerSeq()
}
Sync layer integration point: wherever the initial sinceSeq is determined in SyncService or OperationApplierService, check for the Android hint first.
Option B: Cached Operations (More Ambitious)
- The worker writes fetched operations to a local cache (SharedPreferences or a small SQLite table) instead of just tracking seq
- On app start, the TypeScript layer reads cached ops via the bridge, applies them immediately, then syncs live for anything newer
- Benefit: app state is updated almost instantly on open, even before a network call
Trade-offs:
- More storage and complexity on the native side
- Need to handle cache invalidation (e.g., if the user switches accounts)
- Operations could be large if the user was offline for days
Recommendation: Start with Option A. It's simple, low-risk, and already covers the common case (user opens app after a short break). Option B adds latency savings of one network round-trip, which matters less on modern connections.
Implementation Steps (Option A)
- Add
getLastSyncSeq()toAndroidInterfaceandJavaScriptInterface - In the sync initialization path, check
IS_ANDROID_WEB_VIEWand callgetLastSyncSeq() - If the returned seq is greater than 0, use it as the starting point for
sinceSeq - Fall back to the normal sync path if 0 or unavailable
Edge Cases
- Account switch:
lastServerSeqis keyed bybaseUrl.hashCode(), so switching accounts resets to 0 automatically - Worker never ran: Returns 0, sync proceeds normally — no regression
- Stale seq: If the seq is very old (worker was killed by OS), the app just fetches more ops than usual — still correct, just slower
Phase 2: Push-Based Cancellation via FCM
Problem
WorkManager's minimum periodic interval is 15 minutes. A user could complete a task on their desktop and still receive the reminder on their phone if it fires within that window.
Approach
Use Firebase Cloud Messaging (FCM) to push a lightweight signal from the SuperSync server when reminder-relevant operations occur. The Android app receives the push and immediately cancels the stale notification.
Prerequisites
- SuperSync server must support webhook/push triggers on new operations
- FCM project setup and device token registration
- Server-side logic to determine which operations are "reminder-relevant"
Design
Server Side
- Client registers its FCM token with the SuperSync server (new API endpoint)
- When the server receives operations matching reminder-relevant action codes (HRX, HX, HD, HCR, HU with reminder changes), it sends a data-only FCM message to registered tokens for that account
- The FCM payload is minimal:
{ "type": "reminder_change", "seq": 12345 }
Client Side
- A
FirebaseMessagingServicereceives the data message - It reads the current
lastServerSeqfrom SharedPreferences - If the incoming seq is newer, it fetches operations from
lastServerSeqto the new seq using the existingSuperSyncBackgroundProvider - Parses and cancels notifications using the existing logic in
SyncReminderWorker - Updates
lastServerSeq
Hybrid Approach
Keep the 15-minute WorkManager poll as a fallback. FCM delivery is best-effort — messages can be delayed or dropped by the OS (Doze mode, battery optimization). The worker ensures eventual consistency even if FCM fails.
FCM push (immediate, best-effort)
↓
Cancel notification
↓
WorkManager poll (15-min, guaranteed)
↓
Cancel any remaining stale notifications
Implementation Steps
- Add Firebase SDK to the Android project
- Create
SyncFirebaseMessagingServiceextendingFirebaseMessagingService - Add FCM token registration endpoint to SuperSync server
- Add server-side push logic for reminder-relevant operations
- Bridge FCM token to TypeScript layer so it can be sent during SuperSync auth
- Keep existing WorkManager poll as fallback
Considerations
- Privacy: FCM messages go through Google's servers. The payload should contain only the seq number, never task content.
- Battery: Data-only FCM messages are low-impact. Combined with the existing WorkManager poll, this adds negligible battery drain.
- Server cost: One push per reminder-relevant operation per registered device. For most users this is a handful per day.
- Multiple devices: Each device registers its own FCM token. The server pushes to all tokens for the account.
Phase 3: Extend to Other Sync Providers
Dropbox / WebDAV
The BackgroundSyncProvider interface already supports this. A Dropbox implementation would:
- Download
sync-data.json(~100KB+) via the Dropbox API - Diff against a locally cached copy to detect task completions/deletions
- Return the set of taskIds to cancel
This is heavier than SuperSync's operation-based API but workable for the ~15-minute poll interval. WebDAV would be similar.
Key difference: Dropbox/WebDAV providers would need to cache the previous state locally to compute diffs, adding storage overhead. SuperSync's seq-based pagination avoids this entirely.
Implementation would add:
DropboxBackgroundProviderimplementingBackgroundSyncProviderWebDavBackgroundProviderimplementingBackgroundSyncProvider- Credential bridging for Dropbox OAuth tokens and WebDAV credentials
- Provider selection logic in
SyncReminderWorkerbased on stored provider ID
Priority and Sequencing
| Phase | Effort | Impact | Recommendation |
|---|---|---|---|
| Phase 1 (seq hint) | Small (~1 day) | Medium — faster app start | Do first |
| Phase 2 (FCM push) | Large (~1 week, needs server changes) | High — instant cancellation | Do when server supports it |
| Phase 3 (other providers) | Medium per provider | Medium — broader coverage | Do on demand |