From 8a9e421ea7babaacba099252ee37f7fe16c64498 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 02:16:29 +0100 Subject: [PATCH] Ignore ended Telecom callbacks and contain route listener failures --- .../java/com/utun/chat/data/UtunConnection.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnection.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnection.kt index 3602d8ec..b7f6dea4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnection.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnection.kt @@ -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) { + 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 { 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 {