Added conference calling

This commit is contained in:
Juha Heinanen
2026-01-13 22:51:08 +02:00
parent a2b4555e46
commit ef29fd1855
3 changed files with 85 additions and 48 deletions
@@ -11,6 +11,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
var onhold = false var onhold = false
var held = false var held = false
var conferenceCall = false
var onHoldCall: Call? = null var onHoldCall: Call? = null
var newCall: Call? = null var newCall: Call? = null
var rejected = false // Incoming rejected by user or outgoing fails but not due to 408 or 480 var rejected = false // Incoming rejected by user or outgoing fails but not due to 408 or 480
@@ -74,6 +74,7 @@ import androidx.compose.material.icons.filled.RecordVoiceOver
import androidx.compose.material.icons.filled.SpeakerPhone import androidx.compose.material.icons.filled.SpeakerPhone
import androidx.compose.material.icons.filled.VoiceOverOff import androidx.compose.material.icons.filled.VoiceOverOff
import androidx.compose.material.icons.filled.Voicemail import androidx.compose.material.icons.filled.Voicemail
import androidx.compose.material.icons.filled.WifiCalling
import androidx.compose.material.icons.outlined.ArrowCircleRight import androidx.compose.material.icons.outlined.ArrowCircleRight
import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
@@ -882,6 +883,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor } val filteredCalls = calls.filter { it.ua.account.aor == selectedAor }
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" } val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" }
val conferenceCall = filteredCalls.any { it.conferenceCall }
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
@@ -987,9 +989,8 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
} }
// Only show the dialer if we are not in a transient state // Only show the dialer if we are not in a transient state
if (!dialingOrRinging) { 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(
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
@@ -1406,23 +1407,50 @@ private fun CallRow(
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
dialerState.showCallConferenceButton.value = false
dialerState.showSuggestions.value = false dialerState.showSuggestions.value = false
callClick(ctx, viewModel, dialerState) callClick(ctx, viewModel, dialerState)
}, },
enabled = dialerState.callButtonEnabled.value enabled = dialerState.callButtonsEnabled.value
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Call, imageVector = Icons.Filled.Call,
modifier = Modifier.size(42.dp), modifier = Modifier.size(42.dp),
tint = colorResource(if (dialerState.callButtonEnabled.value) tint = colorResource(if (dialerState.callButtonsEnabled.value)
R.color.colorTrafficGreen R.color.colorTrafficGreen
else else
R.color.colorTrafficYellow), R.color.colorTrafficYellow),
contentDescription = null, contentDescription = null,
) )
} }
} else { if (dialerState.showCallConferenceButton.value) {
Spacer(modifier = Modifier.weight(1f, true))
IconButton(
modifier = Modifier.size(48.dp),
enabled = dialerState.callButtonsEnabled.value,
onClick = {
dialerState.showCallButton.value = false
dialerState.showSuggestions.value = false
callClick(ctx, viewModel, dialerState)
}
) {
Icon(
imageVector = Icons.Filled.WifiCalling,
modifier = Modifier.size(42.dp),
tint = colorResource(
if (dialerState.callButtonsEnabled.value)
R.color.colorTrafficGreen
else
R.color.colorTrafficYellow
),
contentDescription = null,
)
}
}
}
else {
if (call!!.showCancelButton.value) { if (call!!.showCancelButton.value) {
if (!call.conferenceCall)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
@@ -1495,6 +1523,8 @@ private fun CallRow(
} }
var showTransferDialog by remember { mutableStateOf(false) } var showTransferDialog by remember { mutableStateOf(false) }
if (!call.conferenceCall)
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = call.transferButtonEnabled.value, enabled = call.transferButtonEnabled.value,
@@ -1747,6 +1777,7 @@ private fun CallRow(
} }
} }
} }
} }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -1919,13 +1950,13 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
if (uriText.isNotEmpty()) { if (uriText.isNotEmpty()) {
val uris = Contact.contactUris(uriText) val uris = Contact.contactUris(uriText)
if (uris.isEmpty()) if (uris.isEmpty())
makeCall(ctx, viewModel, uriText) makeCall(ctx, viewModel, uriText, dialerState.showCallConferenceButton.value)
else if (uris.size == 1) else if (uris.size == 1)
makeCall(ctx, viewModel, uris[0]) makeCall(ctx, viewModel, uris[0], dialerState.showCallConferenceButton.value)
else { else {
selectItems.value = uris selectItems.value = uris
selectItemAction.value = { index -> selectItemAction.value = { index ->
makeCall(ctx, viewModel, uris[index]) makeCall(ctx, viewModel, uris[index], dialerState.showCallConferenceButton.value)
} }
showSelectItemDialog.value = true showSelectItemDialog.value = true
} }
@@ -1942,7 +1973,7 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
} }
} }
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String) { private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val aor = ua.account.aor val aor = ua.account.aor
@@ -1969,14 +2000,14 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String) {
else if (!BaresipService.requestAudioFocus(ctx)) else if (!BaresipService.requestAudioFocus(ctx))
Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show()
else { else {
viewModel.dialerState.callButtonEnabled.value = false viewModel.dialerState.callButtonsEnabled.value = false
if (Build.VERSION.SDK_INT < 31) { if (Build.VERSION.SDK_INT < 31) {
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
am.mode = AudioManager.MODE_IN_COMMUNICATION am.mode = AudioManager.MODE_IN_COMMUNICATION
runCall(ctx, viewModel, ua, uri) runCall(ctx, viewModel, ua, uri, conferenceCall)
} else { } else {
if (am.mode == AudioManager.MODE_IN_COMMUNICATION) { if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
runCall(ctx, viewModel, ua, uri) runCall(ctx, viewModel, ua, uri, conferenceCall)
} else { } else {
audioModeChangedListener = AudioManager.OnModeChangedListener { mode -> audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
if (mode == AudioManager.MODE_IN_COMMUNICATION) { if (mode == AudioManager.MODE_IN_COMMUNICATION) {
@@ -1986,7 +2017,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String) {
am.removeOnModeChangedListener(audioModeChangedListener!!) am.removeOnModeChangedListener(audioModeChangedListener!!)
audioModeChangedListener = null audioModeChangedListener = null
} }
runCall(ctx, viewModel, ua, uri) runCall(ctx, viewModel, ua, uri, conferenceCall)
} else { } else {
Log.d(TAG, "Audio mode changed to mode ${am.mode} using " + Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
"device ${am.communicationDevice!!.type}") "device ${am.communicationDevice!!.type}")
@@ -2015,13 +2046,13 @@ private fun reject(call: Call) {
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here") Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
} }
private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: String) { private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: String, conferenceCall: Boolean) {
callRunnable = Runnable { callRunnable = Runnable {
callRunnable = null callRunnable = null
val newCall = call(ctx, viewModel, ua, uri) val newCall = call(ctx, viewModel, ua, uri, conferenceCall)
if (newCall == null) { if (newCall == null) {
BaresipService.abandonAudioFocus(ctx) BaresipService.abandonAudioFocus(ctx)
viewModel.dialerState.callButtonEnabled.value = true viewModel.dialerState.callButtonsEnabled.value = true
} }
} }
callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay) callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
@@ -2032,6 +2063,7 @@ private fun call(
viewModel: ViewModel, viewModel: ViewModel,
ua: UserAgent, ua: UserAgent,
uri: String, uri: String,
conferenceCall: Boolean,
onHoldCall: Call? = null onHoldCall: Call? = null
): Call? { ): Call? {
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
@@ -2040,6 +2072,7 @@ private fun call(
Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri") Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
val call = Call(callp, ua, uri, "out", "outgoing") val call = Call(callp, ua, uri, "out", "outgoing")
call.onHoldCall = onHoldCall call.onHoldCall = onHoldCall
call.conferenceCall = conferenceCall
call.add() call.add()
if (onHoldCall != null) if (onHoldCall != null)
onHoldCall.newCall = call onHoldCall.newCall = call
@@ -2077,7 +2110,7 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
if (attended) { if (attended) {
if (call.hold()) { if (call.hold()) {
call.referTo = uri call.referTo = uri
call(ctx, viewModel, ua, uri, call) call(ctx, viewModel, ua, uri, false,call)
} }
} }
else { else {
@@ -2102,7 +2135,8 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
viewModel.dialerState.callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots) viewModel.dialerState.callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots)
viewModel.dialerState.callUriEnabled.value = true viewModel.dialerState.callUriEnabled.value = true
viewModel.dialerState.showCallButton.value = true viewModel.dialerState.showCallButton.value = true
viewModel.dialerState.callButtonEnabled.value = true viewModel.dialerState.showCallConferenceButton.value = true
viewModel.dialerState.callButtonsEnabled.value = true
viewModel.dialerState.showSuggestions.value = false viewModel.dialerState.showSuggestions.value = false
dialpadButtonEnabled.value = true dialpadButtonEnabled.value = true
if (BaresipService.isMicMuted) { if (BaresipService.isMicMuted) {
@@ -2279,7 +2313,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
} }
"call established" -> { "call established" -> {
if (aor == viewModel.selectedAor.value) { if (aor == viewModel.selectedAor.value) {
viewModel.dialerState.callButtonEnabled.value = true // Re-enable dialer viewModel.dialerState.callButtonsEnabled.value = true // Re-enable dialer
val callp = params[1] as Long val callp = params[1] as Long
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
if (call != null) { if (call != null) {
@@ -2351,7 +2385,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (call in Call.calls()) if (call in Call.calls())
acceptTransfer(ctx, viewModel, ua, call!!, ev[1]) acceptTransfer(ctx, viewModel, ua, call!!, ev[1])
else else
makeCall(ctx, viewModel, ev[1]) makeCall(ctx, viewModel, ev[1], false)
} }
negativeText.value = ctx.getString(R.string.no) negativeText.value = ctx.getString(R.string.no)
onNegativeClicked.value = { onNegativeClicked.value = {
@@ -2366,7 +2400,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
if (call in Call.calls()) if (call in Call.calls())
Api.ua_hangup(uap, callp, 487, "Request Terminated") Api.ua_hangup(uap, callp, 487, "Request Terminated")
call(ctx, viewModel, ua, ev[1]) call(ctx, viewModel, ua, ev[1], false)
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"transfer failed" -> { "transfer failed" -> {
@@ -2374,7 +2408,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
} }
"call closed" -> { "call closed" -> {
if (aor == viewModel.selectedAor.value) { if (aor == viewModel.selectedAor.value) {
viewModel.dialerState.callButtonEnabled.value = true viewModel.dialerState.callButtonsEnabled.value = true
ua.account.resumeUri = "" ua.account.resumeUri = ""
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
if (acc.missedCalls) if (acc.missedCalls)
@@ -44,7 +44,9 @@ class ViewModel: ViewModel() {
val callUriLabel: MutableState<String> = mutableStateOf(""), val callUriLabel: MutableState<String> = mutableStateOf(""),
val showSuggestions: MutableState<Boolean> = mutableStateOf(false), val showSuggestions: MutableState<Boolean> = mutableStateOf(false),
val showCallButton: MutableState<Boolean> = mutableStateOf(true), val showCallButton: MutableState<Boolean> = mutableStateOf(true),
val callButtonEnabled: MutableState<Boolean> = mutableStateOf(true), val showCallConferenceButton: MutableState<Boolean> = mutableStateOf(true),
val callButtonsEnabled: MutableState<Boolean> = mutableStateOf(true),
val conferenceCall: MutableState<Boolean> = mutableStateOf(false),
) )
val dialerState = DialerState() val dialerState = DialerState()