Guard call audio ownership on main and avoid touching released devices

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent 0340a59b15
commit 326bfd124e
2 files changed
+20 -5

No files matched your search

@@ -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() }
@@ -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