A call dropped as soon as the screen locked. The cause is not configuration:
Android freezes background processes, and a frozen WebView stops running the
JavaScript that holds the LiveKit connection up. The only thing the system
accepts as "this app must keep running" is a foreground service, which is why
the persistent notification comes with it.
Two mechanisms, both needed: the service stops the process from being frozen,
and a partial wake lock stops the CPU from sleeping. Either one alone still
drops the call.
The notification carries a hangup action. The service cannot end the call
itself — only the web app knows how to leave a LiveKit room — so it notifies
the plugin's listeners and the web side disconnects through the same path the
in-app button uses.
The web side starts the service on mount when a call is already in progress,
not just on transition: the WebView can reload mid-call, and waiting for the
next connect would leave that call unprotected with no visible symptom until
someone locked their screen.
POST_NOTIFICATIONS is requested at launch on Android 13+. Denying it does not
drop calls — the service still runs — it only hides the notification.
Tests cover connect/disconnect, the already-in-a-call mount, the hangup action,
and that the component is inert in the browser and desktop builds. Verified by
mutation that dropping the start call fails the first one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The web client already has a full mobile UI (MobileShell and 17 sibling
screens) and ships as a PWA, so what was missing to get an APK was a native
container, not an interface.
The WebView loads the instance over the network instead of bundling a copy of
the web build. Bundling would mean republishing an APK on every site deploy —
there is no electron-updater equivalent here — and it would move the page to
https://localhost, turning every same-origin /api and /ws call into a
cross-origin one. The cost is no offline mode, which for a realtime chat app
costs nothing: public/index.html covers the unreachable-server case.
packages/mobile sits outside the pnpm workspace on purpose. Capacitor's CLI and
Gradle both assume a flat node_modules, and keeping it out leaves pnpm-lock.yaml
and the existing web/desktop CI untouched by it.
Icons are generated from the existing brand art. The launch screen is a
layer-list over the brand background rather than Capacitor's per-density
splash.png set, which is light and flashed white before the WebView painted.
CI builds an *unsigned* APK. The signing key stays on the VPS and is applied
there — an APK signature is what proves an update came from us, so the key has
no reason to exist on a second machine.
Background voice (foreground service) is not in this commit: a plain WebView is
throttled when the screen goes off and the call drops. That is the next phase.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>