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>
108 lines
5.3 KiB
XML
108 lines
5.3 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
|
|
<!--
|
|
🔴 **WebRTC aborts the PROCESS without this.** Its NetworkMonitor calls
|
|
ConnectivityManager.getNetworkState() on its own network thread, and the
|
|
JNI layer asserts RTC_CHECK(!env->ExceptionCheck()) straight afterwards.
|
|
Missing this permission makes that call throw, and the check turns a Java
|
|
exception into abort() — SIGABRT inside libjingle_peerconnection_so, no
|
|
AndroidRuntime trace, no catchable failure. Placing a call simply killed
|
|
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" />
|
|
<!--
|
|
🔴 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
|
|
permission declared here is not granted here, and a recorder started
|
|
without it fails with a RuntimeException carrying no useful message.
|
|
-->
|
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
<!-- Video calls (§8.10). Camera is optional hardware: a phone without one
|
|
still installs, and a video call degrades to voice. -->
|
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
|
<!-- The app is usable without a microphone; it simply cannot record. -->
|
|
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
|
|
|
<!-- Voice calls: WebRTC switches the audio mode and routes to earpiece. -->
|
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
|
|
<application
|
|
android:allowBackup="false"
|
|
android:icon="@mipmap/ic_launcher"
|
|
android:roundIcon="@mipmap/ic_launcher_round"
|
|
android:label="@string/app_name"
|
|
android:supportsRtl="true"
|
|
android:theme="@style/Theme.FlamenetMessenger">
|
|
|
|
<!--
|
|
allowBackup is off deliberately, and it is load-bearing here rather
|
|
than tidy. The vault's key lives in the Android Keystore and is never
|
|
backed up; the encrypted records would be. Restoring those onto a
|
|
different device would produce a vault nothing can open, which
|
|
presents as a corrupt install rather than as a missing key.
|
|
-->
|
|
|
|
<!--
|
|
🔴 singleTask is load-bearing, not cosmetic. The default launch mode
|
|
stacks a NEW MainActivity — with a NEW ViewModel, session, poll loop
|
|
and CallManager — every time the app is entered through a
|
|
notification or a fresh launcher intent. Two instances means the
|
|
visible screen's Hang up commands a CallManager that has no call,
|
|
while the real call keeps ringing in the instance underneath: the
|
|
peer never receives the END, and "I ended the call but their phone
|
|
kept going" is exactly how it presents.
|
|
-->
|
|
<activity
|
|
android:name=".app.MainActivity"
|
|
android:exported="true"
|
|
android:launchMode="singleTask"
|
|
android:windowSoftInputMode="adjustResize">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
</intent-filter>
|
|
</activity>
|
|
|
|
<service
|
|
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>
|