Skip to content

feat(android): promote running sessions to an ongoing notification - #423

Open
RonenMars wants to merge 3 commits into
fix/cold-start-deep-linksfrom
feat/live-activity-android
Open

feat(android): promote running sessions to an ongoing notification#423
RonenMars wants to merge 3 commits into
fix/cold-start-deep-linksfrom
feat/live-activity-android

Conversation

@RonenMars

@RonenMars RonenMars commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Stacked on #422. Final PR of the five — Phase 1b (APNs + renewal) is out of scope.

Surfaces a running session on Android as an ongoing notification, resolved through Metro's platform extension so the call site in _layout stays platform-agnostic.
decideActions is reused unchanged, so the cap of three and least-recently-updated eviction behave identically to iOS.

This is not the promoted status-bar chip, and cannot be yet

The plan targeted Android 16's promoted ongoing notification via setRequestPromotedOngoing + setUsesChronometer.
expo-notifications exposes neither. It offers sticky, which maps to setOngoing, and that is the whole surface — there is no chip to request and no chronometer to drive.

So this lands as the plain ongoing notification the runbook names as the acceptable v1 floor.
Elapsed time is omitted rather than rendered: without a chronometer it could only be a snapshot that silently goes stale, and a wrong timer is worse than no timer.

Closing the gap needs those two builder calls exposed upstream or a small native module.
Recorded in docs/expo-web-support.md rather than papered over.

POST_PROMOTED_NOTIFICATIONS is deliberately not declared — nothing can request promotion, and an unused permission is just noise in the manifest.
POST_NOTIFICATIONS is added, since it was previously declared nowhere.

Implementation notes

Posting with an existing identifier replaces the notification in place, so start and update are the same call and an update cannot stack a duplicate.

Notifications are tagged liveSession: true in their data payload.
Adoption after a restart has to clear our own stale surfaces without dismissing push notifications, which also carry a sessionId — filtering on that alone would have cleared the wrong things.

The channel is created at IMPORTANCE_DEFAULT; anything lower forfeits promotion on API 36+ and can be collapsed out of the shade entirely.

The Platform.OS === 'ios' guards in the iOS module are dropped: with .android.ts and .web.ts siblings present, that file is only ever the iOS resolution and the checks were dead code.

Verification

6 new unit tests: payload shape, waiting_input vs running, in-place replacement, terminal dismissal, shared cap eviction, and that adoption spares push notifications.
npm run test:unit 930/930 green; lint 0 errors; typecheck unchanged from baseline.

Device verification on Android 13–15 and 16+ hardware is still pending, including the aggressive-battery OEM check.


CI is red on this branch, and it was red before this work

Type check, i18n, and Integration tests fail here. None of those failures come from this stack.

Measured on a clean worktree at integration-merge-354-355-376 (f2242bc4) with no changes applied:

  • Type check — 3 errors: makeSearchStyles in app/conversation/[id].tsx:67, plus two i18n key-signature errors in components/onboarding/steps/ConnectStep.tsx:196,197.
  • i18n — 3 failures in 1 suite.
  • Integration tests — 4 suites / 34 tests, all __tests__/integration/conversation-*.

CI on this branch reports the identical Test Suites: 4 failed, 36 passed / Tests: 34 failed, 211 passed, and tsc reports the same 3 errors and no others.
Independently confirmed by the repo owner against their own clean worktree.

These are deliberately left alone — fixing them is unrelated to Live Activities and belongs in its own PR.

Surfaces a running session on Android as an ongoing notification, resolved through Metro's platform extension so the call site in _layout stays platform-agnostic.
decideActions is reused unchanged, so the cap of three and the least-recently-updated eviction behave identically to iOS.

This is a plain ongoing notification, not the Android 16 promoted status-bar chip the runbook aimed at.
expo-notifications exposes `sticky`, which maps to setOngoing, but neither setRequestPromotedOngoing nor setUsesChronometer, so there is no chip to request and no chronometer to drive.
Elapsed time is therefore omitted rather than rendered: without a chronometer it could only be a snapshot that silently goes stale, and a wrong timer is worse than none.
Closing that gap needs those builder calls exposed upstream or a small native module — noted in docs/expo-web-support.md rather than papered over.

POST_PROMOTED_NOTIFICATIONS is deliberately not declared, since nothing can request promotion yet and an unused permission is just noise in the manifest.

Posting with an existing identifier replaces the notification in place, so start and update are the same call and an update cannot stack a duplicate.
Notifications are tagged `liveSession` in their data payload, because adoption after a restart has to clear our stale surfaces without dismissing push notifications, which also carry a sessionId.

The Platform.OS === 'ios' guards in the iOS module are dropped: with .android.ts and .web.ts siblings present, that file is only ever the iOS resolution and the checks were dead.
@RonenMars
RonenMars force-pushed the fix/cold-start-deep-links branch from 27a4db5 to b7e37b9 Compare July 25, 2026 21:43
@RonenMars
RonenMars force-pushed the feat/live-activity-android branch from 47e34ba to 7f7d726 Compare July 25, 2026 21:43
getRawLines() only dropped blank rows, so Claude Code's status-bar box
borders leaked into the raw terminal view as literal characters when
parse confidence fell to low.
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.

1 participant