From 4a785772be2fcca58c1cbb5441534b7e62c8f3ca Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 19 Sep 2026 18:16:46 +0300 Subject: [PATCH] =?UTF-8?q?chatgui-android:=20PTT=20=D0=BF=D0=BE=20volume-?= =?UTF-8?q?=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=D0=BC=20(=D0=B2=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=85/=D0=B2=D0=BD=D0=B8=D0=B7)=20+=20=D1=81=D0=B8?= =?UTF-8?q?=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BE=D0=B2=D0=B5=D1=80=D0=BB=D0=B5=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - volume up/down обе держат PTT (holdCount), настройка radio_volume_ptt - syncRadioOverlay при включении/выключении рации --- .../main/java/com/utun/chat/MainActivity.kt | 33 +++-- .../java/com/utun/chat/data/ConfigProvider.kt | 12 +- .../utun/chat/ui/screens/SettingsScreen.kt | 116 +++++++++++------- .../com/utun/chat/viewmodel/ChatViewModel.kt | 9 ++ 4 files changed, 111 insertions(+), 59 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index 00dae398..7be8b75c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt @@ -403,14 +403,33 @@ class MainActivity : ComponentActivity() { if (NativeLib.isReady()) NativeLib.setActive(false) } - /** Volume-up (удержание) → PTT рации, при активной рации и включённой опции. */ + /** Volume-кнопки (удержание) → PTT рации, при активной рации и включённой опции. */ + private var pttHoldCount = 0 + override fun dispatchKeyEvent(event: KeyEvent): Boolean { - if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP && - ChatApplication.instance.configProvider.getRadioVolumeUpPtt() && - RadioAudioEngine.instance().isActive()) { - when (event.action) { - KeyEvent.ACTION_DOWN -> { RadioAudioEngine.instance().talkBegin(); return true } - KeyEvent.ACTION_UP -> { RadioAudioEngine.instance().talkEnd(); return true } + val isVol = event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + val pttOn = ChatApplication.instance.configProvider.getRadioVolumePtt() + && RadioAudioEngine.instance().isActive() + if (!pttOn || !isVol) { + pttHoldCount = 0 + return super.dispatchKeyEvent(event) + } + when (event.action) { + KeyEvent.ACTION_DOWN -> { + if (event.repeatCount == 0) { + if (pttHoldCount == 0) RadioAudioEngine.instance().talkBegin() + pttHoldCount++ + LogManager.addLog("DEBUG", "PTT", "key=${event.keyCode} down hold=$pttHoldCount") + } + return true + } + KeyEvent.ACTION_UP -> { + if (pttHoldCount > 0) { + pttHoldCount-- + if (pttHoldCount == 0) RadioAudioEngine.instance().talkEnd() + } + LogManager.addLog("DEBUG", "PTT", "key=${event.keyCode} up hold=$pttHoldCount") + return true } } return super.dispatchKeyEvent(event) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index b51b9f22..ea3637b5 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -53,7 +53,7 @@ class ConfigProvider(private val context: Context) { private val soundEnabledKey = booleanPreferencesKey("sound_enabled") private val mutedChannelsKey = stringSetPreferencesKey("muted_channels") private val callAudioRouteKey = stringPreferencesKey("call_audio_route") - private val radioVolumeUpPttKey = booleanPreferencesKey("radio_volume_up_ptt") + private val radioVolumePttKey = booleanPreferencesKey("radio_volume_ptt") private val radioOverlayKey = booleanPreferencesKey("radio_overlay") private fun keysFilePath(): String { @@ -261,14 +261,14 @@ class ConfigProvider(private val context: Context) { context.dataStore.edit { prefs -> prefs[callAudioRouteKey] = r.name } } - /* ── Рация: volume-up PTT и оверлей ── */ + /* ── Рация: volume-кнопки PTT и оверлей ── */ - fun getRadioVolumeUpPtt(): Boolean = runBlocking { - context.dataStore.data.first()[radioVolumeUpPttKey] ?: false + fun getRadioVolumePtt(): Boolean = runBlocking { + context.dataStore.data.first()[radioVolumePttKey] ?: false } - suspend fun setRadioVolumeUpPtt(enabled: Boolean) { - context.dataStore.edit { prefs -> prefs[radioVolumeUpPttKey] = enabled } + suspend fun setRadioVolumePtt(enabled: Boolean) { + context.dataStore.edit { prefs -> prefs[radioVolumePttKey] = enabled } } fun getRadioOverlay(): Boolean = runBlocking { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 1dcac386..4210eacf 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -14,12 +14,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import com.utun.chat.ChatApplication import com.utun.chat.data.NativeLib import com.utun.chat.data.NotificationSound import com.utun.chat.data.IncomingCallManager import com.utun.chat.data.RadioOverlayService +import com.utun.chat.data.RadioAudioEngine import com.utun.chat.data.LogManager import com.utun.chat.data.ServerEntry import com.utun.chat.data.LocalSocketState @@ -56,7 +58,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = { var dirty by remember { mutableStateOf(false) } var tabIndex by remember { mutableStateOf(0) } - val tabs = listOf("Settings", "Audio", "Storage") + val tabs = listOf("Settings", "Audio", "Storage", "Radio") val handleBack = { if (dirty && NativeLib.isReady()) { @@ -88,6 +90,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = { 0 -> GeneralSettingsTab(vm, dirty, onDirty = { dirty = it }, onExit) 1 -> AudioSettingsTab(vm) 2 -> StorageSettingsTab() + 3 -> RadioSettingsTab() } } } @@ -359,49 +362,6 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool HorizontalDivider(Modifier.padding(vertical = 8.dp)) - /* ── Рация (PTT) ── */ - Text("Рация (PTT)", style = MaterialTheme.typography.titleMedium) - - var volumeUpPtt by remember { mutableStateOf(provider.getRadioVolumeUpPtt()) } - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Volume-up (удержание) — передача", modifier = Modifier.weight(1f)) - Switch(checked = volumeUpPtt, onCheckedChange = { - volumeUpPtt = it - scope.launch { provider.setRadioVolumeUpPtt(it) } - }) - } - - var overlayEnabled by remember { mutableStateOf(provider.getRadioOverlay()) } - val canOverlay = RadioOverlayService.canDrawOverlays(ChatApplication.instance) - Row( - modifier = Modifier.fillMaxWidth().clickable(enabled = !canOverlay) { - if (!canOverlay) { - try { - ChatApplication.instance.startActivity( - Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:${ChatApplication.instance.packageName}"))) - } catch (e: Exception) { - LogManager.addLog("ERROR", "SETTINGS", "open overlay permission failed: ${e.message}") - } - } - }, - verticalAlignment = Alignment.CenterVertically - ) { - Column(Modifier.weight(1f)) { - Text("PTT-кнопка поверх приложений") - if (!canOverlay) Text("Требуется разрешение «поверх других приложений» — нажмите", - style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error) - } - Switch(checked = overlayEnabled && canOverlay, enabled = canOverlay, onCheckedChange = { - overlayEnabled = it - scope.launch { provider.setRadioOverlay(it) } - if (it) RadioOverlayService.start(ChatApplication.instance) - else RadioOverlayService.stop(ChatApplication.instance) - }) - } - - HorizontalDivider(Modifier.padding(vertical = 8.dp)) - /* Debug */ Text("Debug", style = MaterialTheme.typography.titleMedium) DebugLevelDropdown(label = "Global Level", current = debugLevel.uppercase(), onSelect = setDebug) @@ -502,6 +462,68 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool dismissButton = { TextButton(onClick = { showClearDb = false }) { Text("Cancel") } }) } +/* ── Radio Settings Tab ── */ + +@Composable +private fun RadioSettingsTab() { + val provider = ChatApplication.instance.configProvider + val scope = rememberCoroutineScope() + + Column( + modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + /* ── Volume-кнопки PTT ── */ + var volumePtt by remember { mutableStateOf(provider.getRadioVolumePtt()) } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Кнопки громкости (удержание) — передача", modifier = Modifier.weight(1f)) + Switch(checked = volumePtt, onCheckedChange = { + volumePtt = it + scope.launch { provider.setRadioVolumePtt(it) } + }) + } + + HorizontalDivider(Modifier.padding(vertical = 4.dp)) + + /* ── Оверлей PTT поверх приложений ── */ + var overlayEnabled by remember { mutableStateOf(provider.getRadioOverlay()) } + var canOverlay by remember { mutableStateOf(RadioOverlayService.canDrawOverlays(ChatApplication.instance)) } + LifecycleResumeEffect(Unit) { + canOverlay = RadioOverlayService.canDrawOverlays(ChatApplication.instance) + onPauseOrDispose { } + } + val openOverlayPermission = { + try { + val i = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:${ChatApplication.instance.packageName}")) + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ChatApplication.instance.startActivity(i) + } catch (e: Exception) { + LogManager.addLog("ERROR", "SETTINGS", "open overlay permission failed: ${e.message}") + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text("PTT-кнопка поверх приложений") + if (!canOverlay) Text( + "Требуется разрешение «поверх других приложений» — откройте настройки", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + modifier = Modifier.clickable { openOverlayPermission() }) + } + Switch(checked = overlayEnabled && canOverlay, enabled = canOverlay, onCheckedChange = { + overlayEnabled = it + scope.launch { provider.setRadioOverlay(it) } + if (it && RadioAudioEngine.instance().isActive()) + RadioOverlayService.start(ChatApplication.instance) + else + RadioOverlayService.stop(ChatApplication.instance) + }) + } + } +} + /* ── Audio Settings Tab ── */ @OptIn(ExperimentalMaterial3Api::class) @@ -582,8 +604,10 @@ private fun AudioSettingsTab(vm: ChatViewModel) { ) { val ctx = ChatApplication.instance try { - ctx.startActivity(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, - Uri.parse("package:${ctx.packageName}"))) + val i = Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, + Uri.parse("package:${ctx.packageName}")) + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ctx.startActivity(i) } catch (e: Exception) { LogManager.addLog("ERROR", "SETTINGS", "open full-screen-intent settings failed: ${e.message}") } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 0691933b..4d2b8bf3 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -740,6 +740,7 @@ class ChatViewModel : ViewModel() { _radioTalking.value = null radioAudio.stop() } + syncRadioOverlay() LogManager.addLog("INFO", "VM", "toggleRadio -> ${if (next) "on" else "off"} group=$groupId") } @@ -763,6 +764,14 @@ class ChatViewModel : ViewModel() { radioAudio.stop() } + private fun syncRadioOverlay() { + val ctx = ChatApplication.instance + if (_radioActive.value && ctx.configProvider.getRadioOverlay()) + RadioOverlayService.start(ctx) + else + RadioOverlayService.stop(ctx) + } + fun radioName(nodeId: Long): String = members.value.firstOrNull { it.nodeId == nodeId }?.name ?.takeIf { it.isNotEmpty() } ?: "0x%016x".format(nodeId)