From 9a19cce1e6f482a06a1bc897cb41d8af1e325f75 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sat, 4 Apr 2026 19:05:56 +0300 Subject: [PATCH] Call hold/unhold improvements for multiple simultaneous calls --- app/src/main/cpp/baresip.c | 4 +-- app/src/main/kotlin/com/tutpro/baresip/Api.kt | 2 +- .../com/tutpro/baresip/BaresipService.kt | 33 +++++++++++++++--- .../main/kotlin/com/tutpro/baresip/Call.kt | 34 ++++++++++++++++--- .../com/tutpro/baresip/ConnectionService.kt | 26 +++++++++----- .../kotlin/com/tutpro/baresip/MainScreen.kt | 29 ++++++++++------ 6 files changed, 95 insertions(+), 33 deletions(-) diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index af1da6c4..80714900 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -1384,7 +1384,7 @@ JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_call_1start_1audio( re_thread_leave(); } -JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1hold( +JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1hold( JNIEnv *env, jobject obj, jlong call, jboolean hold) { (void)env; @@ -1403,7 +1403,7 @@ JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1hold( } if (err) LOGW("call_hold error: %d\n", err); - return err; + return err == 0; } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1ismuted( diff --git a/app/src/main/kotlin/com/tutpro/baresip/Api.kt b/app/src/main/kotlin/com/tutpro/baresip/Api.kt index f778cab8..3cb7d6f2 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Api.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Api.kt @@ -90,7 +90,7 @@ object Api { external fun bevent_stop(event: Long) external fun call_connect(callp: Long, peer_uri: String): Int - external fun call_hold(callp: Long, hold: Boolean): Int + external fun call_hold(callp: Long, hold: Boolean): Boolean @Suppress("unused") external fun call_ismuted(callp: Long): Boolean external fun call_transfer(callp: Long, peer_uri: String): Int diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index bdce2868..121cac74 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -851,10 +851,8 @@ class BaresipService: Service() { "call incoming" -> { val peerUri = ev[1] Log.d(TAG, "Incoming call $uap/$callp/$peerUri") - if (Call.ofCallp(callp) == null) Call(callp, ua, peerUri, "in", "incoming").add() - val extras = android.os.Bundle() extras.putLong("uap", uap) extras.putLong("callp", callp) @@ -888,13 +886,38 @@ class BaresipService: Service() { return } "call update" -> { - val held = when (ev[1].toInt()) { + val newHeldState = when (ev[1].toInt()) { Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true else -> false } - call!!.held = held + val connection = ConnectionService.connections[callp] - if (held) connection?.setOnHold() else connection?.setActive() + + if (call!!.held && !newHeldState) { + Log.d(TAG, "Call ${call.callp} un-held by peer. Requesting Telecom Active.") + // Clear local UI state + call.onhold = false + call.callOnHold.value = false + call.showOnHoldNotice.value = false + + // Tell Android to make this call active. + // Telecom will automatically trigger onHold for other connections. + connection?.setActive() + } + + call.held = newHeldState + + if (newHeldState) { + connection?.setOnHold() + call.callOnHold.value = true + call.showOnHoldNotice.value = true + } else if (!call.onhold) { + // Only set active if we haven't manually put it on hold ourselves + call.callOnHold.value = false + call.showOnHoldNotice.value = false + connection?.setActive() + } + if (call.state() == Api.CALL_STATE_EARLY) { if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 3762fb6e..10036eb9 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -5,6 +5,7 @@ import android.media.AudioManager import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.core.net.toUri import java.util.* class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) { @@ -57,20 +58,43 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str } fun hold(): Boolean { - if (Api.call_hold(callp, true) == 0) + if (onhold) return true + val connection = ConnectionService.connections[callp] + if (connection != null) { + connection.setOnHold() + return true + } + // Fallback if no Telecom connection exists + if (Api.call_hold(callp, true)) { onhold = true + callOnHold.value = true + showOnHoldNotice.value = true + } return onhold } fun resume(): Boolean { - if (Api.call_hold(callp, false) == 0) + if (!onhold) return true + val connection = ConnectionService.connections[callp] + if (connection != null) { + connection.setAddress( + "sip:$peerUri".toUri(), + android.telecom.TelecomManager.PRESENTATION_ALLOWED + ) + connection.setActive() + return true + } + // Fallback if no Telecom connection exists + if (Api.call_hold(callp, false)) { onhold = false + callOnHold.value = false + showOnHoldNotice.value = false + } return !onhold } fun transfer(uri: String): Boolean { - val err = Api.call_hold(callp, true) - if (err != 0) + if (!Api.call_hold(callp, true)) return false onhold = true referTo = uri @@ -79,7 +103,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str fun executeTransfer(): Boolean { return if (onHoldCall != null) { - if (Api.call_hold(callp, true) == 0) + if (Api.call_hold(callp, true)) Api.call_replace_transfer(onHoldCall!!.callp, callp) else false diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index 31998163..948a090a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -201,19 +201,27 @@ class ConnectionService : ConnectionService() { } override fun onHold() { - Log.d(TAG, "Telecom Connection onHold $callp") - val c = Call.ofCallp(callp) - if (c?.conferenceCall != true) - c?.hold() - setOnHold() + super.onHold() + Log.d(TAG, "Telecom requested Hold for $callp") + val call = BaresipService.calls.find { it.callp == this.callp } + if (call != null && !call.onhold && !call.conferenceCall) { + call.onhold = true + Api.call_hold(call.callp, true) + } } override fun onUnhold() { Log.d(TAG, "Telecom Connection onUnhold $callp") - val c = Call.ofCallp(callp) - if (c?.conferenceCall != true) - c?.resume() - setActive() + val call = BaresipService.calls.find { it.callp == this.callp } + if (call != null) { + if (!call.conferenceCall) { + call.onhold = false + call.callOnHold.value = false + call.showOnHoldNotice.value = false + Api.call_hold(call.callp, false) + } + setActive() + } } override fun onPlayDtmfTone(c: Char) { diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index e0a76ff2..c34ada70 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -108,6 +108,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -649,7 +650,8 @@ private fun TopAppBar( val aor = viewModel.selectedAor.value val ua = uas.value.find { it.account.aor == aor } val call = ua?.currentCall() - val connection = if (call != null) ConnectionService.connections[call.callp] else null + val connection = + if (call != null) ConnectionService.connections[call.callp] else null if (connection != null) { @Suppress("DEPRECATION") connection.setAudioRoute( @@ -892,9 +894,9 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont val calls by viewModel.calls.collectAsState() val selectedAor by viewModel.selectedAor.collectAsState() - val filteredCalls = calls.filter { it.ua.account.aor == selectedAor && it.status.value != "disconnecting" } - val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" } - val conferenceCall = filteredCalls.any { it.conferenceCall } + val aorCalls = calls.filter { it.ua.account.aor == selectedAor } + val hasActiveCalls = aorCalls.any { !it.callOnHold.value } + val conferenceCall = aorCalls.any { it.conferenceCall } LaunchedEffect(isRefreshing) { if (isRefreshing) { @@ -997,12 +999,13 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont ) { AccountSpinner(ctx, viewModel, navController) - filteredCalls.forEach { call -> - CallCard(ctx = ctx, viewModel = viewModel, call = call, dialerState = null) + aorCalls.forEach { call -> + key(call.callp) { + CallCard(ctx = ctx, viewModel = viewModel, call = call, dialerState = null) + } } - // Only show the dialer if we are not in a transient state - if (!dialingOrRinging && (filteredCalls.isEmpty() || conferenceCall)) + if (!hasActiveCalls || conferenceCall) CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState) Indicator( @@ -1539,7 +1542,7 @@ private fun CallRow( modifier = Modifier.size(48.dp), onClick = { val connection = ConnectionService.connections[call.callp] - if (call.onhold) { + if (call.callOnHold.value) { if (!Call.isAnyCallActive(ctx)) { Log.d( TAG, @@ -1549,6 +1552,7 @@ private fun CallRow( connection.onUnhold() else call.resume() + call.callOnHold.value = false } } else { @@ -1560,6 +1564,7 @@ private fun CallRow( connection.onHold() else call.hold() + call.callOnHold.value = true } }, ) { @@ -1728,8 +1733,10 @@ private fun CallRow( modifier = Modifier .fillMaxWidth() .clickable { - transferUri = suggestion.toString() - call.showSuggestions.value = false + transferUri = + suggestion.toString() + call.showSuggestions.value = + false } .padding(12.dp) ) {