Ignore ended Telecom callbacks and contain route listener failures

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent f031e510b8
commit 8a9e421ea7
1 file changed
+35 -15
@@ -17,9 +17,8 @@ import java.util.concurrent.CopyOnWriteArrayList
* Self-managed Telecom Connection для P2P-звонка uTun.
*
* Сам звонок (сигналинг + аудио) полностью ведёт приложение; Connection лишь
* регистрирует вызов в Telecom, чтобы система считала его настоящим и корректно
* маршрутизировала аудио VOICE_COMMUNICATION на разговорный динамик (иначе на
* Android 14+ вызов без Connection не получает earpiece-маршрут).
* регистрирует вызов в Telecom для системного управления VoIP аудио. Фактический
* маршрут подтверждают callbacks CallEndpoint (API 34+) или CallAudioState; успешный запрос не подменяет подтверждение.
*
* Владелец — TelecomCallManager (создаётся в UtunConnectionService, хранится в
* реестре по callId). Ответ/отбой/разрыв приходят сюда из системного ввода
@@ -46,10 +45,11 @@ class UtunConnection(
private var requestedEndpoint: android.os.ParcelUuid? = null
private var requestSerial = 0L
private var requestedLegacyRoute = 0
private var ended = false
init {
setConnectionProperties(PROPERTY_SELF_MANAGED)
setConnectionCapabilities(CAPABILITY_SUPPORT_HOLD or CAPABILITY_MUTE)
setConnectionCapabilities(CAPABILITY_MUTE) // Hold не реализован приложением и не рекламируется Telecom.
setAudioModeIsVoip(true)
setAddress(Uri.fromParts("node", "0x%016x".format(peerNodeId), null), TelecomManager.PRESENTATION_UNKNOWN)
if (displayName.isNotEmpty()) setCallerDisplayName(displayName, TelecomManager.PRESENTATION_UNKNOWN)
@@ -58,20 +58,26 @@ class UtunConnection(
"created callId=0x%016x peer=0x%016x name=%s".format(callId, peerNodeId, displayName))
}
override fun onAnswer() = TelecomCallManager.notifyAnswer(callId)
override fun onAnswer() = systemInput("answer") { TelecomCallManager.notifyAnswer(callId) }
override fun onReject() = TelecomCallManager.notifyReject(callId)
override fun onReject() = systemInput("reject") { TelecomCallManager.notifyReject(callId) }
override fun onDisconnect() = TelecomCallManager.notifyDisconnect(callId)
override fun onDisconnect() = systemInput("disconnect") { TelecomCallManager.notifyDisconnect(callId) }
override fun onAbort() = TelecomCallManager.notifyAbort(callId)
override fun onShowIncomingCallUi() = TelecomCallManager.notifyIncomingUiAllowed(callId)
override fun onAbort() = systemInput("abort") { TelecomCallManager.notifyAbort(callId) }
override fun onShowIncomingCallUi() = systemInput("incoming UI") { TelecomCallManager.notifyIncomingUiAllowed(callId) }
private fun systemInput(event: String, action: () -> Unit) {
if (ended) { LogManager.addLog("WARN", "UtunConn", "late system input ignored id=$callId event=$event"); return }
action()
}
override fun onStateChanged(state: Int) {
LogManager.addLog("DEBUG", "UtunConn", "state -> %s".format(Connection.stateToString(state)))
}
override fun onCallAudioStateChanged(audioState: CallAudioState?) {
if (ended) return
LogManager.addLog("DEBUG", "UtunConn",
"audioState route=%s muted=%s".format(audioState?.route ?: -1, audioState?.isMuted ?: false))
if (Build.VERSION.SDK_INT < 34 && audioState != null) {
@@ -83,12 +89,13 @@ class UtunConnection(
}
if (route != null) confirmRoute(route)
else LogManager.addLog("WARN", "UtunConn", "unrecognized audio route=${audioState.route}")
onEndpointsChanged?.invoke()
notifyEndpointsChanged()
wantedRoute?.let { routeTo(it) }
}
}
override fun onCallEndpointChanged(endpoint: CallEndpoint) {
if (ended) return
val route = when (endpoint.endpointType) {
CallEndpoint.TYPE_EARPIECE -> AudioRoute.EARPIECE
CallEndpoint.TYPE_SPEAKER -> AudioRoute.SPEAKER
@@ -105,20 +112,29 @@ class UtunConnection(
wantedRoute = null; requestedLegacyRoute = 0
}
LogManager.addLog("INFO", "UtunConn", "route confirmed id=$callId route=$route pending=$wantedRoute")
onRouteChanged?.invoke(route)
try { onRouteChanged?.invoke(route) } catch (e: Exception) {
LogManager.addLog("ERROR", "UtunConn", "route confirmation callback failed id=$callId route=$route: $e")
}
}
override fun onAvailableCallEndpointsChanged(availableEndpoints: MutableList<CallEndpoint>) {
if (ended) return
val changed = this.availableEndpoints.map { it.identifier to it.endpointType }.toSet() !=
availableEndpoints.map { it.identifier to it.endpointType }.toSet()
this.availableEndpoints.clear()
this.availableEndpoints.addAll(availableEndpoints)
if (changed) LogManager.addLog("INFO", "UtunConn",
"endpoints id=$callId types=${availableEndpoints.map { it.endpointType }} headset=$headsetAvailable confirmed=$confirmedRoute")
onEndpointsChanged?.invoke()
notifyEndpointsChanged()
wantedRoute?.let { routeTo(it) }
}
private fun notifyEndpointsChanged() {
try { onEndpointsChanged?.invoke() } catch (e: Exception) {
LogManager.addLog("ERROR", "UtunConn", "endpoint list callback failed id=$callId: $e")
}
}
/** Перевести вызов в звонящее состояние (входящий). */
fun markRinging() = setRinging()
@@ -127,6 +143,7 @@ class UtunConnection(
/** Завершение с причиной (коды приложения: 0 — отбой, 2 — отклонено, 7 — ошибка). */
fun markDisconnected(reason: Int) {
ended = true; availableEndpoints.clear()
wantedRoute = null; requestedEndpoint = null; requestedLegacyRoute = 0; ++requestSerial
onRouteChanged = null; onEndpointsChanged = null
val code = when (reason) {
@@ -140,6 +157,7 @@ class UtunConnection(
/** Маршрутизация аудио через Telecom (CallEndpoint API на Android 14+, иначе setAudioRoute). */
fun routeTo(route: AudioRoute) {
if (ended) { LogManager.addLog("WARN", "UtunConn", "route after disconnect ignored id=$callId route=$route"); return }
wantedRoute = route
if (confirmedRoute == route && requestedEndpoint == null && requestedLegacyRoute == 0) { wantedRoute = null; return }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@@ -158,14 +176,16 @@ class UtunConnection(
val executor = ContextCompat.getMainExecutor(ChatApplication.instance)
requestCallEndpointChange(ep, executor, object : OutcomeReceiver<Void, CallEndpointException> {
override fun onResult(result: Void?) {
LogManager.addLog("INFO", "UtunConn", "endpoint change ok -> %d".format(ep.endpointType))
if (requestSerial != serial || ended) { LogManager.addLog("DEBUG", "UtunConn", "stale endpoint result id=$callId serial=$serial"); return }
LogManager.addLog("INFO", "UtunConn", "endpoint request completed id=$callId type=${ep.endpointType} confirmed=$confirmedRoute")
if (requestSerial == serial) {
requestedEndpoint = null
wantedRoute?.takeIf { it != route }?.let { routeTo(it) }
}
}
override fun onError(error: CallEndpointException) {
LogManager.addLog("WARN", "UtunConn", "endpoint change failed: %s".format(error.message))
if (requestSerial != serial || ended) { LogManager.addLog("DEBUG", "UtunConn", "stale endpoint error id=$callId serial=$serial"); return }
LogManager.addLog("WARN", "UtunConn", "endpoint request failed id=$callId type=${ep.endpointType}: $error")
if (requestSerial == serial) {
requestedEndpoint = null
val next = wantedRoute?.takeIf { it != route }
@@ -176,7 +196,7 @@ class UtunConnection(
})
return
} catch (e: Exception) {
requestedEndpoint = null; wantedRoute = null
if (requestSerial == serial) { requestedEndpoint = null; wantedRoute = null; ++requestSerial }
LogManager.addLog("WARN", "UtunConn", "requestCallEndpointChange threw: %s".format(e.message))
}
} else {