Release radio resources independently and retain failed cleanup owners
This commit is contained in:
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.
|
||||
|
||||
Reference in new issue
Block a user