Answer a call without opening the app first

A high-priority notification is a banner, not a ringing phone. On a locked
device the old one showed a line of text, and the only way to act was to unlock,
find the app and open it — by which time the caller has given up. A call is a
live event with seconds on the clock.

Now a full-screen intent with `CallStyle`, so the lock screen shows Answer and
Decline. ⚠️ `CATEGORY_CALL` and `CallStyle` are not decoration: Android 14
grants USE_FULL_SCREEN_INTENT at install only to apps that ring like this, and
without them the permission is refused and the call silently degrades to the
banner it used to be.

⚠️ Decline is a broadcast, not an activity. The point of answering from a lock
screen is that the phone stays locked for the choice the user did NOT make.

🔴 **The notification is `ongoing`**, so it cannot be swiped away by accident
mid-ring — which also means it never dismisses itself, and every path that ends
a call has to cancel it. Four of them: declined, answered, the caller hung up,
and nobody picked up. The last has no event behind it, so the service checks for
a parked offer past the dial timeout; without it the lock screen keeps offering
to answer a call that is long dead.

🔴 `isStale()` asks without consuming. `take()` is the handover and can only
happen once, so a staleness check built on it would swallow a live offer and
drop a call that was still ringing — the exact opposite of the bug it exists to
prevent. There is a test for that specifically.

Answering hands off through the SAME parked offer the app already uses on
launch. This session consumed the envelope, so the SDP in hand is the only copy
— a second answer path would be a second way to lose a call.

engine 168, client 96, app 46.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-29 13:05:51 -04:00
co-authored by Claude Opus 4.8
parent dc1be35816
commit b8ffe2ea71
6 changed files with 237 additions and 8 deletions
+23
View File
@@ -22,6 +22,20 @@
<!-- ⚠️ 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" />
<!--
🔴 Answering a call without opening the app first.
A high-priority notification is a banner; it does NOT wake a locked phone
into something you can answer. A full-screen intent does, which is why a
ringing call needs one and an ordinary message does not.
⚠️ Android 14 restricts this to calling and alarm apps and grants it at
install only to those — the notification must be CATEGORY_CALL and use
CallStyle, which is exactly what MessengerService builds. Declaring it
without that would be denied, silently, and the call would go back to
being a banner nobody can answer from the lock screen.
-->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!--
Voice notes. Requested at runtime as well: on this minSdk a dangerous
@@ -80,5 +94,14 @@
android:name=".app.MessengerService"
android:exported="false"
android:foregroundServiceType="remoteMessaging" />
<!--
Decline, from the notification, without opening the app. The whole
point of answering from a lock screen is that the phone stays locked
for the choice the user did NOT make.
-->
<receiver
android:name=".CallActionReceiver"
android:exported="false" />
</application>
</manifest>