Files
ARIA-AGENT/android/android/app/src/main/java/com/ariacockpit/PhoneCallModule.kt
T
duffyduck 0044e222db 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>
2026-05-16 15:59:55 +02:00

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) {}
}