From 326bfd124e046bba8e0e90aeeb7896dec3a6d292 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 23:06:10 +0100 Subject: [PATCH] Guard call audio ownership on main and avoid touching released devices --- .../com/utun/chat/data/CallAudioEngine.kt | 22 +++++++++++++++---- .../com/utun/chat/data/CallAudioOutput.kt | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt index 66238a2d..331218dd 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt @@ -134,6 +134,7 @@ class CallAudioEngine { val am = ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.mode = AudioManager.MODE_NORMAL if (Build.VERSION.SDK_INT >= 31) am.clearCommunicationDevice() + modeOwned = false } catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "release fallback routing failed id=$callId: ${e.message}") } @@ -157,8 +158,17 @@ class CallAudioEngine { fun isMuted(): Boolean = muted /** Main: остановить прежний запуск, подготовить маршрут и запустить независимые RX/TX. - * true — workers запущены; ошибки внутри них приходят через onFailure. false — синхронный отказ старта. */ + * true — workers запущены; ошибки внутри них приходят через onFailure. false — синхронный отказ старта. + * Ошибка очистки прежнего/частичного запуска бросается владельцу с сохранёнными ресурсами. id не равен нулю. */ fun start(id: Long): Boolean { + check(Looper.myLooper() == Looper.getMainLooper()) { + LogManager.addLog("ERROR", "CallAudio", "start outside main id=$id") + "Call audio start requires main" + } + require(id != 0L) { + LogManager.addLog("ERROR", "CallAudio", "start rejected: zero call id") + "Call id must be nonzero" + } stop() startElapsedMs = SystemClock.elapsedRealtime() @@ -287,7 +297,7 @@ class CallAudioEngine { "nativeMs=$nativeMs modeMs=$modeMs routeMs=$routeMs") return true } catch (e: Exception) { - LogManager.addLog("ERROR", "CallAudio", "start failed id=$id: ${e.message}") + LogManager.addLog("ERROR", "CallAudio", "start failed id=$id: $e") stop() return false } @@ -316,6 +326,10 @@ class CallAudioEngine { * Join ограничен 250 мс на worker; живые потоки и устройства остаются до следующей попытки. * При неполной очистке бросает ошибку владельцу; native останавливается лишь после устройств. */ fun stop() { + check(Looper.myLooper() == Looper.getMainLooper()) { + LogManager.addLog("ERROR", "CallAudio", "stop outside main id=$callId") + "Call audio stop requires main" + } if (callId == 0L) return val id = callId active = false @@ -335,13 +349,13 @@ class CallAudioEngine { } } synchronized(recordLock) { - audioRecord?.let { rec -> + audioRecord?.takeIf { captureThread != null }?.let { rec -> try { if (rec.recordingState == AudioRecord.RECORDSTATE_RECORDING) rec.stop() } catch (e: Exception) { LogManager.addLog("ERROR", "CallAudio", "record stop failed id=$id: $e") } } } synchronized(trackLock) { - audioTrack?.let { track -> + audioTrack?.takeIf { playThread != null }?.let { track -> try { track.removeOnRoutingChangedListener(trackRouteListener) } catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track listener removal failed id=$id: $e") } try { if (track.playState == AudioTrack.PLAYSTATE_PLAYING) track.pause() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioOutput.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioOutput.kt index 66fa3f9e..8da48db4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioOutput.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioOutput.kt @@ -90,7 +90,8 @@ internal class CallAudioOutput private constructor( companion object { /** AAudio пробуется лишь при tryFast и deviceId встроенного speaker из communication devices API 31+. - * Native проверяет формат/latency/burst/device; отказ возвращает AudioTrack, сбой fallback бросает исключение. */ + * Native проверяет формат/latency/burst/device; отказ возвращает AudioTrack, сбой fallback бросает исключение. + * Владелец сохраняет возвращённый выход до проверки track.state и дальнейшей настройки, включая отказ release. */ fun open(manager: AudioManager, deviceId: Int, tryFast: Boolean): CallAudioOutput { val type = if (Build.VERSION.SDK_INT >= 31) manager.availableCommunicationDevices.firstOrNull { it.id == deviceId }?.type else null