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

View File

@ -1,11 +1,18 @@
package com.tutpro.baresip
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
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 held = false
var terminated = false
var conferenceCall = false
var onHoldCall: Call? = null
var newCall: Call? = null
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 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() {
BaresipService.calls.add(0, this)
BaresipService.calls.add(this)
}
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? {
for (c in BaresipService.calls.reversed())
if (c.status == status) return c
for (c in BaresipService.calls)
if (c.status.value == status) return c
return null
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,106 +1,129 @@
package com.tutpro.baresip
import android.app.Application
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import kotlinx.coroutines.launch
// Sealed class for type-safe navigation events
sealed class NavigationCommand {
data class NavigateToChat(val aor: String, val peer: String?) : NavigationCommand()
object NavigateToHome : NavigationCommand()
data class NavigateToCalls(val aor: String) : NavigationCommand()
object NavigateToHome: NavigationCommand()
// Add other navigation commands as needed
data class NavigateToChat(val aor: String, val peerUri: String) : NavigationCommand()
}
data class AorPeer(val aor: String, val peer: String)
class ViewModel: ViewModel() {
class ViewModel(application: Application) : AndroidViewModel(application) {
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
}
// A map to store message drafts. Key is "aor:peerUri"
private val messageDrafts = mutableMapOf<String, String>()
fun getAorPeerMessage(aor: String, peerUri: String): String {
val key = AorPeer(aor, peerUri)
return _aorPeerMessage.value.getOrDefault(key, "")
return messageDrafts["$aor:$peerUri"] ?: ""
}
private val _showKeyboard = mutableIntStateOf(0)
val showKeyboard: State<Int> = _showKeyboard
fun requestShowKeyboard() {
_showKeyboard.intValue++
fun updateAorPeerMessage(aor: String, peerUri: String, message: String) {
val key = "$aor:$peerUri"
if (message.isEmpty()) {
messageDrafts.remove(key)
} else {
messageDrafts[key] = message
}
}
private val _hideKeyboard = mutableIntStateOf(0)
val hideKeyboard: State<Int> = _hideKeyboard
data class DialerState(
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() {
_hideKeyboard.intValue++
}
val dialerState = DialerState()
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)
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) {
_micIcon.value = icon
}
private val _isDialpadVisible = MutableStateFlow(false)
val isDialpadVisible: StateFlow<Boolean> = _isDialpadVisible
fun toggleDialpadVisibility() {
_isDialpadVisible.value = !_isDialpadVisible.value
}
private val _selectedCallRow = MutableStateFlow<CallRow?>(null)
fun selectCallRow(callRow: CallRow) {
_selectedCallRow.value = callRow
fun requestShowKeyboard() {
_showKeyboard.value += 1
}
fun consumeSelectedCallRow(): CallRow? {
val callRow = _selectedCallRow.value
_selectedCallRow.value = null
return callRow
fun requestHideKeyboard() {
_hideKeyboard.value += 1
}
private val _navigationCommand = MutableSharedFlow<NavigationCommand>()
val navigationCommand = _navigationCommand.asSharedFlow()
fun onNewMessageReceived(aor: String, peer: String) {
fun navigateToHome() {
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)
}
}
}