Open channel radio from Android calls and move invitation into user details

This commit is contained in:
evgeny committed 2026-10-19 14:02:56 +02:00
1 parent 62a976aef5
commit 25376197db
2 files changed
+29 -9

No files matched your search

@@ -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 рации, при активной рации и включённой опции. */
@@ -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) {