Alert people when the app is not on screen

Two platforms, two mechanisms, because iOS leaves no choice.

Android gets MessengerService, a foreground service holding the long-poll
with nobody in the middle. It rebuilds its session from the vault rather
than sharing the ViewModel's — by the time it runs the Activity may be
gone, which is the situation it exists for. Notifications for messages,
and a high-priority one naming the caller for an incoming call, because an
unnamed ringing phone is not answerable.

⚠️ The persistent notification is not a bug. Android requires a visible
notice for a foreground service; removing it means the OS kills the
connection. That is the price of not routing wake-ups through Google.

⚠️ Exactly one poller at a time: the ViewModel polls while the app is on
screen, the service while it is not, handed over in onStart/onStop. Two
would race for the same envelopes and double-notify, so the pair has to
stay symmetrical.

iOS gets APNs, and that IS Apple on the delivery path — there is no
alternative, because iOS suspends a backgrounded process and provides
exactly one way to wake it. What Apple learns is metadata: that a device
was woken and when. The push is content-free by construction, since the
relay holds ciphertext it cannot read. ⚠️ Nothing may later put the
message into the push payload; anything that could would mean the relay
had read it.

Relay stores one token per DEVICE, not per user — messages are sealed per
device, so waking the wrong one wakes an app with nothing to decrypt.
Re-registering replaces rather than accumulates, because APNs reissues
tokens and stale ones wake devices that no longer exist. An empty token
means forget: a sign-out must reach this, or a handset goes on being woken
for an account it no longer holds. Android tokens are refused outright —
it receives directly and needs no row.

Not yet done: the APNs sender itself, which needs a .p8 auth key that only
a human with the developer account can mint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-26 20:54:25 -04:00
parent 66da4352b8
commit 30d4829ad0
4 changed files with 303 additions and 0 deletions
+14
View File
@@ -13,6 +13,15 @@
the app.
-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Receiving while the app is not on screen, with nobody in the middle.
⚠️ remoteMessaging is the correct Android 14 type for this: it is
"continue a user-initiated conversation", which is exactly what a
held poll is. dataSync would be throttled and is not what this does. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<!-- ⚠️ Runtime permission on API 33+, which is this app's minSdk. Without
it the service still runs and every alert is silently dropped. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--
Voice notes. Requested at runtime as well: on this minSdk a dangerous
@@ -51,5 +60,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MessengerService"
android:exported="false"
android:foregroundServiceType="remoteMessaging" />
</application>
</manifest>