From 25376197db8a8fba1e6c4d2b5baad01daf756026 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 14:02:56 +0200 Subject: [PATCH] Open channel radio from Android calls and move invitation into user details --- .../main/java/com/utun/chat/MainActivity.kt | 20 +++++++++++++++++-- .../utun/chat/ui/screens/MemberListScreen.kt | 18 ++++++++++------- 2 files changed, 29 insertions(+), 9 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 be1a64a9..45e7a66d 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 @@ -18,6 +18,8 @@ import android.view.MotionEvent import android.view.WindowManager import com.utun.chat.data.CallController import com.utun.chat.data.CallPhase +import com.utun.chat.data.RadioCallController +import com.utun.chat.data.RadioCallPhase import androidx.appcompat.app.AppCompatActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.BackHandler @@ -145,6 +147,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + RadioCallController.notificationIntent(intent) readFilesIntent(intent) lifecycleScope.launch { ChatApplication.instance.configProvider.radioVolumePttEnabled.collect { enabled -> @@ -206,6 +209,15 @@ class MainActivity : AppCompatActivity() { var screen by rememberSaveable { mutableStateOf(Screen.ChannelList) } var filesReturnScreen by rememberSaveable { mutableStateOf(Screen.Chat) } val fileChannels by vm.channels.collectAsState() + val radioCall by RadioCallController.state.collectAsState() + val radioActivation by RadioCallController.activation.collectAsState() + LaunchedEffect(radioActivation, fileChannels) { + val activation = radioActivation ?: return@LaunchedEffect + if (fileChannels.none { it.id == activation.channelId }) return@LaunchedEffect + screen = Screen.Chat + vm.enableRadioForCall(activation.channelId) { success -> RadioCallController.activated(activation.id, success) } + } + BackHandler(radioCall != null && radioCall?.phase != RadioCallPhase.ENDED) { RadioCallController.end() } LaunchedEffect(requestedFiles, fileChannels) { requestedFiles?.let { target -> val channel = fileChannels.find { it.id == target.group.toULong().toString() } @@ -259,11 +271,11 @@ class MainActivity : AppCompatActivity() { /* После звонка снимаем showWhenLocked входящего. Экран может гаснуть: аудио и CPU удерживает CallService независимо от Activity. */ - LaunchedEffect(callState) { + LaunchedEffect(callState, radioCall) { val st = callState val inCall = st != null && st.phase != CallPhase.NONE && st.phase != CallPhase.ENDED - if (inCall) { + if (inCall || radioCall != null && radioCall?.phase != RadioCallPhase.ENDED) { hadCall = true } else { if (hadCall) releaseLockScreen() @@ -516,6 +528,7 @@ class MainActivity : AppCompatActivity() { if (callState?.phase == CallPhase.INCOMING) { IncomingCallDialog(viewModel = vm) } + radioCall?.let { RadioCallDialog(it) } } } @@ -603,6 +616,7 @@ class MainActivity : AppCompatActivity() { } } CallController.setActivityVisible(true) + RadioCallController.setVisible(true) } override fun onResume() { @@ -619,6 +633,7 @@ class MainActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + RadioCallController.notificationIntent(intent) readFilesIntent(intent) if (intent.getBooleanExtra(MicrophonePermission.EXTRA_REQUEST, false)) { intent.removeExtra(MicrophonePermission.EXTRA_REQUEST) @@ -691,6 +706,7 @@ class MainActivity : AppCompatActivity() { if (MicrophonePermission.requester === microphoneRequester) MicrophonePermission.requester = null if (RadioOverlayService.overlayPermissionRequester === overlayPermissionRequester) RadioOverlayService.overlayPermissionRequester = null CallController.setActivityVisible(false) + RadioCallController.setVisible(false) } /** Volume-кнопки (удержание) → PTT рации, при активной рации и включённой опции. */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index f05dc2b6..228c3731 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -110,7 +110,6 @@ fun MemberListScreen( onCall = { viewModel.startCall(channel.id, m.nodeId, m.name) }, onRadioCall = { com.utun.chat.data.RadioCallController.start(channel.id, m.nodeId) }, onMessage = { viewModel.startDm(channel.id, m.nodeId, m.name) }, - onInvite = { viewModel.showPmInvite(m.nodeId, m.name, channel.id) }, onRadioMute = { viewModel.toggleMemberRadioMute(channel.id, m.nodeId) } ) } @@ -130,6 +129,7 @@ fun MemberListScreen( isSelf = member?.isSelf == true, canEdit = member != null, onFiles = { onFiles(detail.nodeId) }, + onInvite = { viewModel.showPmInvite(detail.nodeId, member?.name ?: detail.name, channel.id) }, onConnect = { Log.i("utun-gui", "Member detail connect: channel=${channel.id} node=${detail.nodeId}") viewModel.connectMemberNode(channel.id, detail.nodeId) @@ -185,7 +185,6 @@ private fun MemberListItem( onCall: () -> Unit, onRadioCall: () -> Unit, onMessage: () -> Unit, - onInvite: () -> Unit, onRadioMute: () -> Unit ) { val onlineColor = when { @@ -278,15 +277,12 @@ private fun MemberListItem( RadioSpeakerIcon(speaking, member.radioMuted, radioStatus + ". " + stringResource(if (member.radioMuted) R.string.radio_member_listen else R.string.radio_member_mute)) } - IconButton(onClick = onInvite, modifier = actionModifier) { - Icon(Icons.Filled.Add, stringResource(R.string.invite_to_group)) + IconButton(onClick = onRadioCall, enabled = member.online, modifier = actionModifier) { + PortableRadioIcon(stringResource(R.string.radio_call_start)) } IconButton(onClick = onCall, enabled = member.online, modifier = actionModifier) { Icon(Icons.Filled.Call, stringResource(R.string.call)) } - IconButton(onClick = onRadioCall, enabled = member.online, modifier = actionModifier) { - PortableRadioIcon(stringResource(R.string.radio_call_start)) - } IconButton(onClick = onMessage, modifier = actionModifier) { Icon(Icons.AutoMirrored.Filled.Message, stringResource(R.string.message)) } @@ -310,6 +306,7 @@ private fun MemberDetailCard( isSelf: Boolean = false, canEdit: Boolean, onFiles: () -> Unit, + onInvite: () -> Unit, onConnect: () -> Unit, onEdit: () -> Unit ) { @@ -403,6 +400,13 @@ private fun MemberDetailCard( } } + if (!isSelf) { + TextButton(onClick = onInvite) { + Icon(Icons.Filled.Add, stringResource(R.string.invite_to_group)) + Spacer(Modifier.width(4.dp)) + Text(stringResource(R.string.invite_to_group)) + } + } Spacer(Modifier.height(4.dp)) val headerParts = mutableListOf("0x${detail.nodeId.toString(16).padStart(16, '0')}") if (detail.lastSeen > 0) {