From 0044e222db2c319527297d72b413c9208cbac8df Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 16 May 2026 15:59:55 +0200 Subject: [PATCH] fix(phone): Anruf-Erkennung im Hintergrund + bei gesperrtem Display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: App bekommt im minimierten oder display-gesperrten Zustand nicht mit ob ein Anruf angefangen oder beendet wurde — TTS spricht weiter waehrend Telefon klingelt, oder bleibt stumm nach Auflegen. Zwei Ursachen: 1) Kotlin: TelephonyCallback war auf reactApplicationContext.mainExecutor registriert. Wenn die Activity pausiert ist (display aus, App im Hintergrund), wird der mainExecutor verzoegert oder gar nicht abgearbeitet — Call-State-Events kommen nicht durch. Fix: eigener Executors.newSingleThreadExecutor() — laeuft unabhaengig vom UI-Thread solange der App-Prozess lebt (Foreground-Service garantiert das). 2) TS: TelephonyManager-Listener kann nach laengerer Hintergrund-Zeit verloren gehen (React-Bridge-Context recreated nach Resume). Fix: neue refresh()-Methode in phoneCallService, AppState-Resume ruft sie auf — wenn telephonyAttached=false ist, wird der Native- Listener neu attached. Plus: Status-Property telephonyAttached macht in Logs sichtbar ob Pfad 1 (TelephonyManager) wirklich greift. Pfad 2 (AudioFocus fuer VoIP) war nie betroffen, der laeuft komplett im Native-Code. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/ariacockpit/PhoneCallModule.kt | 8 +++- android/src/screens/ChatScreen.tsx | 5 +++ android/src/services/phoneCall.ts | 40 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/android/android/app/src/main/java/com/ariacockpit/PhoneCallModule.kt b/android/android/app/src/main/java/com/ariacockpit/PhoneCallModule.kt index 08da982..0f0a5b7 100644 --- a/android/android/app/src/main/java/com/ariacockpit/PhoneCallModule.kt +++ b/android/android/app/src/main/java/com/ariacockpit/PhoneCallModule.kt @@ -15,6 +15,7 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.modules.core.DeviceEventManagerModule +import java.util.concurrent.Executors /** * Lauscht auf Anruf-Statusaenderungen — wenn das Telefon klingelt oder ein @@ -35,6 +36,11 @@ class PhoneCallModule(reactContext: ReactApplicationContext) : ReactContextBaseJ private var legacyListener: PhoneStateListener? = null private var modernCallback: Any? = null // TelephonyCallback ab API 31 private var lastState: Int = TelephonyManager.CALL_STATE_IDLE + // Eigener Single-Thread-Executor statt mainExecutor — der wird bei + // pausierter Activity verzoegert oder gar nicht abgearbeitet, der eigene + // Thread laeuft unabhaengig solange der App-Prozess lebt (was er ja tut, + // wir haben einen Foreground-Service der das garantiert). + private val callbackExecutor = Executors.newSingleThreadExecutor() @ReactMethod fun start(promise: Promise) { @@ -59,7 +65,7 @@ class PhoneCallModule(reactContext: ReactApplicationContext) : ReactContextBaseJ handleStateChange(state) } } - tm.registerTelephonyCallback(reactApplicationContext.mainExecutor, cb) + tm.registerTelephonyCallback(callbackExecutor, cb) modernCallback = cb } else { @Suppress("DEPRECATION") diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index bc4cd3d..a88639e 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -509,6 +509,11 @@ const ChatScreen: React.FC = () => { } }).catch(() => {}); } + // PhoneCall-Listener pruefen: kann passieren dass der nach laengerer + // Hintergrund-Zeit verloren geht (Bridge-Context recreated). Refresh + // versucht ihn neu zu attachen falls noetig — sonst kriegt die App + // bei display-aus / minimized keine Anruf-Events mit. + phoneCallService.refresh().catch(() => {}); } lastState = next; }); diff --git a/android/src/services/phoneCall.ts b/android/src/services/phoneCall.ts index 3ddcc42..626f84c 100644 --- a/android/src/services/phoneCall.ts +++ b/android/src/services/phoneCall.ts @@ -43,6 +43,42 @@ class PhoneCallService { /** Damit Resume nach VoIP-Loss nicht doppelt feuert wenn auch * TelephonyManager-IDLE-Event kommt. */ private interruptedByFocus: boolean = false; + /** True wenn der TelephonyManager-Listener (Pfad 1) wirklich registriert + * ist. False wenn READ_PHONE_STATE abgelehnt wurde oder Native nicht ging. */ + private telephonyAttached: boolean = false; + + /** Status fuer Diagnose: laeuft die Anruf-Erkennung tatsaechlich? */ + status(): { focusAttached: boolean; telephonyAttached: boolean } { + return { + focusAttached: this.focusSubscription !== null, + telephonyAttached: this.telephonyAttached, + }; + } + + /** Nach App-Resume: pruefen ob die Listener noch leben. Wenn der + * TelephonyManager-Listener verloren ging (kann passieren wenn der + * React-Bridge-Context recreated wurde), neu attachen. */ + async refresh(): Promise { + if (!this.started) return; + if (this.telephonyAttached) return; // alles ok + if (!PhoneCall) return; + try { + const ok = await PhoneCall.start(); + if (ok) { + if (!this.subscription) { + const emitter = new NativeEventEmitter(NativeModules.PhoneCall as any); + this.subscription = emitter.addListener( + 'PhoneCallStateChanged', + (e: { state: PhoneState }) => this._onStateChanged(e.state), + ); + } + this.telephonyAttached = true; + console.log('[PhoneCall] refresh: TelephonyManager-Listener re-attached'); + } + } catch (err: any) { + console.warn('[PhoneCall] refresh fehlgeschlagen:', err?.message || err); + } + } async start(): Promise { if (this.started || Platform.OS !== 'android') return false; @@ -82,7 +118,10 @@ class PhoneCallService { 'PhoneCallStateChanged', (e: { state: PhoneState }) => this._onStateChanged(e.state), ); + this.telephonyAttached = true; console.log('[PhoneCall] TelephonyManager-Listener aktiv'); + } else { + console.warn('[PhoneCall] PhoneCall.start() lieferte false — Native-Listener nicht aktiv'); } } else { console.warn('[PhoneCall] READ_PHONE_STATE abgelehnt — VoIP-Calls werden trotzdem ueber AudioFocus erkannt'); @@ -108,6 +147,7 @@ class PhoneCallService { this.started = false; this.lastState = 'idle'; this.interruptedByFocus = false; + this.telephonyAttached = false; } private _onStateChanged(state: PhoneState): void {