fix(phone): Anruf-Erkennung im Hintergrund + bei gesperrtem Display
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) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
import com.facebook.react.bridge.ReactMethod
|
import com.facebook.react.bridge.ReactMethod
|
||||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lauscht auf Anruf-Statusaenderungen — wenn das Telefon klingelt oder ein
|
* 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 legacyListener: PhoneStateListener? = null
|
||||||
private var modernCallback: Any? = null // TelephonyCallback ab API 31
|
private var modernCallback: Any? = null // TelephonyCallback ab API 31
|
||||||
private var lastState: Int = TelephonyManager.CALL_STATE_IDLE
|
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
|
@ReactMethod
|
||||||
fun start(promise: Promise) {
|
fun start(promise: Promise) {
|
||||||
@@ -59,7 +65,7 @@ class PhoneCallModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
|
|||||||
handleStateChange(state)
|
handleStateChange(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tm.registerTelephonyCallback(reactApplicationContext.mainExecutor, cb)
|
tm.registerTelephonyCallback(callbackExecutor, cb)
|
||||||
modernCallback = cb
|
modernCallback = cb
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@@ -509,6 +509,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).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;
|
lastState = next;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,6 +43,42 @@ class PhoneCallService {
|
|||||||
/** Damit Resume nach VoIP-Loss nicht doppelt feuert wenn auch
|
/** Damit Resume nach VoIP-Loss nicht doppelt feuert wenn auch
|
||||||
* TelephonyManager-IDLE-Event kommt. */
|
* TelephonyManager-IDLE-Event kommt. */
|
||||||
private interruptedByFocus: boolean = false;
|
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<void> {
|
||||||
|
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<boolean> {
|
async start(): Promise<boolean> {
|
||||||
if (this.started || Platform.OS !== 'android') return false;
|
if (this.started || Platform.OS !== 'android') return false;
|
||||||
@@ -82,7 +118,10 @@ class PhoneCallService {
|
|||||||
'PhoneCallStateChanged',
|
'PhoneCallStateChanged',
|
||||||
(e: { state: PhoneState }) => this._onStateChanged(e.state),
|
(e: { state: PhoneState }) => this._onStateChanged(e.state),
|
||||||
);
|
);
|
||||||
|
this.telephonyAttached = true;
|
||||||
console.log('[PhoneCall] TelephonyManager-Listener aktiv');
|
console.log('[PhoneCall] TelephonyManager-Listener aktiv');
|
||||||
|
} else {
|
||||||
|
console.warn('[PhoneCall] PhoneCall.start() lieferte false — Native-Listener nicht aktiv');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('[PhoneCall] READ_PHONE_STATE abgelehnt — VoIP-Calls werden trotzdem ueber AudioFocus erkannt');
|
console.warn('[PhoneCall] READ_PHONE_STATE abgelehnt — VoIP-Calls werden trotzdem ueber AudioFocus erkannt');
|
||||||
@@ -108,6 +147,7 @@ class PhoneCallService {
|
|||||||
this.started = false;
|
this.started = false;
|
||||||
this.lastState = 'idle';
|
this.lastState = 'idle';
|
||||||
this.interruptedByFocus = false;
|
this.interruptedByFocus = false;
|
||||||
|
this.telephonyAttached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onStateChanged(state: PhoneState): void {
|
private _onStateChanged(state: PhoneState): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user