0044e222db
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>
133 lines
5.4 KiB
Kotlin
133 lines
5.4 KiB
Kotlin
package com.ariacockpit
|
|
|
|
import android.Manifest
|
|
import android.content.Context
|
|
import android.content.pm.PackageManager
|
|
import android.os.Build
|
|
import android.telephony.PhoneStateListener
|
|
import android.telephony.TelephonyCallback
|
|
import android.telephony.TelephonyManager
|
|
import android.util.Log
|
|
import androidx.core.content.ContextCompat
|
|
import com.facebook.react.bridge.Arguments
|
|
import com.facebook.react.bridge.Promise
|
|
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
|
|
* Anruf laeuft, sendet das Modul ein "PhoneCallStateChanged"-Event an JS.
|
|
*
|
|
* JS-Side stoppt dann die TTS-Wiedergabe damit ARIA nicht mitten ins Gespraech
|
|
* weiterredet. Ohne READ_PHONE_STATE-Permission failt start() leise — der Rest
|
|
* der App funktioniert wie bisher.
|
|
*
|
|
* State-Strings: "idle" | "ringing" | "offhook"
|
|
*/
|
|
class PhoneCallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
override fun getName() = "PhoneCall"
|
|
|
|
companion object { private const val TAG = "PhoneCall" }
|
|
|
|
private var telephonyManager: TelephonyManager? = null
|
|
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) {
|
|
try {
|
|
val perm = ContextCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.READ_PHONE_STATE)
|
|
if (perm != PackageManager.PERMISSION_GRANTED) {
|
|
Log.w(TAG, "READ_PHONE_STATE Permission fehlt — Anruf-Erkennung inaktiv")
|
|
promise.resolve(false)
|
|
return
|
|
}
|
|
val tm = reactApplicationContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
|
if (tm == null) {
|
|
Log.w(TAG, "TelephonyManager nicht verfuegbar")
|
|
promise.resolve(false)
|
|
return
|
|
}
|
|
telephonyManager = tm
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
val cb = object : TelephonyCallback(), TelephonyCallback.CallStateListener {
|
|
override fun onCallStateChanged(state: Int) {
|
|
handleStateChange(state)
|
|
}
|
|
}
|
|
tm.registerTelephonyCallback(callbackExecutor, cb)
|
|
modernCallback = cb
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
val l = object : PhoneStateListener() {
|
|
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
|
|
handleStateChange(state)
|
|
}
|
|
}
|
|
@Suppress("DEPRECATION")
|
|
tm.listen(l, PhoneStateListener.LISTEN_CALL_STATE)
|
|
legacyListener = l
|
|
}
|
|
Log.i(TAG, "PhoneCall-Listener aktiv")
|
|
promise.resolve(true)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "start fehlgeschlagen", e)
|
|
promise.reject("START_FAILED", e.message ?: "Unbekannter Fehler", e)
|
|
}
|
|
}
|
|
|
|
@ReactMethod
|
|
fun stop(promise: Promise) {
|
|
try {
|
|
val tm = telephonyManager
|
|
if (tm != null) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
(modernCallback as? TelephonyCallback)?.let { tm.unregisterTelephonyCallback(it) }
|
|
modernCallback = null
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
legacyListener?.let { tm.listen(it, PhoneStateListener.LISTEN_NONE) }
|
|
legacyListener = null
|
|
}
|
|
}
|
|
telephonyManager = null
|
|
lastState = TelephonyManager.CALL_STATE_IDLE
|
|
promise.resolve(true)
|
|
} catch (e: Exception) {
|
|
promise.reject("STOP_FAILED", e.message ?: "")
|
|
}
|
|
}
|
|
|
|
private fun handleStateChange(state: Int) {
|
|
if (state == lastState) return
|
|
lastState = state
|
|
val name = when (state) {
|
|
TelephonyManager.CALL_STATE_RINGING -> "ringing"
|
|
TelephonyManager.CALL_STATE_OFFHOOK -> "offhook"
|
|
TelephonyManager.CALL_STATE_IDLE -> "idle"
|
|
else -> return
|
|
}
|
|
Log.i(TAG, "Telefon-State: $name")
|
|
val params = Arguments.createMap().apply { putString("state", name) }
|
|
try {
|
|
reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
.emit("PhoneCallStateChanged", params)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Event-emit fehlgeschlagen: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@ReactMethod fun addListener(eventName: String) {}
|
|
@ReactMethod fun removeListeners(count: Int) {}
|
|
}
|