More work on multiple calls

This commit is contained in:
Juha Heinanen
2026-01-12 22:22:28 +02:00
parent 04656b4449
commit b07f0afbcf
6 changed files with 675 additions and 639 deletions
@@ -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" -> {
@@ -5,7 +5,9 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf 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
@@ -132,7 +134,7 @@ 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.reversed())
if (c.status == status) return c if (c.status.value == status) return c
return null return null
} }
@@ -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))
}
} }
@@ -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")
}
} }
} }
} }
@@ -106,7 +106,6 @@ 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.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -212,12 +211,15 @@ private fun MainScreen(
val ua = uas.value.find { it.account.aor == viewModel.selectedAor.value } val ua = uas.value.find { it.account.aor == viewModel.selectedAor.value }
val call = ua?.currentCall() val call = ua?.currentCall()
LaunchedEffect(viewModel.showKeyboard.value) { val showKeyboard by viewModel.showKeyboard.collectAsState()
val hideKeyboard by viewModel.hideKeyboard.collectAsState()
LaunchedEffect(showKeyboard) {
if (viewModel.showKeyboard.value > 0) if (viewModel.showKeyboard.value > 0)
keyboardController?.show() keyboardController?.show()
} }
LaunchedEffect(viewModel.hideKeyboard.value) { LaunchedEffect(hideKeyboard) {
if (viewModel.hideKeyboard.value > 0) if (viewModel.hideKeyboard.value > 0)
keyboardController?.hide() keyboardController?.hide()
} }
@@ -385,10 +387,10 @@ private fun MainScreen(
} }
LaunchedEffect(key1 = call?.status, key2 = configuration.orientation) { LaunchedEffect(key1 = call?.status, key2 = configuration.orientation) {
val isConnected = call != null && call.status == "connected" && !call.held val isConnected = call != null && call.status.value == "connected" && !call.held
if (isConnected) { if (isConnected) {
if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
focusDtmf.value = true call.focusDtmf.value = true
delay(300) delay(300)
keyboardController?.show() keyboardController?.show()
} }
@@ -479,9 +481,7 @@ private fun MainScreen(
} }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(
@@ -853,6 +853,21 @@ private val negativeText = mutableStateOf("")
private val onNegativeClicked = mutableStateOf({}) private val onNegativeClicked = mutableStateOf({})
private val showDialog = mutableStateOf(false) private val showDialog = mutableStateOf(false)
@Composable
private fun CallCard(
ctx: Context,
viewModel: ViewModel,
call: Call?,
dialerState: ViewModel.DialerState?
) {
Column {
CallUriRow(ctx, viewModel, call, dialerState)
CallRow(ctx, viewModel, call, dialerState)
if (call != null && call.showOnHoldNotice.value)
OnHoldNotice()
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MainContent(navController: NavController, viewModel: ViewModel, contentPadding: PaddingValues) { private fun MainContent(navController: NavController, viewModel: ViewModel, contentPadding: PaddingValues) {
@@ -863,6 +878,12 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
val swipeThreshold = 200 val swipeThreshold = 200
val ctx = LocalContext.current val ctx = LocalContext.current
val calls by viewModel.calls.collectAsState()
val selectedAor by viewModel.selectedAor.collectAsState()
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor }
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" }
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
delay(1000) delay(1000)
@@ -936,8 +957,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
} }
} } else if (offset > swipeThreshold) {
else if (offset > swipeThreshold) {
if (uas.value.isNotEmpty()) { if (uas.value.isNotEmpty()) {
val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value) val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
val newPos = when (curPos) { val newPos = when (curPos) {
@@ -962,10 +982,16 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
AccountSpinner(ctx, viewModel, navController) AccountSpinner(ctx, viewModel, navController)
CallUriRow(ctx, viewModel)
CallRow(ctx, viewModel) filteredCalls.forEach { call ->
if (showOnHoldNotice.value) CallCard(ctx = ctx, viewModel = viewModel, call = call, dialerState = null)
OnHoldNotice() }
// Only show the dialer if we are not in a transient state
if (!dialingOrRinging) {
CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState)
}
Indicator( Indicator(
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
@@ -1142,7 +1168,14 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
} }
@Composable @Composable
private fun CallUriRow(ctx: Context, viewModel: ViewModel) { private fun CallUriRow(
ctx: Context,
viewModel: ViewModel,
call: Call?,
dialerState: ViewModel.DialerState?
) {
val isDialer = dialerState != null
val suggestions by remember { contactNames } val suggestions by remember { contactNames }
var filteredSuggestions by remember { mutableStateOf(suggestions) } var filteredSuggestions by remember { mutableStateOf(suggestions) }
@@ -1159,27 +1192,29 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
OutlinedTextField( OutlinedTextField(
value = callUri.value, value = if (isDialer) dialerState.callUri.value else call!!.callUri.value,
readOnly = !callUriEnabled.value, readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = {
if (it != callUri.value) { if (isDialer) {
callUri.value = it if (it != dialerState.callUri.value) {
dialerState.callUri.value = it
filteredSuggestions = suggestions.filter { suggestion -> filteredSuggestions = suggestions.filter { suggestion ->
it.length > 2 && suggestion.startsWith(it, ignoreCase = true) it.length > 2 && suggestion.startsWith(it, ignoreCase = true)
} }
showSuggestions.value = it.length > 2 dialerState.showSuggestions.value = it.length > 2
}
} }
}, },
trailingIcon = { trailingIcon = {
if (callUriEnabled.value && callUri.value.isNotEmpty()) if (isDialer && dialerState.callUriEnabled.value && dialerState.callUri.value.isNotEmpty())
Icon(Icons.Outlined.Clear, Icon(Icons.Outlined.Clear,
contentDescription = null, contentDescription = null,
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (showSuggestions.value) if (dialerState.showSuggestions.value)
showSuggestions.value = false dialerState.showSuggestions.value = false
else else
callUri.value = "" dialerState.callUri.value = ""
}, },
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -1189,12 +1224,21 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
.padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp) .padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp)
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onFocusChanged { .onFocusChanged {
if (isDialer) {
val account = Account.ofAor(viewModel.selectedAor.value) val account = Account.ofAor(viewModel.selectedAor.value)
if (account != null && account.numericKeypad) if (account != null && account.numericKeypad)
if (!isDialpadVisible) if (!isDialpadVisible)
viewModel.toggleDialpadVisibility() viewModel.toggleDialpadVisibility()
}
},
label = {
Text(text = if (isDialer)
dialerState.callUriLabel.value
else
call!!.callUriLabel.value,
fontSize = 18.sp
)
}, },
label = { Text(text = callUriLabel.value, fontSize = 18.sp) },
textStyle = TextStyle(fontSize = 18.sp), textStyle = TextStyle(fontSize = 18.sp),
keyboardOptions = if (isDialpadVisible) keyboardOptions = if (isDialpadVisible)
KeyboardOptions(keyboardType = KeyboardType.Phone) KeyboardOptions(keyboardType = KeyboardType.Phone)
@@ -1212,7 +1256,7 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
) )
.animateContentSize() .animateContentSize()
) { ) {
if (showSuggestions.value && filteredSuggestions.isNotEmpty()) { if (isDialer && dialerState.showSuggestions.value && filteredSuggestions.isNotEmpty()) {
Box(modifier = Modifier Box(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 150.dp)) { .heightIn(max = 150.dp)) {
@@ -1234,8 +1278,8 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
callUri.value = suggestion dialerState.callUri.value = suggestion
showSuggestions.value = false dialerState.showSuggestions.value = false
} }
.padding(12.dp) .padding(12.dp)
) { ) {
@@ -1252,24 +1296,24 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
} }
} }
} }
if (showCallTimer.value) { if (call != null && call.showCallTimer.value) {
CallTimer( CallTimer(
initialDurationSeconds = callDuration.toLong(), initialDurationSeconds = call.callDuration.toLong(),
modifier = Modifier.padding( modifier = Modifier.padding(
start = 6.dp, start = 6.dp,
top = 6.dp, top = 6.dp,
end = if (securityIconTint.intValue != -1) 6.dp else 0.dp end = if (call.securityIconTint.value != -1) 6.dp else 0.dp
) )
) )
} }
if (securityIconTint.intValue != -1) if (call != null && call.securityIconTint.value != -1)
Box( Box(
modifier = Modifier modifier = Modifier
.padding(top = 4.dp) .padding(top = 4.dp)
.size(32.dp) .size(32.dp)
.clip(CircleShape) .clip(CircleShape)
.clickable { .clickable {
when (securityIconTint.intValue) { when (call.securityIconTint.value) {
R.color.colorTrafficRed -> { R.color.colorTrafficRed -> {
alertTitle.value = ctx.getString(R.string.alert) alertTitle.value = ctx.getString(R.string.alert)
alertMessage.value = ctx.getString(R.string.call_not_secure) alertMessage.value = ctx.getString(R.string.call_not_secure)
@@ -1285,17 +1329,13 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
dialogMessage.value = ctx.getString(R.string.call_is_secure) dialogMessage.value = ctx.getString(R.string.call_is_secure)
positiveText.value = ctx.getString(R.string.unverify) positiveText.value = ctx.getString(R.string.unverify)
onPositiveClicked.value = { onPositiveClicked.value = {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val call = ua.currentCall()
if (call != null) {
if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0) if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0)
Log.e( Log.e(
TAG, TAG,
"Command 'zrtp_unverify ${call.zid}' failed" "Command 'zrtp_unverify ${call.zid}' failed"
) )
else else
securityIconTint.intValue = R.color.colorTrafficYellow call.securityIconTint.value = R.color.colorTrafficYellow
}
} }
negativeText.value = ctx.getString(R.string.cancel) negativeText.value = ctx.getString(R.string.cancel)
showDialog.value = true showDialog.value = true
@@ -1305,18 +1345,17 @@ private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = if (securityIconTint.intValue == R.color.colorTrafficRed) imageVector = if (call.securityIconTint.value == R.color.colorTrafficRed)
Icons.Filled.LockOpen Icons.Filled.LockOpen
else else
Icons.Filled.Lock, Icons.Filled.Lock,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
tint = colorResource(securityIconTint.intValue) tint = colorResource(call.securityIconTint.value)
) )
} }
} }
} }
@Composable @Composable
private fun CallTimer( private fun CallTimer(
initialDurationSeconds: Long, initialDurationSeconds: Long,
@@ -1348,8 +1387,14 @@ private fun CallTimer(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun CallRow(ctx: Context, viewModel: ViewModel) { private fun CallRow(
ctx: Context,
viewModel: ViewModel,
call: Call?,
dialerState: ViewModel.DialerState?
) {
val isDialer = dialerState != null
val isDialpadVisible by viewModel.isDialpadVisible.collectAsState() val isDialpadVisible by viewModel.isDialpadVisible.collectAsState()
Row( modifier = Modifier Row( modifier = Modifier
@@ -1357,43 +1402,38 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Absolute.SpaceBetween horizontalArrangement = Arrangement.Absolute.SpaceBetween
) { ) {
if (showCallButton.value) if (isDialer) {
if (dialerState.showCallButton.value)
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
showSuggestions.value = false dialerState.showSuggestions.value = false
callClick(ctx, viewModel) callClick(ctx, viewModel, dialerState)
}, },
enabled=callButtonEnabled.value enabled = dialerState.callButtonEnabled.value
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Call, imageVector = Icons.Filled.Call,
modifier = Modifier.size(42.dp), modifier = Modifier.size(42.dp),
tint = colorResource(if (callButtonEnabled.value) tint = colorResource(if (dialerState.callButtonEnabled.value)
R.color.colorTrafficGreen R.color.colorTrafficGreen
else else
R.color.colorTrafficYellow), R.color.colorTrafficYellow),
contentDescription = null, contentDescription = null,
) )
} }
} else {
if (showCancelButton.value) { if (call!!.showCancelButton.value) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
showSuggestions.value = false
abandonAudioFocus(ctx) abandonAudioFocus(ctx)
val ua: UserAgent = UserAgent.ofAor(viewModel.selectedAor.value)!!
val call = ua.currentCall()
if (call != null) {
val callp = call.callp
Log.d( Log.d(
TAG, TAG,
"AoR ${ua.account.aor} canceling call $callp with ${callUri.value}" "AoR ${call.ua.account.aor} canceling call ${call.callp} with ${call.callUri.value}"
) )
Api.ua_hangup(ua.uap, callp, 487, "Request Terminated") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
}
}, },
) { ) {
Icon( Icon(
@@ -1406,20 +1446,14 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
} }
if (showHangupButton.value) { if (call.showHangupButton.value) {
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
abandonAudioFocus(ctx) abandonAudioFocus(ctx)
val uaCalls = ua.calls() Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp} with ${call.callUri.value}")
if (uaCalls.isNotEmpty()) { Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
val call = uaCalls.first()
val callp = call.callp
Log.d(TAG, "AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}")
Api.ua_hangup(ua.uap, callp, 487, "Request Terminated")
}
} }
) { ) {
Icon( Icon(
@@ -1433,32 +1467,27 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val aor = ua.account.aor
val call = ua.currentCall()
if (call != null) {
if (call.onhold) { if (call.onhold) {
Log.d( Log.d(
TAG, TAG,
"AoR $aor resuming call ${call.callp} with ${callUri.value}" "AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
) )
call.resume() call.resume()
call.onhold = false call.onhold = false
} else { } else {
Log.d( Log.d(
TAG, TAG,
"AoR $aor holding call ${call.callp} with ${callUri.value}" "AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
) )
call.hold() call.hold()
call.onhold = true call.onhold = true
} }
}
}, },
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.PauseCircle, imageVector = Icons.Outlined.PauseCircle,
modifier = Modifier.size(42.dp), modifier = Modifier.size(42.dp),
tint = if (callOnHold.value) tint = if (call.callOnHold.value)
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
else else
MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.secondary,
@@ -1469,11 +1498,8 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
var showTransferDialog by remember { mutableStateOf(false) } var showTransferDialog by remember { mutableStateOf(false) }
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = transferButtonEnabled.value, enabled = call.transferButtonEnabled.value,
onClick = { onClick = {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val call = ua.currentCall()
if (call != null) {
if (call.onHoldCall != null) { if (call.onHoldCall != null) {
if (!call.executeTransfer()) { if (!call.executeTransfer()) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
@@ -1482,13 +1508,12 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
} }
} else } else
showTransferDialog = true showTransferDialog = true
}
}, },
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.ArrowCircleRight, imageVector = Icons.Outlined.ArrowCircleRight,
modifier = Modifier.size(42.dp), modifier = Modifier.size(42.dp),
tint = if (callTransfer.value) tint = if (call.callTransfer.value)
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
else else
MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.secondary,
@@ -1500,9 +1525,6 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
val showDialog = remember { mutableStateOf(true) } val showDialog = remember { mutableStateOf(true) }
val blindChecked = remember { mutableStateOf(true) } val blindChecked = remember { mutableStateOf(true) }
val selectedAor: String by viewModel.selectedAor.collectAsState()
val ua = UserAgent.ofAor(selectedAor)!!
val call = ua.currentCall()
if (showDialog.value) if (showDialog.value)
BasicAlertDialog( BasicAlertDialog(
@@ -1537,7 +1559,6 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
singleLine = true, singleLine = true,
onValueChange = { onValueChange = {
if (it != transferUri) { if (it != transferUri) {
transferUri = it
filteredSuggestions = filteredSuggestions =
suggestions.filter { suggestion -> suggestions.filter { suggestion ->
transferUri.length > 2 && transferUri.length > 2 &&
@@ -1546,7 +1567,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
ignoreCase = true ignoreCase = true
) )
} }
showSuggestions.value = transferUri.length > 2 call.showSuggestions.value = transferUri.length > 2
} }
}, },
trailingIcon = { trailingIcon = {
@@ -1555,8 +1576,8 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
Icons.Outlined.Clear, Icons.Outlined.Clear,
contentDescription = null, contentDescription = null,
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (showSuggestions.value) if (call.showSuggestions.value)
showSuggestions.value = false call.showSuggestions.value = false
else else
transferUri = "" transferUri = ""
}, },
@@ -1585,7 +1606,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
) )
.animateContentSize() .animateContentSize()
) { ) {
if (showSuggestions.value && filteredSuggestions.isNotEmpty()) { if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) {
Box(modifier = Modifier Box(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 150.dp)) { .heightIn(max = 150.dp)) {
@@ -1608,7 +1629,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
transferUri = suggestion transferUri = suggestion
showSuggestions.value = false call.showSuggestions.value = false
} }
.padding(12.dp) .padding(12.dp)
) { ) {
@@ -1624,7 +1645,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
} }
} }
} }
if (call != null && call.replaces()) if (call.replaces())
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
@@ -1677,7 +1698,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
} }
TextButton( TextButton(
onClick = { onClick = {
showSuggestions.value = false call.showSuggestions.value = false
var uriText = transferUri.trim() var uriText = transferUri.trim()
if (uriText.isNotEmpty()) { if (uriText.isNotEmpty()) {
val uris = Contact.contactUris(uriText) val uris = Contact.contactUris(uriText)
@@ -1688,7 +1709,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
transfer( transfer(
ctx, ctx,
viewModel, viewModel,
ua, call.ua,
if (Utils.isTelNumber(uri)) "tel:$uri" else uri, if (Utils.isTelNumber(uri)) "tel:$uri" else uri,
!blindChecked.value !blindChecked.value
) )
@@ -1701,7 +1722,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
transfer( transfer(
ctx, ctx,
viewModel, viewModel,
ua, call.ua,
if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText, if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText,
!blindChecked.value !blindChecked.value
) )
@@ -1730,25 +1751,24 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
} }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val shouldRequestFocus by focusDtmf val shouldRequestFocus by call.focusDtmf
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
BasicTextField( BasicTextField(
value = dtmfText.value, value = call.dtmfText.value,
onValueChange = { newText -> onValueChange = { newText ->
if (newText.length > dtmfText.value.length) { if (newText.length > call.dtmfText.value.length) {
val char = newText.last() val char = newText.last()
if (char.isDigit() || char == '*' || char == '#') { if (char.isDigit() || char == '*' || char == '#') {
Log.d(TAG, "Got DTMF digit '$char'") Log.d(TAG, "Got DTMF digit '$char'")
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! call.sendDigit(char)
ua.currentCall()?.sendDigit(char)
} }
} }
dtmfText.value = newText call.dtmfText.value = newText
}, },
modifier = Modifier modifier = Modifier
.width(80.dp) .width(80.dp)
.focusRequester(focusRequester), .focusRequester(focusRequester),
enabled = dtmfEnabled.value, enabled = call.dtmfEnabled.value,
textStyle = TextStyle( textStyle = TextStyle(
fontSize = 16.sp, fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
@@ -1759,11 +1779,11 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
interactionSource = interactionSource, interactionSource = interactionSource,
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
OutlinedTextFieldDefaults.DecorationBox( OutlinedTextFieldDefaults.DecorationBox(
value = dtmfText.value, value = call.dtmfText.value,
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
innerTextField = innerTextField, innerTextField = innerTextField,
singleLine = true, singleLine = true,
enabled = dtmfEnabled.value, enabled = call.dtmfEnabled.value,
interactionSource = interactionSource, interactionSource = interactionSource,
label = { label = {
Text( Text(
@@ -1790,17 +1810,15 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
LaunchedEffect(shouldRequestFocus) { LaunchedEffect(shouldRequestFocus) {
if (shouldRequestFocus) { if (shouldRequestFocus) {
focusRequester.requestFocus() focusRequester.requestFocus()
focusDtmf.value = false call.focusDtmf.value = false
} }
} }
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! val stats = call.stats("audio")
val call = ua.currentCall() if (stats.isNotEmpty() && call.startTime != null) {
val stats = call?.stats("audio")
if (stats != null && call.startTime != null && stats != "") {
val parts = stats.split(",") as java.util.ArrayList val parts = stats.split(",") as java.util.ArrayList
if (parts[2] == "0/0") { if (parts[2] == "0/0") {
parts[2] = "?/?" parts[2] = "?/?"
@@ -1814,8 +1832,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
alertTitle.value = ctx.getString(R.string.call_info) alertTitle.value = ctx.getString(R.string.call_info)
alertMessage.value = alertMessage.value =
"${String.format(ctx.getString(R.string.duration), duration)}\n" + "${String.format(ctx.getString(R.string.duration), duration)}\n" +
"${ctx.getString(R.string.codecs)}: ${txCodec[0]} ch ${txCodec[2]}/" + "${ctx.getString(R.string.codecs)}: ${txCodec[0]} ch ${txCodec[2]}/${rxCodec[0]} ch ${rxCodec[2]}\n" +
"${rxCodec[0]} ch ${rxCodec[2]}\n" +
"${String.format(ctx.getString(R.string.rate), parts[0])}\n" + "${String.format(ctx.getString(R.string.rate), parts[0])}\n" +
"${String.format(ctx.getString(R.string.average_rate), parts[1])}\n" + "${String.format(ctx.getString(R.string.average_rate), parts[1])}\n" +
"${ctx.getString(R.string.packets)}: ${parts[2]}\n" + "${ctx.getString(R.string.packets)}: ${parts[2]}\n" +
@@ -1838,12 +1855,12 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
} }
} }
if (showAnswerRejectButtons.value) { if (call.showAnswerRejectButtons.value) {
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
answer(ctx, viewModel) answer(ctx, call)
}, },
) { ) {
Icon( Icon(
@@ -1859,7 +1876,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
reject(viewModel) reject(call)
}, },
) { ) {
Icon( Icon(
@@ -1872,6 +1889,7 @@ private fun CallRow(ctx: Context, viewModel: ViewModel) {
} }
} }
} }
}
@Composable @Composable
private fun OnHoldNotice() { private fun OnHoldNotice() {
@@ -1894,12 +1912,11 @@ private fun spinToAor(viewModel: ViewModel, aor: String) {
viewModel.triggerAccountUpdate() viewModel.triggerAccountUpdate()
} }
private fun callClick(ctx: Context, viewModel: ViewModel) { private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel.DialerState?) {
if (viewModel.selectedAor.value != "") { if (viewModel.selectedAor.value != "") {
if (Utils.checkPermissions(ctx, arrayOf(RECORD_AUDIO))) { if (Utils.checkPermissions(ctx, arrayOf(RECORD_AUDIO))) {
if (Call.inCall()) if (dialerState != null) {
return val uriText = dialerState.callUri.value.trim()
val uriText = callUri.value.trim()
if (uriText.isNotEmpty()) { if (uriText.isNotEmpty()) {
val uris = Contact.contactUris(uriText) val uris = Contact.contactUris(uriText)
if (uris.isEmpty()) if (uris.isEmpty())
@@ -1913,12 +1930,12 @@ private fun callClick(ctx: Context, viewModel: ViewModel) {
} }
showSelectItemDialog.value = true showSelectItemDialog.value = true
} }
} } else {
else {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val latestPeerUri = CallHistoryNew.aorLatestPeerUri(ua.account.aor) val latestPeerUri = CallHistoryNew.aorLatestPeerUri(ua.account.aor)
if (latestPeerUri != null) if (latestPeerUri != null)
callUri.value = Utils.friendlyUri(ctx, latestPeerUri, ua.account) dialerState.callUri.value = Utils.friendlyUri(ctx, latestPeerUri, ua.account)
}
} }
} }
else else
@@ -1953,7 +1970,7 @@ 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 {
callButtonEnabled.value = false viewModel.dialerState.callButtonEnabled.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
@@ -1984,18 +2001,28 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String) {
} }
} }
private fun answer(ctx: Context, call: Call) {
Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}")
val intent = Intent(ctx, BaresipService::class.java)
intent.action = "Call Answer"
intent.putExtra("uap", call.ua.uap)
intent.putExtra("callp", call.callp)
ctx.startService(intent)
}
private fun reject(call: Call) {
Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}")
call.rejected = true
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) {
callRunnable = Runnable { callRunnable = Runnable {
callRunnable = null callRunnable = null
if (!call(ctx, viewModel, ua, uri)) { val newCall = call(ctx, viewModel, ua, uri)
if (newCall == null) {
BaresipService.abandonAudioFocus(ctx) BaresipService.abandonAudioFocus(ctx)
showCallButton.value = true viewModel.dialerState.callButtonEnabled.value = true
callButtonEnabled.value = true
showCancelButton.value = false
}
else {
showCallButton.value = false
showCancelButton.value = true
} }
} }
callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay) callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
@@ -2003,11 +2030,10 @@ private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: Stri
private fun call( private fun call(
ctx: Context, ctx: Context,
viewModel: ViewModel, viewModel: ViewModel, ua: UserAgent,
ua: UserAgent,
uri: String, uri: String,
onHoldCall: Call? = null onHoldCall: Call? = null
): Boolean { ): Call? {
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF) val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
return if (callp != 0L) { return if (callp != 0L) {
@@ -2019,7 +2045,7 @@ private fun call(
onHoldCall.newCall = call onHoldCall.newCall = call
if (call.connect(uri)) { if (call.connect(uri)) {
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
true call
} else { } else {
Log.w(TAG, "call_connect $callp failed") Log.w(TAG, "call_connect $callp failed")
if (onHoldCall != null) if (onHoldCall != null)
@@ -2027,35 +2053,11 @@ private fun call(
call.remove() call.remove()
call.destroy() call.destroy()
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
false null
} }
} else { } else {
Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed") Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
false null
}
}
private fun answer(ctx: Context, viewModel: ViewModel) {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val call = ua.currentCall()
if (call != null) {
Log.d(TAG, "AoR ${ua.account.aor} answering call from ${callUri.value}")
val intent = Intent(ctx, BaresipService::class.java)
intent.action = "Call Answer"
intent.putExtra("uap", ua.uap)
intent.putExtra("callp", call.callp)
ctx.startService(intent)
}
}
private fun reject(viewModel: ViewModel) {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val call = ua.currentCall()
if (call != null) {
val callp = call.callp
Log.d(TAG, "AoR ${ua.account.aor} rejecting call $callp from ${callUri.value}")
call.rejected = true
Api.ua_hangup(ua.uap, callp, 486, "Busy Here")
} }
} }
@@ -2108,10 +2110,11 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
viewModel.updateMicIcon(Icons.Filled.Mic) viewModel.updateMicIcon(Icons.Filled.Mic)
} }
} else { } else {
viewModel.dialerState.callUri.value = ""
pullToRefreshEnabled.value = false pullToRefreshEnabled.value = false
call.callUriEnabled.value = false call.callUriEnabled.value = false
val isLandscape = ctx.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE val isLandscape = ctx.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
if (isLandscape || call.held || call.status != "connected") { if (isLandscape || call.held || call.status.value != "connected") {
call.focusDtmf.value = false call.focusDtmf.value = false
call.dtmfEnabled.value = !call.held call.dtmfEnabled.value = !call.held
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
@@ -2123,9 +2126,9 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
call.focusDtmf.value = true call.focusDtmf.value = true
viewModel.requestShowKeyboard() viewModel.requestShowKeyboard()
} }
when (call.status) { when (call.status.value) {
"outgoing", "transferring", "answered" -> { "outgoing", "transferring", "answered" -> {
call.callUriLabel.value = if (call.status == "answered") call.callUriLabel.value = if (call.status.value == "answered")
ctx.getString(R.string.incoming_call_from_dots) ctx.getString(R.string.incoming_call_from_dots)
else else
ctx.getString(R.string.outgoing_call_to_dots) ctx.getString(R.string.outgoing_call_to_dots)
@@ -2133,7 +2136,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
call.showCallTimer.value = false call.showCallTimer.value = false
call.securityIconTint.value = -1 call.securityIconTint.value = -1
call.showCallButton.value = false call.showCallButton.value = false
call.showCancelButton.value = call.status == "outgoing" call.showCancelButton.value = call.status.value == "outgoing"
call.showHangupButton.value = !call.showCancelButton.value call.showHangupButton.value = !call.showCancelButton.value
call.showAnswerRejectButtons.value = false call.showAnswerRejectButtons.value = false
call.showOnHoldNotice.value = false call.showOnHoldNotice.value = false
@@ -2248,7 +2251,8 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (!BaresipService.isMainVisible) if (!BaresipService.isMainVisible)
viewModel.navigateToHome() viewModel.navigateToHome()
spinToAor(viewModel, aor) spinToAor(viewModel, aor)
showCall(ctx, viewModel, ua) val callp = params[1] as Long
showCall(ctx, viewModel, ua, Call.ofCallp(callp))
} }
"call redirect" -> { "call redirect" -> {
val redirectUri = ev[1] val redirectUri = ev[1]
@@ -2275,8 +2279,13 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
} }
"call established" -> { "call established" -> {
if (aor == viewModel.selectedAor.value) { if (aor == viewModel.selectedAor.value) {
dtmfText.value = "" viewModel.dialerState.callButtonEnabled.value = true // Re-enable dialer
showCall(ctx, viewModel, ua) val callp = params[1] as Long
val call = Call.ofCallp(callp)
if (call != null) {
call.dtmfText.value = ""
}
showCall(ctx, viewModel, ua, call)
} }
} }
"call update" -> { "call update" -> {
@@ -2301,14 +2310,14 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
} }
call.zid = ev[2] call.zid = ev[2]
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
securityIconTint.intValue = call.security call.securityIconTint.value = call.security
} }
negativeText.value = ctx.getString(R.string.no) negativeText.value = ctx.getString(R.string.no)
onNegativeClicked.value = { onNegativeClicked.value = {
call.security = R.color.colorTrafficYellow call.security = R.color.colorTrafficYellow
call.zid = ev[2] call.zid = ev[2]
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
securityIconTint.intValue = R.color.colorTrafficYellow call.securityIconTint.value = R.color.colorTrafficYellow
onNegativeClicked.value = {} onNegativeClicked.value = {}
} }
showDialog.value = true showDialog.value = true
@@ -2321,7 +2330,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
return return
} }
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
securityIconTint.intValue = call.security call.securityIconTint.value = call.security
} }
"call transfer", "transfer show" -> { "call transfer", "transfer show" -> {
if (!BaresipService.isMainVisible) if (!BaresipService.isMainVisible)
@@ -2364,24 +2373,13 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"call closed" -> { "call closed" -> {
val call = ua.currentCall()
if (call != null) {
call.resume()
callDuration = call.duration()
showCallTimer.value = true
}
else {
showCallTimer.value = false
securityIconTint.intValue = -1
}
if (aor == viewModel.selectedAor.value) { if (aor == viewModel.selectedAor.value) {
viewModel.dialerState.callButtonEnabled.value = true
ua.account.resumeUri = "" ua.account.resumeUri = ""
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
if (acc.missedCalls) if (acc.missedCalls)
viewModel.triggerAccountUpdate() viewModel.triggerAccountUpdate()
} }
//if (kgm.isDeviceLocked)
// this.setShowWhenLocked(false)
} }
"message", "message show", "message reply" -> { "message", "message show", "message reply" -> {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
@@ -2404,6 +2402,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
else -> Log.e(TAG, "Unknown event '${ev[0]}'") else -> Log.e(TAG, "Unknown event '${ev[0]}'")
} }
viewModel.updateCalls(Call.calls().toList())
handleNextEvent() handleNextEvent()
} }
@@ -2423,10 +2422,10 @@ fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: Str
Log.w(TAG, "handleIntent 'call' did not find ua $uap") Log.w(TAG, "handleIntent 'call' did not find ua $uap")
return return
} }
callUri.value = intent.getStringExtra("peer")!! viewModel.dialerState.callUri.value = intent.getStringExtra("peer")!!
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
if (ev[0] == "call") if (ev[0] == "call")
callClick(ctx, viewModel) callClick(ctx, viewModel, viewModel.dialerState)
} }
"call show", "call answer" -> { "call show", "call answer" -> {
val callp = intent.getLongExtra("callp", 0L) val callp = intent.getLongExtra("callp", 0L)
@@ -2438,7 +2437,7 @@ fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: Str
val ua = call.ua val ua = call.ua
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
if (ev[0] == "call answer") if (ev[0] == "call answer")
answer(ctx, viewModel) answer(ctx, call)
else else
BaresipService.postServiceEvent(ServiceEvent( BaresipService.postServiceEvent(ServiceEvent(
"call incoming", "call incoming",
@@ -2549,8 +2548,8 @@ fun callAction(ctx: Context, viewModel: ViewModel, uri: Uri?, action: String) {
private fun redirect(ctx: Context, viewModel: ViewModel, ua: UserAgent, redirectUri: String) { private fun redirect(ctx: Context, viewModel: ViewModel, ua: UserAgent, redirectUri: String) {
if (ua.account.aor != viewModel.selectedAor.value) if (ua.account.aor != viewModel.selectedAor.value)
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
callUri.value = redirectUri viewModel.dialerState.callUri.value = redirectUri
callClick(ctx, viewModel) callClick(ctx, viewModel, viewModel.dialerState)
} }
private fun acceptTransfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, call: Call, uri: String) { private fun acceptTransfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, call: Call, uri: String) {
@@ -4,13 +4,40 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
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.Icons
import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.Mic
import kotlinx.coroutines.launch
// Sealed class for type-safe navigation events
sealed class NavigationCommand {
object NavigateToHome : NavigationCommand()
data class NavigateToCalls(val aor: String) : NavigationCommand()
data class NavigateToChat(val aor: String, val peerUri: String) : NavigationCommand()
}
class ViewModel: ViewModel() { class ViewModel: ViewModel() {
// A map to store message drafts. Key is "aor:peerUri"
private val messageDrafts = mutableMapOf<String, String>()
fun getAorPeerMessage(aor: String, peerUri: String): String {
return messageDrafts["$aor:$peerUri"] ?: ""
}
fun updateAorPeerMessage(aor: String, peerUri: String, message: String) {
val key = "$aor:$peerUri"
if (message.isEmpty()) {
messageDrafts.remove(key)
} else {
messageDrafts[key] = message
}
}
data class DialerState( data class DialerState(
val callUri: MutableState<String> = mutableStateOf(""), val callUri: MutableState<String> = mutableStateOf(""),
val callUriEnabled: MutableState<Boolean> = mutableStateOf(true), val callUriEnabled: MutableState<Boolean> = mutableStateOf(true),
@@ -43,11 +70,24 @@ class ViewModel: ViewModel() {
private val _hideKeyboard = MutableStateFlow(0) private val _hideKeyboard = MutableStateFlow(0)
val hideKeyboard = _hideKeyboard.asStateFlow() 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) { fun onNewMessageReceived(aor: String, peerUri: String) {
val acc = Account.ofAor(aor) viewModelScope.launch {
if (acc != null) { _navigationCommand.emit(NavigationCommand.NavigateToChat(aor, peerUri))
acc.unreadMessages = true
triggerAccountUpdate()
} }
} }
@@ -60,7 +100,7 @@ class ViewModel: ViewModel() {
} }
fun triggerAccountUpdate() { fun triggerAccountUpdate() {
_accountUpdate.value = _accountUpdate.value + 1 _accountUpdate.value += 1
} }
fun updateMicIcon(icon: ImageVector) { fun updateMicIcon(icon: ImageVector) {
@@ -72,19 +112,23 @@ class ViewModel: ViewModel() {
} }
fun requestShowKeyboard() { fun requestShowKeyboard() {
_showKeyboard.value = _showKeyboard.value + 1 _showKeyboard.value += 1
} }
fun requestHideKeyboard() { fun requestHideKeyboard() {
_hideKeyboard.value = _hideKeyboard.value + 1 _hideKeyboard.value += 1
} }
fun navigateToHome() { fun navigateToHome() {
// This function can be used to navigate to the main screen viewModelScope.launch {
_navigationCommand.emit(NavigationCommand.NavigateToHome)
}
} }
fun navigateToCalls(aor: String) { fun navigateToCalls(aor: String) {
// This function can be used to navigate to the calls screen viewModelScope.launch {
_navigationCommand.emit(NavigationCommand.NavigateToCalls(aor))
}
} }
} }