Added support for conference calls

This commit is contained in:
Juha Heinanen
2026-01-15 04:38:14 +02:00
7 changed files with 851 additions and 798 deletions

View File

@ -830,7 +830,7 @@ class BaresipService: Service() {
return return
} }
"call outgoing" -> { "call outgoing" -> {
if (call!!.status == "transferring") if (call!!.status.value == "transferring")
break break
stopMediaPlayer() stopMediaPlayer()
setCallVolume() setCallVolume()
@ -989,8 +989,8 @@ class BaresipService: Service() {
} }
"call answered" -> { "call answered" -> {
stopMediaPlayer() stopMediaPlayer()
if (call!!.status == "incoming") if (call!!.status.value == "incoming")
call.status = "answered" call.status.value = "answered"
else else
return return
} }
@ -1002,7 +1002,7 @@ class BaresipService: Service() {
Log.d(TAG, "AoR $aor call $callp established in mode ${am.mode}") Log.d(TAG, "AoR $aor call $callp established in mode ${am.mode}")
if (am.mode != MODE_IN_COMMUNICATION) if (am.mode != MODE_IN_COMMUNICATION)
am.mode = MODE_IN_COMMUNICATION am.mode = MODE_IN_COMMUNICATION
call!!.status = "connected" call!!.status.value = "connected"
call.onhold = false call.onhold = false
if (ua.account.callHistory) if (ua.account.callHistory)
call.startTime = GregorianCalendar() call.startTime = GregorianCalendar()
@ -1020,7 +1020,7 @@ class BaresipService: Service() {
else else
playRingBack() playRingBack()
} }
if (!isMainVisible || call.status != "connected") if (!isMainVisible || call.status.value != "connected")
return return
} }
"call verified", "call secure" -> { "call verified", "call secure" -> {
@ -1104,6 +1104,8 @@ class BaresipService: Service() {
call.onHoldCall = null call.onHoldCall = null
} }
call.remove() call.remove()
if (call.conferenceCall && ua.calls().isEmpty())
Api.module_unload("mixminus")
val reason = ev[1] val reason = ev[1]
val tone = ev[2] val tone = ev[2]
if (tone == "busy") { if (tone == "busy") {

View File

@ -1,11 +1,18 @@
package com.tutpro.baresip package com.tutpro.baresip
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import java.util.* import java.util.*
class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, var status: String) { class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) {
var status: MutableState<String> = mutableStateOf(initialStatus)
var onhold = false var onhold = false
var held = false var held = false
var terminated = 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
@ -15,8 +22,29 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
var referTo = "" var referTo = ""
var dumpfiles = arrayOf("", "") var dumpfiles = arrayOf("", "")
// UI state properties
val callUri: MutableState<String> = mutableStateOf("")
val callUriEnabled: MutableState<Boolean> = mutableStateOf(true)
val callUriLabel: MutableState<String> = mutableStateOf("")
val securityIconTint: MutableState<Int> = mutableIntStateOf(-1)
val showCallTimer: MutableState<Boolean> = mutableStateOf(false)
var callDuration: Int = 0
val showSuggestions: MutableState<Boolean> = mutableStateOf(false)
val showCallButton: MutableState<Boolean> = mutableStateOf(true)
val callButtonEnabled: MutableState<Boolean> = mutableStateOf(true)
val showCancelButton: MutableState<Boolean> = mutableStateOf(false)
val showAnswerRejectButtons: MutableState<Boolean> = mutableStateOf(false)
val showHangupButton: MutableState<Boolean> = mutableStateOf(false)
val showOnHoldNotice: MutableState<Boolean> = mutableStateOf(false)
val callOnHold: MutableState<Boolean> = mutableStateOf(false)
val transferButtonEnabled: MutableState<Boolean> = mutableStateOf(false)
val callTransfer: MutableState<Boolean> = mutableStateOf(false)
val dtmfText: MutableState<String> = mutableStateOf("")
val dtmfEnabled: MutableState<Boolean> = mutableStateOf(false)
val focusDtmf: MutableState<Boolean> = mutableStateOf(false)
fun add() { fun add() {
BaresipService.calls.add(0, this) BaresipService.calls.add(this)
} }
fun remove() { fun remove() {
@ -107,8 +135,8 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
} }
fun call(status: String): Call? { fun call(status: String): Call? {
for (c in BaresipService.calls.reversed()) for (c in BaresipService.calls)
if (c.status == status) return c if (c.status.value == status) return c
return null return null
} }

View File

@ -1,20 +1,20 @@
package com.tutpro.baresip package com.tutpro.baresip
import java.util.* import java.util.GregorianCalendar
class CallRow( data class CallRow(
val aor: String, val peerUri: String, val direction: Int, startTime: GregorianCalendar?, val aor: String,
val stopTime: GregorianCalendar, val recording: Array<String> val peerUri: String,
var direction: Int,
var startTime: GregorianCalendar?,
var stopTime: GregorianCalendar,
var recording: Array<String>
) { ) {
class Details( data class Details(
val direction: Int, val startTime: GregorianCalendar?, var direction: Int,
val stopTime: GregorianCalendar, val recording: Array<String> var startTime: GregorianCalendar?,
var stopTime: GregorianCalendar,
var recording: Array<String>
) )
val details = mutableListOf(Details(direction, startTime, stopTime, recording))
val details = ArrayList<Details>()
init {
details.add(Details(direction, startTime, stopTime, recording))
}
} }

View File

@ -252,29 +252,20 @@ class MainActivity : ComponentActivity() {
navController = rememberNavController() navController = rememberNavController()
LaunchedEffect(key1 = viewModel, key2 = navController) { LaunchedEffect(key1 = viewModel) {
viewModel.navigationCommand.collect { command -> viewModel.navigationCommand.collect { command ->
Log.d(TAG, "MainActivity: Received NavigationCommand: $command") Log.d(TAG, "MainActivity: Received NavigationCommand: $command")
when (command) { when (command) {
is NavigationCommand.NavigateToChat -> { is NavigationCommand.NavigateToChat -> {
val route = "chat/${command.aor}/${command.peer}" val route = "chat/${command.aor}/${command.peerUri}"
navController.navigate(route) { navController.navigate(route)
launchSingleTop = true
popUpTo("main")
}
} }
is NavigationCommand.NavigateToCalls -> { is NavigationCommand.NavigateToCalls -> {
val route = "calls/${command.aor}" val route = "calls/${command.aor}"
navController.navigate(route) { navController.navigate(route)
launchSingleTop = true
popUpTo("main")
}
} }
is NavigationCommand.NavigateToHome -> { is NavigationCommand.NavigateToHome -> {
navController.navigate("main") { navController.navigate("main")
launchSingleTop = true
popUpTo("main")
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -40,8 +40,9 @@ class UserAgent(val uap: Long) {
return result return result
} }
// Returns call of UA that was added last or NULL
fun currentCall(): Call? { fun currentCall(): Call? {
for (c in BaresipService.calls) for (c in BaresipService.calls.reversed())
if (c.ua == this) if (c.ua == this)
return c return c
return null return null

View File

@ -1,106 +1,129 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Application import androidx.compose.runtime.MutableState
import androidx.compose.material.icons.Icons import androidx.compose.runtime.mutableStateOf
import androidx.compose.material.icons.filled.Mic
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// Sealed class for type-safe navigation events
sealed class NavigationCommand { sealed class NavigationCommand {
data class NavigateToChat(val aor: String, val peer: String?) : NavigationCommand() object NavigateToHome : NavigationCommand()
data class NavigateToCalls(val aor: String) : NavigationCommand() data class NavigateToCalls(val aor: String) : NavigationCommand()
object NavigateToHome: NavigationCommand() data class NavigateToChat(val aor: String, val peerUri: String) : NavigationCommand()
// Add other navigation commands as needed
} }
data class AorPeer(val aor: String, val peer: String) class ViewModel: ViewModel() {
class ViewModel(application: Application) : AndroidViewModel(application) { // A map to store message drafts. Key is "aor:peerUri"
private val messageDrafts = mutableMapOf<String, String>()
private val _selectedAor = MutableStateFlow("")
val selectedAor: StateFlow<String> = _selectedAor.asStateFlow()
fun updateSelectedAor(newValue: String) {
_selectedAor.value = newValue
}
private val _accountUpdate = MutableStateFlow(0)
val accountUpdate: StateFlow<Int> = _accountUpdate
fun triggerAccountUpdate() {
_accountUpdate.value++
}
private val _aorPeerMessage = MutableStateFlow(mutableMapOf<AorPeer, String>())
fun updateAorPeerMessage(aor: String, peerUri: String, message: String) {
val key = AorPeer(aor, peerUri)
if (message == "")
_aorPeerMessage.value.remove(key)
else
_aorPeerMessage.value[key] = message
}
fun getAorPeerMessage(aor: String, peerUri: String): String { fun getAorPeerMessage(aor: String, peerUri: String): String {
val key = AorPeer(aor, peerUri) return messageDrafts["$aor:$peerUri"] ?: ""
return _aorPeerMessage.value.getOrDefault(key, "")
} }
private val _showKeyboard = mutableIntStateOf(0) fun updateAorPeerMessage(aor: String, peerUri: String, message: String) {
val showKeyboard: State<Int> = _showKeyboard val key = "$aor:$peerUri"
if (message.isEmpty()) {
fun requestShowKeyboard() { messageDrafts.remove(key)
_showKeyboard.intValue++ } else {
messageDrafts[key] = message
}
} }
private val _hideKeyboard = mutableIntStateOf(0) data class DialerState(
val hideKeyboard: State<Int> = _hideKeyboard val callUri: MutableState<String> = mutableStateOf(""),
val callUriEnabled: MutableState<Boolean> = mutableStateOf(true),
val callUriLabel: MutableState<String> = mutableStateOf(""),
val showSuggestions: MutableState<Boolean> = mutableStateOf(false),
val showCallButton: MutableState<Boolean> = mutableStateOf(true),
val showCallConferenceButton: MutableState<Boolean> = mutableStateOf(true),
val callButtonsEnabled: MutableState<Boolean> = mutableStateOf(true),
val conferenceCall: MutableState<Boolean> = mutableStateOf(false),
)
fun requestHideKeyboard() { val dialerState = DialerState()
_hideKeyboard.intValue++
} private val _calls = MutableStateFlow<List<Call>>(emptyList())
val calls = _calls.asStateFlow()
private val _selectedAor = MutableStateFlow("")
val selectedAor = _selectedAor.asStateFlow()
private val _accountUpdate = MutableStateFlow(0)
val accountUpdate = _accountUpdate.asStateFlow()
private val _micIcon = MutableStateFlow(Icons.Filled.Mic) private val _micIcon = MutableStateFlow(Icons.Filled.Mic)
val micIcon: StateFlow<ImageVector> = _micIcon.asStateFlow() val micIcon = _micIcon.asStateFlow()
private val _isDialpadVisible = MutableStateFlow(false)
val isDialpadVisible = _isDialpadVisible.asStateFlow()
private val _showKeyboard = MutableStateFlow(0)
val showKeyboard = _showKeyboard.asStateFlow()
private val _hideKeyboard = MutableStateFlow(0)
val hideKeyboard = _hideKeyboard.asStateFlow()
private val _navigationCommand = MutableSharedFlow<NavigationCommand>()
val navigationCommand = _navigationCommand.asSharedFlow()
private var _selectedCallRow: CallRow? = null
fun selectCallRow(callRow: CallRow) {
_selectedCallRow = callRow
}
fun consumeSelectedCallRow(): CallRow? {
val callRow = _selectedCallRow
_selectedCallRow = null
return callRow
}
fun onNewMessageReceived(aor: String, peerUri: String) {
viewModelScope.launch {
_navigationCommand.emit(NavigationCommand.NavigateToChat(aor, peerUri))
}
}
fun updateCalls(calls: List<Call>) {
_calls.value = calls
}
fun updateSelectedAor(aor: String) {
_selectedAor.value = aor
}
fun triggerAccountUpdate() {
_accountUpdate.value += 1
}
fun updateMicIcon(icon: ImageVector) { fun updateMicIcon(icon: ImageVector) {
_micIcon.value = icon _micIcon.value = icon
} }
private val _isDialpadVisible = MutableStateFlow(false)
val isDialpadVisible: StateFlow<Boolean> = _isDialpadVisible
fun toggleDialpadVisibility() { fun toggleDialpadVisibility() {
_isDialpadVisible.value = !_isDialpadVisible.value _isDialpadVisible.value = !_isDialpadVisible.value
} }
private val _selectedCallRow = MutableStateFlow<CallRow?>(null) fun requestShowKeyboard() {
_showKeyboard.value += 1
fun selectCallRow(callRow: CallRow) {
_selectedCallRow.value = callRow
} }
fun consumeSelectedCallRow(): CallRow? { fun requestHideKeyboard() {
val callRow = _selectedCallRow.value _hideKeyboard.value += 1
_selectedCallRow.value = null
return callRow
} }
private val _navigationCommand = MutableSharedFlow<NavigationCommand>() fun navigateToHome() {
val navigationCommand = _navigationCommand.asSharedFlow()
fun onNewMessageReceived(aor: String, peer: String) {
viewModelScope.launch { viewModelScope.launch {
_navigationCommand.emit(NavigationCommand.NavigateToChat(aor, peer)) _navigationCommand.emit(NavigationCommand.NavigateToHome)
} }
} }
@ -110,9 +133,4 @@ class ViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
fun navigateToHome() {
viewModelScope.launch {
_navigationCommand.emit(NavigationCommand.NavigateToHome)
}
}
} }