feat: self-contained Android push via the Subscribe stream #87

Merged
nalum merged 2 commits from feat/self-contained-android-push into main 2026-08-16 12:40:58 +00:00
Owner

Closes #58. ADR-0030.

Why

Android push depended on two external things, both against system rule 3: a separate ntfy relay and a third-party UnifiedPush distributor app each member had to install. The main server already streams notifications (NotificationService.Subscribe), and the web client already consumes it — Android didn't.

What

  • Render live from the stream. InboxRepository.live() already held Subscribe open (to keep the inbox fresh); it now takes an onNotification callback. The signed-in shell passes one that raises each streamed Notification as a system notification via the existing Notifier, on its own per-kind channel, keyed on uid.
  • Drop UnifiedPush entirely — the org.unifiedpush.android:connector dependency and its pin, PushRegistrar.kt, AppPushService.kt, the manifest service, and the push_endpoint storage in SessionStore (incl. its sign-out/switch clearing).
  • Keep InboxPollWorker (15-min) as the closed-app fallback — its old guard (skip when a distributor endpoint exists) is gone, so it always runs when the app is closed.
  • Android loses the "send test" button and its ApiClient.push client: PushService.SendTest does Web Push to subscribed devices, which Android no longer is. Settings copy now states the real delivery model.

Transport

The existing Connect server-streaming RPC — stays proto-first (rule 4), typed Kotlin client already in gen/android, no hand-rolled reader. No Web Push encryption needed: the app holds its own TLS+JWT connection, no relay to protect. (Alternatives — embedding ntfy, a raw SSE endpoint — rejected in the ADR.)

Kept (out of scope)

internal/push, VAPID, push_subscription, the PushService RPCs — these still serve web Web Push, unchanged. Web is untouched. Per-kind preferences unchanged.

Known interim regression (accepted, see ADR)

A self-held connection lives only while the app process does. Until the follow-up foreground service (#59), closed-app push falls back to the 15-minute poll. For a family install with the connector being dropped anyway, this is acceptable.

Deliberate surface divergence (rule 1)

Android's delivery model (self-held stream) now differs from web's (Web Push); Android loses the test-send button while web keeps it. Recorded in the ADR.

Follow-up

The ntfy k8s deployment loses its only consumer but is outside this issue's Scope list; tearing it down (manifest, deploy-ntfy, CoreDNS rewrite) moves on its own.

Verification

make check green; :app:assembleDebug, app+data unit tests, and compileDebugKotlin all pass. Live rendering while the app is open needs on-device confirmation against the live deploy (the one thing CI/make check can't cover).

Stacking

Stacked for fast-forward on feat/phone-nav-collapse (#86). Merge after the chain: #82 → #79 → #80 → #81 → #84 → #85 → #86 → this.

🤖 Generated with Claude Code

Closes #58. ADR-0030. ## Why Android push depended on two external things, both against system rule 3: a separate **ntfy** relay and a third-party **UnifiedPush distributor app** each member had to install. The main server already streams notifications (`NotificationService.Subscribe`), and the web client already consumes it — Android didn't. ## What - **Render live from the stream.** `InboxRepository.live()` already held `Subscribe` open (to keep the inbox fresh); it now takes an `onNotification` callback. The signed-in shell passes one that raises each streamed `Notification` as a system notification via the existing `Notifier`, on its own per-kind channel, keyed on uid. - **Drop UnifiedPush entirely** — the `org.unifiedpush.android:connector` dependency and its pin, `PushRegistrar.kt`, `AppPushService.kt`, the manifest service, and the `push_endpoint` storage in `SessionStore` (incl. its sign-out/switch clearing). - **Keep `InboxPollWorker` (15-min) as the closed-app fallback** — its old guard (skip when a distributor endpoint exists) is gone, so it always runs when the app is closed. - **Android loses the "send test" button** and its `ApiClient.push` client: `PushService.SendTest` does Web Push to *subscribed* devices, which Android no longer is. Settings copy now states the real delivery model. ## Transport The existing Connect server-streaming RPC — stays proto-first (rule 4), typed Kotlin client already in `gen/android`, no hand-rolled reader. No Web Push encryption needed: the app holds its own TLS+JWT connection, no relay to protect. (Alternatives — embedding ntfy, a raw SSE endpoint — rejected in the ADR.) ## Kept (out of scope) `internal/push`, VAPID, `push_subscription`, the `PushService` RPCs — these still serve **web** Web Push, unchanged. Web is untouched. Per-kind preferences unchanged. ## Known interim regression (accepted, see ADR) A self-held connection lives only while the app process does. Until the follow-up foreground service (#59), **closed-app push falls back to the 15-minute poll**. For a family install with the connector being dropped anyway, this is acceptable. ## Deliberate surface divergence (rule 1) Android's delivery model (self-held stream) now differs from web's (Web Push); Android loses the test-send button while web keeps it. Recorded in the ADR. ## Follow-up The ntfy k8s deployment loses its only consumer but is outside this issue's Scope list; tearing it down (manifest, `deploy-ntfy`, CoreDNS rewrite) moves on its own. ## Verification `make check` green; `:app:assembleDebug`, app+data unit tests, and `compileDebugKotlin` all pass. **Live rendering while the app is open needs on-device confirmation against the live deploy** (the one thing CI/`make check` can't cover). ## Stacking Stacked for fast-forward on `feat/phone-nav-collapse` (#86). Merge after the chain: **#82 → #79 → #80 → #81 → #84 → #85 → #86 → this**. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(android): render live notifications, drop UnifiedPush
All checks were successful
check / commits (pull_request) Successful in 6s
check / web (pull_request) Successful in 1m29s
check / go (pull_request) Successful in 2m38s
check / report (pull_request) Successful in 3s
android / build (pull_request) Successful in 5m13s
android / report (pull_request) Successful in 2s
6a7e3f1770

Test report

Suite Tests Result Skipped
Unit 1330 ✅ pass 1
Integration 83 ✅ pass —

Coverage: 27.6%

Updated by the check workflow · commit 6a7e3f1770

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1330 | ✅ pass | 1 | | Integration | 83 | ✅ pass | — | **Coverage:** 27.6% <sub>Updated by the check workflow · commit 6a7e3f1770aa5eee261590e8e29f2c675cbf1e95</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 31 ✅ pass 0

Updated by the android workflow · commit 6a7e3f1770

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 31 | ✅ pass | 0 | <sub>Updated by the android workflow · commit 6a7e3f1770aa5eee261590e8e29f2c675cbf1e95</sub>
nalum force-pushed feat/self-contained-android-push from 6a7e3f1770
All checks were successful
check / commits (pull_request) Successful in 6s
check / web (pull_request) Successful in 1m29s
check / go (pull_request) Successful in 2m38s
check / report (pull_request) Successful in 3s
android / build (pull_request) Successful in 5m13s
android / report (pull_request) Successful in 2s
to 4435e6575b
Some checks failed
android / build (push) Has been cancelled
android / report (push) Has been cancelled
check / commits (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
check / commits (pull_request) Has been cancelled
check / go (pull_request) Has been cancelled
check / report (pull_request) Has been cancelled
check / web (pull_request) Has been cancelled
android / build (pull_request) Has been cancelled
android / report (pull_request) Has been cancelled
2026-08-16 12:40:39 +00:00
Compare
nalum changed target branch from feat/phone-nav-collapse to main 2026-08-16 12:40:54 +00:00
nalum merged commit 4435e6575b into main 2026-08-16 12:40:58 +00:00
nalum deleted branch feat/self-contained-android-push 2026-08-16 12:40:58 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!87
No description provided.