Browse Source

chatgui-android: PTT по volume-кнопкам (вверх/вниз) + синхронизация оверлея

- volume up/down обе держат PTT (holdCount), настройка radio_volume_ptt
- syncRadioOverlay при включении/выключении рации
proxy
evgeny 2 weeks ago
parent
commit
4a785772be
  1. 31
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  2. 12
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  3. 116
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
  4. 9
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

31
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()) {
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 -> { RadioAudioEngine.instance().talkBegin(); return true }
KeyEvent.ACTION_UP -> { RadioAudioEngine.instance().talkEnd(); return true }
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)

12
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 {

116
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}")
}

9
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)

Loading…
Cancel
Save