chatgui-android: PTT по volume-кнопкам (вверх/вниз) + синхронизация оверлея
- volume up/down обе держат PTT (holdCount), настройка radio_volume_ptt - syncRadioOverlay при включении/выключении рации
This commit is contained in:
1 parent
8a9184675a
commit
4a785772be
4 files changed
+111
-59
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+70
-46
@@ -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}")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user