Call hold/unhold improvements for multiple simultaneous calls

This commit is contained in:
Juha Heinanen
2026-04-04 19:05:56 +03:00
parent f1ad2af9f4
commit 9a19cce1e6
6 changed files with 95 additions and 33 deletions
+2 -2
View File
@@ -1384,7 +1384,7 @@ JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_call_1start_1audio(
re_thread_leave(); 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) JNIEnv *env, jobject obj, jlong call, jboolean hold)
{ {
(void)env; (void)env;
@@ -1403,7 +1403,7 @@ JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1hold(
} }
if (err) if (err)
LOGW("call_hold error: %d\n", err); LOGW("call_hold error: %d\n", err);
return err; return err == 0;
} }
JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1ismuted( JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1ismuted(
@@ -90,7 +90,7 @@ object Api {
external fun bevent_stop(event: Long) external fun bevent_stop(event: Long)
external fun call_connect(callp: Long, peer_uri: String): Int 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") @Suppress("unused")
external fun call_ismuted(callp: Long): Boolean external fun call_ismuted(callp: Long): Boolean
external fun call_transfer(callp: Long, peer_uri: String): Int external fun call_transfer(callp: Long, peer_uri: String): Int
@@ -851,10 +851,8 @@ class BaresipService: Service() {
"call incoming" -> { "call incoming" -> {
val peerUri = ev[1] val peerUri = ev[1]
Log.d(TAG, "Incoming call $uap/$callp/$peerUri") Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
if (Call.ofCallp(callp) == null) if (Call.ofCallp(callp) == null)
Call(callp, ua, peerUri, "in", "incoming").add() Call(callp, ua, peerUri, "in", "incoming").add()
val extras = android.os.Bundle() val extras = android.os.Bundle()
extras.putLong("uap", uap) extras.putLong("uap", uap)
extras.putLong("callp", callp) extras.putLong("callp", callp)
@@ -888,13 +886,38 @@ class BaresipService: Service() {
return return
} }
"call update" -> { "call update" -> {
val held = when (ev[1].toInt()) { val newHeldState = when (ev[1].toInt()) {
Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true
else -> false else -> false
} }
call!!.held = held
val connection = ConnectionService.connections[callp] 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 (call.state() == Api.CALL_STATE_EARLY) {
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer() stopMediaPlayer()
+29 -5
View File
@@ -5,6 +5,7 @@ import android.media.AudioManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri
import java.util.* import java.util.*
class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) { 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 { 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 onhold = true
callOnHold.value = true
showOnHoldNotice.value = true
}
return onhold return onhold
} }
fun resume(): Boolean { 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 onhold = false
callOnHold.value = false
showOnHoldNotice.value = false
}
return !onhold return !onhold
} }
fun transfer(uri: String): Boolean { fun transfer(uri: String): Boolean {
val err = Api.call_hold(callp, true) if (!Api.call_hold(callp, true))
if (err != 0)
return false return false
onhold = true onhold = true
referTo = uri referTo = uri
@@ -79,7 +103,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
fun executeTransfer(): Boolean { fun executeTransfer(): Boolean {
return if (onHoldCall != null) { return if (onHoldCall != null) {
if (Api.call_hold(callp, true) == 0) if (Api.call_hold(callp, true))
Api.call_replace_transfer(onHoldCall!!.callp, callp) Api.call_replace_transfer(onHoldCall!!.callp, callp)
else else
false false
@@ -201,19 +201,27 @@ class ConnectionService : ConnectionService() {
} }
override fun onHold() { override fun onHold() {
Log.d(TAG, "Telecom Connection onHold $callp") super.onHold()
val c = Call.ofCallp(callp) Log.d(TAG, "Telecom requested Hold for $callp")
if (c?.conferenceCall != true) val call = BaresipService.calls.find { it.callp == this.callp }
c?.hold() if (call != null && !call.onhold && !call.conferenceCall) {
setOnHold() call.onhold = true
Api.call_hold(call.callp, true)
}
} }
override fun onUnhold() { override fun onUnhold() {
Log.d(TAG, "Telecom Connection onUnhold $callp") Log.d(TAG, "Telecom Connection onUnhold $callp")
val c = Call.ofCallp(callp) val call = BaresipService.calls.find { it.callp == this.callp }
if (c?.conferenceCall != true) if (call != null) {
c?.resume() if (!call.conferenceCall) {
setActive() call.onhold = false
call.callOnHold.value = false
call.showOnHoldNotice.value = false
Api.call_hold(call.callp, false)
}
setActive()
}
} }
override fun onPlayDtmfTone(c: Char) { override fun onPlayDtmfTone(c: Char) {
@@ -108,6 +108,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -649,7 +650,8 @@ private fun TopAppBar(
val aor = viewModel.selectedAor.value val aor = viewModel.selectedAor.value
val ua = uas.value.find { it.account.aor == aor } val ua = uas.value.find { it.account.aor == aor }
val call = ua?.currentCall() 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) { if (connection != null) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
connection.setAudioRoute( connection.setAudioRoute(
@@ -892,9 +894,9 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
val calls by viewModel.calls.collectAsState() val calls by viewModel.calls.collectAsState()
val selectedAor by viewModel.selectedAor.collectAsState() val selectedAor by viewModel.selectedAor.collectAsState()
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor && it.status.value != "disconnecting" } val aorCalls = calls.filter { it.ua.account.aor == selectedAor }
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" } val hasActiveCalls = aorCalls.any { !it.callOnHold.value }
val conferenceCall = filteredCalls.any { it.conferenceCall } val conferenceCall = aorCalls.any { it.conferenceCall }
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
@@ -997,12 +999,13 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
) { ) {
AccountSpinner(ctx, viewModel, navController) AccountSpinner(ctx, viewModel, navController)
filteredCalls.forEach { call -> aorCalls.forEach { call ->
CallCard(ctx = ctx, viewModel = viewModel, call = call, dialerState = null) 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 (!hasActiveCalls || conferenceCall)
if (!dialingOrRinging && (filteredCalls.isEmpty() || conferenceCall))
CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState) CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState)
Indicator( Indicator(
@@ -1539,7 +1542,7 @@ private fun CallRow(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
val connection = ConnectionService.connections[call.callp] val connection = ConnectionService.connections[call.callp]
if (call.onhold) { if (call.callOnHold.value) {
if (!Call.isAnyCallActive(ctx)) { if (!Call.isAnyCallActive(ctx)) {
Log.d( Log.d(
TAG, TAG,
@@ -1549,6 +1552,7 @@ private fun CallRow(
connection.onUnhold() connection.onUnhold()
else else
call.resume() call.resume()
call.callOnHold.value = false
} }
} }
else { else {
@@ -1560,6 +1564,7 @@ private fun CallRow(
connection.onHold() connection.onHold()
else else
call.hold() call.hold()
call.callOnHold.value = true
} }
}, },
) { ) {
@@ -1728,8 +1733,10 @@ private fun CallRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
transferUri = suggestion.toString() transferUri =
call.showSuggestions.value = false suggestion.toString()
call.showSuggestions.value =
false
} }
.padding(12.dp) .padding(12.dp)
) { ) {