Show explicit caller choice and already-active radio outcome on Android

This commit is contained in:
evgeny committed 2026-10-19 15:20:25 +02:00
1 parent 187fc7fee8
commit a1f8eaeec2
3 files changed
+38 -9

No files matched your search

@@ -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<RadioCallState?>(null)
val state: StateFlow<RadioCallState?> = _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
}
@@ -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)
}
}
}
@@ -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)) }
}
}
}