From a1f8eaeec23dad583d6d8cce440ab28cc962ec16 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 15:20:25 +0200 Subject: [PATCH] Show explicit caller choice and already-active radio outcome on Android --- .../com/utun/chat/data/RadioCallController.kt | 22 ++++++++++++++++--- .../com/utun/chat/ui/screens/CallScreen.kt | 19 +++++++++++----- .../utun/chat/ui/screens/RadioCallDialog.kt | 6 ++++- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioCallController.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioCallController.kt index feeeb9a1..260d96b0 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioCallController.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioCallController.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.launch import java.nio.ByteBuffer import java.nio.ByteOrder -enum class RadioCallPhase { OUTGOING, INCOMING, RINGING, PREPARING, ENDED } +enum class RadioCallPhase { OUTGOING, INCOMING, RINGING, PREPARING, ENDED, WAIT_DECISION } data class RadioCallState( val id: Long, val channelId: String, val peer: Long, val peerName: String, val channelName: String, val incoming: Boolean, val phase: RadioCallPhase, val reason: Int = 0 @@ -34,6 +34,7 @@ object RadioCallController { private const val CANCEL = 5 private const val FAIL = 6 private const val BUSY = 7 + private const val CONTINUE = 8 private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val _state = MutableStateFlow(null) val state: StateFlow = _state @@ -123,7 +124,7 @@ object RadioCallController { if (type == 12) { ready = NativeLib.isReady(); return } if (type == 13) { reset(); return } if (type != 65 || !ready || !NativeLib.isReady()) return - if (data == null || data.size != 27 || data[24].toInt() !in 1..4 || data[25].toInt() !in 0..7 || data[26].toInt() !in 0..1) { + if (data == null || data.size != 27 || data[24].toInt() !in 1..5 || data[25].toInt() !in 0..9 || data[26].toInt() !in 0..1) { LogManager.addLog("ERROR", "radio", "Invalid call event size=${data?.size}"); return } val buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN) @@ -152,7 +153,10 @@ object RadioCallController { return } if (current.phase == RadioCallPhase.ENDED) return - if (phase == 3 && !incoming) { + if (phase == 5 && !incoming) { + CallRingback.stop() + _state.value = current.copy(phase = RadioCallPhase.WAIT_DECISION) + } else if (phase == 3 && !incoming) { _state.value = current.copy(phase = RadioCallPhase.RINGING) CallRingback.startRinging() } else if (phase == 4) { @@ -177,6 +181,16 @@ object RadioCallController { scope.launch { delay(2500); if (_state.value?.id == st.id && _state.value?.phase == RadioCallPhase.ENDED) clear() } } + fun continueCall(id: Long) { + checkMain() + val st = _state.value ?: return + if (st.id != id || st.phase != RadioCallPhase.WAIT_DECISION) return + _state.value = st.copy(phase = RadioCallPhase.OUTGOING) + LogManager.addLog("INFO", "radio", "Caller overrides phone busy id=$id") + if (!NativeLib.radioCall(id, CONTINUE)) { finish(st, 5); return } + CallRingback.startConnecting() + } + private fun clear() { if (busy()) { IncomingCallManager.stopRinging(); CallRingback.stop() } _state.value = null; _activation.value = null; answerOnVisible = 0 @@ -189,6 +203,8 @@ object RadioCallController { 4 -> R.string.radio_call_cancelled 6 -> R.string.radio_call_no_answer 7 -> R.string.radio_call_audio_error + 8 -> R.string.phone_busy_warning + 9 -> R.string.radio_call_already_active else -> R.string.radio_call_unavailable } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt index e6d7b0fc..95b0112e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt @@ -72,11 +72,12 @@ private fun fmtDuration(sec: Int): String { } @Composable -private fun callStatusText(phase: CallPhase): String = when (phase) { +private fun callStatusText(phase: CallPhase, reason: Int = -1): String = when (phase) { CallPhase.OUTGOING -> stringResource(R.string.call_connecting) CallPhase.RINGING -> stringResource(R.string.call_ringing) + CallPhase.WAIT_DECISION -> stringResource(R.string.phone_busy_warning) CallPhase.ACTIVE -> stringResource(R.string.call_talking) - CallPhase.ENDED -> stringResource(R.string.call_ended) + CallPhase.ENDED -> stringResource(if (reason == 8) R.string.phone_busy_warning else R.string.call_ended) else -> "" } @@ -236,7 +237,7 @@ fun CallScreen( Text(name, color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Medium) Spacer(Modifier.height(8.dp)) Text( - if (phase == CallPhase.ACTIVE) fmtDuration(duration) else callStatusText(phase), + if (phase == CallPhase.ACTIVE) fmtDuration(duration) else callStatusText(phase, st?.reason ?: -1), color = Color(0xFFAEBCC8), fontSize = 16.sp ) @@ -273,6 +274,12 @@ fun CallScreen( Text(stringResource(R.string.close)) } } else { + if (phase == CallPhase.WAIT_DECISION) { + Button(onClick = { st?.let { com.utun.chat.data.CallController.continueCall(it.callId) } }) { + Text(stringResource(R.string.call_anyway)) + } + Spacer(Modifier.height(20.dp)) + } if (phase == CallPhase.ACTIVE) { Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) { if (earpieceAvailable) { @@ -331,12 +338,14 @@ fun CallScreen( contentAlignment = Alignment.Center ) { IconButton(onClick = { viewModel.hangupCall() }) { - Icon(Icons.Filled.CallEnd, stringResource(R.string.hang_up), tint = Color.White, modifier = Modifier.size(36.dp)) + Icon(Icons.Filled.CallEnd, stringResource(if (phase == CallPhase.WAIT_DECISION) R.string.finish_call else R.string.hang_up), + tint = Color.White, modifier = Modifier.size(36.dp)) } } if (!viewingVideo) { Spacer(Modifier.height(6.dp)) - Text(stringResource(R.string.hang_up), color = Color(0xFFAEBCC8), fontSize = 12.sp) + Text(stringResource(if (phase == CallPhase.WAIT_DECISION) R.string.finish_call else R.string.hang_up), + color = Color(0xFFAEBCC8), fontSize = 12.sp) } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt index 6ed8beb6..d90df74c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt @@ -43,16 +43,20 @@ fun RadioCallDialog(state: RadioCallState) { RadioCallPhase.INCOMING -> R.string.radio_call_waiting RadioCallPhase.RINGING -> R.string.radio_call_ringing RadioCallPhase.PREPARING -> R.string.radio_call_preparing + RadioCallPhase.WAIT_DECISION -> R.string.phone_busy_warning RadioCallPhase.ENDED -> RadioCallController.reasonText(state.reason) })) Spacer(Modifier.height(20.dp)) if (state.phase != RadioCallPhase.ENDED && state.phase != RadioCallPhase.PREPARING) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { TextButton(onClick = { RadioCallController.end(state.id) }) { - Text(stringResource(if (state.phase == RadioCallPhase.INCOMING) R.string.decline else android.R.string.cancel)) + Text(stringResource(if (state.phase == RadioCallPhase.INCOMING) R.string.decline + else if (state.phase == RadioCallPhase.WAIT_DECISION) R.string.finish_call else android.R.string.cancel)) } if (state.phase == RadioCallPhase.INCOMING) { Button(onClick = RadioCallController::accept) { Text(stringResource(R.string.accept)) } + } else if (state.phase == RadioCallPhase.WAIT_DECISION) { + Button(onClick = { RadioCallController.continueCall(state.id) }) { Text(stringResource(R.string.call_anyway)) } } } }