From 1805b3dd2750f40e01cff4f96346d83949a52e49 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 3 Oct 2026 14:08:17 +0300 Subject: [PATCH] android radio: track independent manual PTT owners --- .../com/utun/chat/data/RadioAudioEngine.kt | 15 +++++++++++---- .../com/utun/chat/data/RadioOverlayService.kt | 6 +++--- .../java/com/utun/chat/data/RadioPttHolds.kt | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioPttHolds.kt diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt index faf65173..e99eb511 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt @@ -72,6 +72,7 @@ class RadioAudioEngine { @Volatile private var captureRunning = false @Volatile private var playbackRunning = false @Volatile private var requestedTalk = false + private val pttHolds = RadioPttHolds() @Volatile private var talkRequestNs = 0L @Volatile private var generation = 0L private var audioGroup = 0L @@ -135,6 +136,7 @@ class RadioAudioEngine { active = true groupId = group requestedTalk = false + pttHolds.cancel() _talkingFlow.value = false io.execute { if (!active || generation != version) return@execute @@ -156,6 +158,7 @@ class RadioAudioEngine { mainHandler.removeCallbacks(releaseTalk) active = false requestedTalk = false + pttHolds.cancel() _talkingFlow.value = false ++generation io.execute { releaseLocked() } @@ -513,7 +516,7 @@ class RadioAudioEngine { restoreAudioMode() } - fun talkBegin(): Boolean { + internal fun talkBegin(source: RadioPttSource = RadioPttSource.BUTTON): Boolean { if (!active) return false if (!MicrophonePermission.granted()) { MicrophonePermission.request("PTT") { @@ -524,6 +527,8 @@ class RadioAudioEngine { return false } mainHandler.removeCallbacks(releaseTalk) + pttHolds.press(source) + LogManager.addLog("DEBUG", "RadioAudio", "PTT hold source=$source transmitting=$requestedTalk group=$groupId") if (requestedTalk) return true talkRequestNs = SystemClock.elapsedRealtimeNanos() requestedTalk = true @@ -540,15 +545,17 @@ class RadioAudioEngine { } /** Отпускание: 250мс передачи, повторное касание отменяет завершение. */ - fun talkEnd() { + internal fun talkEnd(source: RadioPttSource = RadioPttSource.BUTTON) { + if (!pttHolds.release(source)) return if (!active || !requestedTalk) return mainHandler.removeCallbacks(releaseTalk) mainHandler.postDelayed(releaseTalk, RELEASE_HOLD_MS) - LogManager.addLog("DEBUG", "RadioAudio", "PTT release scheduled +${RELEASE_HOLD_MS}ms") + LogManager.addLog("DEBUG", "RadioAudio", "PTT release source=$source scheduled +${RELEASE_HOLD_MS}ms") } /** Отмена без хвоста передачи при выключении/потере разрешений. */ - fun cancelTalk() { + internal fun cancelTalk(source: RadioPttSource? = null) { + if (!pttHolds.cancel(source)) return mainHandler.removeCallbacks(releaseTalk) finishTalk() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt index a80e0143..006a627f 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt @@ -286,7 +286,7 @@ class RadioOverlayService : Service() { private fun removeOverlay() { val view = overlayView ?: return talking = false - RadioAudioEngine.instance().cancelTalk() + RadioAudioEngine.instance().cancelTalk(RadioPttSource.OVERLAY) try { windowManager?.removeView(view) } catch (e: Exception) { LogManager.addLog("WARN", "RadioOverlay", "Overlay removal failed: $e") } @@ -453,7 +453,7 @@ class RadioOverlayService : Service() { private fun beginTalk() { if (talking) return - if (!RadioAudioEngine.instance().talkBegin()) return + if (!RadioAudioEngine.instance().talkBegin(RadioPttSource.OVERLAY)) return talking = true updateButtonColor() } @@ -461,7 +461,7 @@ class RadioOverlayService : Service() { private fun endTalk() { if (!talking) return talking = false - RadioAudioEngine.instance().talkEnd() + RadioAudioEngine.instance().talkEnd(RadioPttSource.OVERLAY) updateButtonColor() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioPttHolds.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioPttHolds.kt new file mode 100644 index 00000000..9c6306f8 --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioPttHolds.kt @@ -0,0 +1,18 @@ +package com.utun.chat.data + +/** Независимые владельцы ручной передачи. Все операции выполняются в главном потоке. */ +internal enum class RadioPttSource { BUTTON, OVERLAY, VOLUME } + +internal class RadioPttHolds { + private val held = mutableSetOf() + + fun press(source: RadioPttSource) { held.add(source) } + + /** Завершать передачу можно только после отпускания последнего владельца. */ + fun release(source: RadioPttSource): Boolean = held.remove(source) && held.isEmpty() + + fun cancel(source: RadioPttSource? = null): Boolean { + if (source == null) held.clear() else held.remove(source) + return held.isEmpty() + } +}