Release radio resources independently and retain failed cleanup owners

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent 530dd81a71
commit e197f72722
1 file changed
+102 -31
@@ -76,11 +76,12 @@ class RadioAudioEngine {
@Volatile private var generation = 0L
private var audioGroup = 0L
private val vadWindow = RadioVadWindow(VAD_BACKGROUND_MS)
private var screenReceiver: BroadcastReceiver? = null
@Volatile private var screenReceiver: BroadcastReceiver? = null
@Volatile private var screenInteractive = false
private var autoSettings = RadioAutoSettings()
private var settingsReady = false
private var settingsRequest = 0L
private var settingsTimeout: Runnable? = null // Только io: текущая отменяемая проверка ACK.
private var captureRequestedMode = RadioAutoSettings.OFF
@Volatile private var autoMode = RadioAutoSettings.OFF
@Volatile private var levelMonitoring = false
@@ -310,12 +311,20 @@ class RadioAudioEngine {
(ChatApplication.instance.getSystemService(Context.POWER_SERVICE) as PowerManager).isInteractive
private fun registerScreenReceiver() {
val version = generation
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!active || generation != version || screenReceiver !== this) {
LogManager.addLog("DEBUG", "radio", "stale screen event ignored action=${intent.action} generation=$version")
return
}
if (intent.action != Intent.ACTION_SCREEN_ON && intent.action != Intent.ACTION_SCREEN_OFF) {
LogManager.addLog("WARN", "radio", "unknown screen event action=${intent.action} group=$audioGroup")
return
}
screenInteractive = intent.action == Intent.ACTION_SCREEN_ON
val version = generation
io.execute {
if (!active || generation != version) return@execute
if (!active || generation != version || screenReceiver !== this) return@execute
if (intent.action == Intent.ACTION_SCREEN_OFF) {
vadWindow.close()
LogManager.addLog("INFO", "radio", "Auto screen off: window closed group=$audioGroup")
@@ -333,10 +342,10 @@ class RadioAudioEngine {
addAction(Intent.ACTION_SCREEN_OFF)
}
val ctx = ChatApplication.instance
screenReceiver = receiver // Владение сохраняется также при исключении после регистрации на стороне ОС.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
ctx.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
else ctx.registerReceiver(receiver, filter)
screenReceiver = receiver
screenInteractive = screenOn()
}
@@ -427,45 +436,107 @@ class RadioAudioEngine {
private fun stopCapture() {
captureRunning = false
val subscription = captureSubscription
if (subscription != null) {
subscription.close()
captureThread?.join()
var failure: Exception? = null
var interrupted = false
try {
if (subscription != null) try {
subscription.close()
captureSubscription = null
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "capture subscription close failed group=$audioGroup: $e")
failure = e
}
interrupted = joinWorker(captureThread, "capture")
captureThread = null
captureSubscription = null
NativeLib.radioCaptureStop()
LogManager.addLog("INFO", "radio", "Auto/PTT capture released group=$audioGroup")
if (subscription != null) try {
NativeLib.radioCaptureStop()
LogManager.addLog("INFO", "radio", "Auto/PTT capture released group=$audioGroup")
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "native capture stop failed group=$audioGroup: $e")
if (failure == null) failure = e
}
autoMode = RadioAutoSettings.OFF
capturePttRequested = false
talking = false
if (_transmittingGroup.value != null) {
LogManager.addLog("DEBUG", "radio", "TX cleared after capture stop group=${_transmittingGroup.value}")
_transmittingGroup.value = null
}
_talkingFlow.value = requestedTalk
_autoActiveFlow.value = false
failure?.let { throw it }
} finally {
if (interrupted) Thread.currentThread().interrupt()
}
autoMode = RadioAutoSettings.OFF
capturePttRequested = false
talking = false
if (_transmittingGroup.value != null) {
LogManager.addLog("DEBUG", "radio", "TX cleared after capture stop group=${_transmittingGroup.value}")
_transmittingGroup.value = null
}
_talkingFlow.value = requestedTalk
_autoActiveFlow.value = false
}
/** Дождаться своего worker до остановки DSP; interruption не разрешает освободить используемые им ресурсы. */
private fun joinWorker(worker: Thread?, kind: String): Boolean {
var interrupted = Thread.interrupted()
while (worker?.isAlive == true) try { worker.join() } catch (e: InterruptedException) {
interrupted = true
LogManager.addLog("WARN", "RadioAudio", "$kind join interrupted group=$audioGroup; finishing cleanup")
}
return interrupted
}
/** Только io: независимая очистка всех владельцев; первый отказ возвращается после остальных попыток. */
private fun releaseLocked() {
mainHandler.removeCallbacks(vadTimeout)
settingsTimeout?.let { mainHandler.removeCallbacks(it) }; settingsTimeout = null
settingsReady = false; ++settingsRequest
vadWindow.setEnabled(false, SystemClock.elapsedRealtime())
screenReceiver?.let { ChatApplication.instance.unregisterReceiver(it) }
screenReceiver = null
stopCapture()
playbackRunning = false
audioTrack?.let { track ->
try { track.stop() } catch (e: Exception) {
LogManager.addLog("WARN", "RadioAudio", "playback stop failed: ${e.message}")
var failure: Exception? = null
var interrupted = Thread.interrupted()
try {
screenReceiver?.let { receiver ->
try {
ChatApplication.instance.unregisterReceiver(receiver)
screenReceiver = null
LogManager.addLog("DEBUG", "radio", "screen receiver removed group=$audioGroup")
} catch (e: IllegalArgumentException) {
screenReceiver = null // ОС подтверждает отсутствие регистрации, в том числе после частичного отказа.
LogManager.addLog("WARN", "radio", "screen receiver already absent group=$audioGroup: $e")
} catch (e: Exception) {
LogManager.addLog("ERROR", "radio", "screen receiver removal failed group=$audioGroup; retained for retry: $e")
failure = e
}
}
try { stopCapture() } catch (e: Exception) { if (failure == null) failure = e }
interrupted = Thread.interrupted() || interrupted
playbackRunning = false
val track = audioTrack
if (playThread != null) try { track?.stop() } catch (e: Exception) {
LogManager.addLog("WARN", "RadioAudio", "playback stop failed group=$audioGroup; release will follow: $e")
}
playThread?.interrupt()
playThread?.join()
interrupted = joinWorker(playThread, "playback") || interrupted
playThread = null
outputLease?.close(); outputLease = null
track.release()
try {
outputLease?.close()
outputLease = null
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "output detach failed group=$audioGroup; retained for retry: $e")
if (e is InterruptedException) interrupted = true
if (failure == null) failure = e
}
if (track != null && outputLease == null) try {
track.release()
audioTrack = null
LogManager.addLog("DEBUG", "RadioAudio", "output released group=$audioGroup track=${System.identityHashCode(track)}")
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "output release failed group=$audioGroup " +
"track=${System.identityHashCode(track)}; retained for retry: $e")
if (failure == null) failure = e
}
try { NativeLib.radioAudioStop() } catch (e: Exception) {
LogManager.addLog("ERROR", "RadioAudio", "native audio stop failed group=$audioGroup: $e")
if (failure == null) failure = e
}
failure?.let { throw it }
} finally {
if (interrupted) Thread.currentThread().interrupt()
}
audioTrack = null
NativeLib.radioAudioStop()
}
/** На main удержать ручной PTT от source; true — запрос принят, false — рация неактивна либо нет RECORD_AUDIO.