From 5d10a16baa1c98ada693462f347e43cd7bd2670c Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 22 Oct 2026 15:07:25 +0200 Subject: [PATCH] Use shared audio controls in radio and stream publication --- .../com/utun/chat/ui/screens/ChatScreen.kt | 32 +---- .../com/utun/chat/ui/screens/StreamScreen.kt | 3 + .../com/utun/chat/viewmodel/ChatViewModel.kt | 111 ------------------ 3 files changed, 5 insertions(+), 141 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index d0971719..758d9e4d 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -22,10 +22,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.AutoMode import androidx.compose.material.icons.filled.Equalizer import androidx.compose.material.icons.filled.Bluetooth -import androidx.compose.material.icons.filled.HeadsetMic import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.People @@ -56,7 +54,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.utun.chat.ChatApplication -import com.utun.chat.data.AudioRoute import com.utun.chat.data.Channel import com.utun.chat.data.Message import com.utun.chat.data.RadioAudioEngine @@ -70,6 +67,7 @@ import com.utun.chat.ui.components.PortableRadioIcon import com.utun.chat.ui.components.GroupAvatar import com.utun.chat.ui.components.GroupPropertiesDialog import com.utun.chat.ui.components.OwnStreamButton +import com.utun.chat.ui.components.MediaAudioDeviceButton import com.utun.chat.data.ConversationTarget import com.utun.chat.viewmodel.ChatViewModel import com.utun.chat.data.StreamSource @@ -369,11 +367,6 @@ private fun RadioPttButton( /* Единый выбор входа/выхода, PTT громкостью, VAD и плавающая PTT-кнопка. */ @Composable private fun RadioSourceBar(viewModel: ChatViewModel) { - val radioAuto by viewModel.radioAuto.collectAsState() - val radioInputSource by viewModel.radioInputSource.collectAsState() - val wiredInputAvailable by viewModel.wiredInputAvailable.collectAsState() - val btInputAvailable by viewModel.btInputAvailable.collectAsState() - val radioRoute by viewModel.radioRoute.collectAsState() val provider = ChatApplication.instance.configProvider val scope = rememberCoroutineScope() val overlayEnabled by provider.radioOverlayEnabled.collectAsState(initial = provider.getRadioOverlay()) @@ -394,28 +387,7 @@ private fun RadioSourceBar(viewModel: ChatViewModel) { horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { - val currentDeviceIcon = if (radioRoute == AudioRoute.HEADSET) Icons.Default.HeadsetMic else Icons.AutoMirrored.Filled.VolumeUp - val routeName = stringResource(if (radioRoute == AudioRoute.HEADSET) R.string.radio_headset else R.string.radio_speaker) - RadioControlButton(Icons.Default.AutoMode, stringResource(R.string.radio_auto_route, routeName), - radioAuto, centerIcon = currentDeviceIcon) { - viewModel.setRadioAuto() - } - RadioControlButton(Icons.AutoMirrored.Filled.VolumeUp, stringResource(R.string.radio_phone_audio), - !radioAuto && radioInputSource == RadioAudioEngine.INPUT_BUILTIN && radioRoute == AudioRoute.SPEAKER) { - viewModel.setRadioInputSource(RadioAudioEngine.INPUT_BUILTIN) - } - if (wiredInputAvailable) { - RadioControlButton(Icons.Default.HeadsetMic, stringResource(R.string.radio_wired_audio), - !radioAuto && radioInputSource == RadioAudioEngine.INPUT_WIRED && radioRoute == AudioRoute.HEADSET) { - viewModel.setRadioInputSource(RadioAudioEngine.INPUT_WIRED) - } - } - if (btInputAvailable) { - RadioControlButton(Icons.Default.HeadsetMic, stringResource(R.string.radio_bluetooth_audio), - !radioAuto && radioInputSource == RadioAudioEngine.INPUT_BT && radioRoute == AudioRoute.HEADSET, bluetooth = true) { - viewModel.setRadioInputSource(RadioAudioEngine.INPUT_BT) - } - } + MediaAudioDeviceButton(tint = Color.White) } VerticalDivider(modifier = Modifier.height(32.dp), color = Color(0xFF344550)) RadioControlButton(Icons.AutoMirrored.Filled.VolumeUp, stringResource(R.string.volume_ptt), diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt index f527f7bf..3931834e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt @@ -1,5 +1,7 @@ package com.utun.chat.ui.screens +import com.utun.chat.ui.components.MediaAudioDeviceButton + import android.Manifest import android.app.Activity import android.content.pm.PackageManager @@ -264,6 +266,7 @@ private fun StreamCaptureToolbar(cameras: List, config: StreamCapt stringResource(R.string.stream_bitrate_auto_value, bitrateValue) else stringResource(R.string.stream_bitrate, bitrateValue) Row(modifier.horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { + MediaAudioDeviceButton() CaptureControlButton(stringResource(if (audio) R.string.stream_audio_on else R.string.stream_audio_off), enabled = audioEnabled, checked = audio, onClick = { onAudioChange(!audio) }) { Icon(if (audio) Icons.AutoMirrored.Filled.VolumeUp else Icons.AutoMirrored.Filled.VolumeOff, 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 bc1ce29a..c84aaad1 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 @@ -174,24 +174,6 @@ class ChatViewModel : ViewModel() { private val _radioTalkState = MutableStateFlow(RadioTalkState()) val radioTalkState: StateFlow = _radioTalkState - private val _radioRoute = MutableStateFlow(AudioRoute.SPEAKER) - val radioRoute: StateFlow = _radioRoute - - private val _radioHeadsetAvailable = MutableStateFlow(false) - val radioHeadsetAvailable: StateFlow = _radioHeadsetAvailable - - private val _radioInputSource = MutableStateFlow(RadioAudioEngine.INPUT_AUTO) - val radioInputSource: StateFlow = _radioInputSource - - private val _wiredInputAvailable = MutableStateFlow(false) - val wiredInputAvailable: StateFlow = _wiredInputAvailable - - private val _btInputAvailable = MutableStateFlow(false) - val btInputAvailable: StateFlow = _btInputAvailable - - private val _radioAuto = MutableStateFlow(false) - val radioAuto: StateFlow = _radioAuto - private var radioToggleCount = 0 /* рация: активный канал (null = выключена). Одна рация — один канал. */ @@ -363,22 +345,12 @@ class ChatViewModel : ViewModel() { } init { - radioAudio.router.onChanged = { route, headset -> - _radioRoute.value = route - _radioHeadsetAvailable.value = headset - } - radioAudio.router.onInputDevicesChanged = { wired, bt -> - _wiredInputAvailable.value = wired - _btInputAvailable.value = bt - } viewModelScope.launch { radioAudio.activeGroup.collect { group -> if (group == null) clearRadioControls() else { _radioActive.value = radioChannelId == _currentChannel.value?.id _radioTalkState.value = RadioTalkState(group) - _radioInputSource.value = radioAudio.inputSource() - _radioAuto.value = _radioInputSource.value == RadioAudioEngine.INPUT_AUTO } syncRadioOverlay() } @@ -1064,23 +1036,11 @@ class ChatViewModel : ViewModel() { if (!ready && radioChannelId == ch.id && !radioAudio.isActive()) turnRadioOff() onReady?.invoke(ready) } - _radioRoute.value = radioAudio.route() - _radioHeadsetAvailable.value = radioAudio.headsetAvailable() - _wiredInputAvailable.value = radioAudio.router.wiredInputAvailable - _btInputAvailable.value = radioAudio.router.btInputAvailable - _radioInputSource.value = radioAudio.inputSource() - _radioAuto.value = _radioInputSource.value == RadioAudioEngine.INPUT_AUTO } else { NativeLib.radioSetActive(groupId, false) radioAudio.stop() _radioTalking.value = null _radioTalkState.value = RadioTalkState() - _radioRoute.value = AudioRoute.SPEAKER - _radioHeadsetAvailable.value = false - _wiredInputAvailable.value = false - _btInputAvailable.value = false - _radioInputSource.value = RadioAudioEngine.INPUT_AUTO - _radioAuto.value = false } _radioActive.value = next if (next) RadioOverlayService.requestPermissions() else syncRadioOverlay() @@ -1113,71 +1073,6 @@ class ChatViewModel : ViewModel() { } } - /** Ручной выбор выхода рации (динамик/гарнитура) — синхронизирует вход. */ - fun setRadioRoute(r: AudioRoute) { - if (!_radioActive.value) return - _radioAuto.value = false - radioAudio.setRoute(r) - _radioRoute.value = radioAudio.route() - val linkedInput = when (r) { - AudioRoute.SPEAKER -> RadioAudioEngine.INPUT_BUILTIN - AudioRoute.HEADSET -> when { - _wiredInputAvailable.value -> RadioAudioEngine.INPUT_WIRED - _btInputAvailable.value -> RadioAudioEngine.INPUT_BT - else -> null - } - else -> null - } - if (linkedInput != null && linkedInput != _radioInputSource.value) { - _radioInputSource.value = linkedInput - viewModelScope.launch { - ChatApplication.instance.configProvider.setRadioInputSource(linkedInput) - radioAudio.reconfigure() - } - } - LogManager.addLog("INFO", "VM", "setRadioRoute -> ${radioAudio.route()}") - } - - /** Ручной выбор входного устройства рации — синхронизирует выход. */ - fun setRadioInputSource(src: Int) { - if (!_radioActive.value) return - val clamped = src.coerceIn(RadioAudioEngine.INPUT_AUTO, RadioAudioEngine.INPUT_BT) - if (clamped == _radioInputSource.value) return - _radioAuto.value = false - _radioInputSource.value = clamped - val linkedRoute = when (clamped) { - RadioAudioEngine.INPUT_BUILTIN -> AudioRoute.SPEAKER - RadioAudioEngine.INPUT_WIRED, RadioAudioEngine.INPUT_BT -> AudioRoute.HEADSET - else -> null - } - if (linkedRoute != null) { - radioAudio.setRoute(linkedRoute, preferWired = clamped == RadioAudioEngine.INPUT_WIRED) - _radioRoute.value = radioAudio.route() - } - viewModelScope.launch { - ChatApplication.instance.configProvider.setRadioInputSource(clamped) - radioAudio.reconfigure() - } - LogManager.addLog("INFO", "VM", "setRadioInputSource -> $clamped") - } - - /** Авто: выход следует за гарнитурой, вход — автовыбор. */ - fun setRadioAuto() { - if (!_radioActive.value) return - _radioAuto.value = true - radioAudio.router.setAutoHeadset(true) - radioAudio.router.reapply() - _radioRoute.value = radioAudio.route() - if (_radioInputSource.value != RadioAudioEngine.INPUT_AUTO) { - _radioInputSource.value = RadioAudioEngine.INPUT_AUTO - viewModelScope.launch { - ChatApplication.instance.configProvider.setRadioInputSource(RadioAudioEngine.INPUT_AUTO) - radioAudio.reconfigure() - } - } - LogManager.addLog("INFO", "VM", "setRadioAuto -> route=${radioAudio.route()}") - } - /* Полностью выключить рацию (используется при удалении активного канала). */ private fun turnRadioOff() { val chId = radioChannelId ?: return @@ -1191,12 +1086,6 @@ class ChatViewModel : ViewModel() { _radioActive.value = false _radioTalking.value = null _radioTalkState.value = RadioTalkState() - _radioRoute.value = AudioRoute.SPEAKER - _radioHeadsetAvailable.value = false - _wiredInputAvailable.value = false - _btInputAvailable.value = false - _radioInputSource.value = RadioAudioEngine.INPUT_AUTO - _radioAuto.value = false } private fun syncRadioOverlay() {