Linear: https://linear.app/engineereddev/issue/ENG-259
What's in this PR (54 files, all under apps/android/):
service/:ConnectionService, aremoteMessagingforeground service with a silentconnectionchannel ("Connected to " / "Reconnecting…" / "Offline", tap opens the app),START_STICKY. Started after pairing, on launch when paired and byBootReceiver(BOOT_COMPLETED,MY_PACKAGE_REPLACED); stopped only on sign-out or revoke.net/ws/:WsClient(OkHttp,pingInterval0): hello withsince=lastPushId, pong, a2 × WS_PING_SECONDS + 15s watchdog, full-jitter backoff 1/2/5/10/30/60 s reset after 60 s, an immediate reconnect from theNetworkCallback, 4401 → sign-out flow, 1012 → retry after 1–3 s;StateFlow<ConnectionState>.data/db/: Room v1 (pusheswith envelope JSON, file id/size and anotifiedflag;devices;sync_state), with the exported schema. Plaintext is never stored.sync/:SyncEngineapplies frames, pagesGET /api/pushesonresyncuntil it overlaps Room, advanceslastPushIdper push, and hands each eligible push toPushSinkonce (a no-op for now).- Settings shows the live connection state.
- Roborazzi: 6 new baselines
connection-{connected,reconnecting,offline}-{light,dark}and 6 re-recorded on purposemain-shell-{settings,settings-busy,sign-out-error}-{light,dark}. The commit says so.
Verify (lead, rebased on e5f90f2):
mise run check:android→ exit 0 (Spotless, detekt, Lint clean, Roborazzi verify)./gradlew pixel6Api34DebugAndroidTest --rerun→ BUILD SUCCESSFUL, "Running tests on devices: pixel6Api34"./gradlew testDebugUnitTest --rerun→ 146 tests, 0 failures- Implementer's forced-Doze acceptance (local server, emulator): connected for all 10 one-minute samples with screen off and
deviceidle force-idle; afterunforce, Room held all 12 test pushes (10 sent during Doze), all marked notified. Package replacement restored the connection.
Deviations:
- No managed-device harness server exists yet (ENG-268), so connection, Doze and package replacement were checked with the issue's permitted local-server fallback.
- Android 14 raised the active foreground-service channel from MIN to LOW; the original importance stays MIN and sound stays off.
- Exactly-once
PushSinkhand-off is durable-claim-based: process death between claim and sink can drop one notification but never duplicates it. Proposed spec clarification: idempotent notification delivery.
🤖 Generated with Claude Code