Added support for multiple uris in baresip contact

Improved selectable alert dialog colors
This commit is contained in:
Juha Heinanen
2026-05-17 18:50:36 +03:00
parent de575d52b7
commit aa0098b16b
6 changed files with 279 additions and 86 deletions

View File

@ -37,7 +37,9 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -108,7 +110,7 @@ private data class ScreenState(
val id: Long = 0,
val newId: Long = 0,
val name: String = "",
val uri: String = "",
val uris: List<String> = emptyList(),
val color: Int = 0,
val avatarImageUri: String? = null,
val tmpAvatarFile: File? = null,
@ -138,7 +140,7 @@ private fun ContactScreen(
screenState = ScreenState(
new = true,
name = "",
uri = uriOrNameArg,
uris = if (uriOrNameArg == "") emptyList() else listOf(uriOrNameArg),
favorite = false,
android = BaresipService.contactsMode == "android",
color = Utils.randomColor(),
@ -153,7 +155,7 @@ private fun ContactScreen(
screenState = ScreenState(
new = false,
name = uriOrNameArg,
uri = contact.uri,
uris = contact.uris,
favorite = contact.favorite,
android = false,
color = contact.color,
@ -320,9 +322,9 @@ private fun ContactContent(
new = screenState.new,
onNameChange = { newName -> onStateChange(screenState.copy(name = newName)) }
)
ContactUri(
uri = screenState.uri,
onUriChange = { newUri -> onStateChange(screenState.copy(uri = newUri)) }
ContactUris(
uris = screenState.uris,
onUrisChange = { newUris -> onStateChange(screenState.copy(uris = newUris)) }
)
Favorite(
ctx = ctx,
@ -456,16 +458,62 @@ private fun ContactName(name: String, new: Boolean, onNameChange: (String) -> Un
}
@Composable
private fun ContactUri(uri: String, onUriChange: (String) -> Unit) {
OutlinedTextField(
value = uri,
placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
onValueChange = onUriChange,
private fun ContactUris(uris: List<String>, onUrisChange: (List<String>) -> Unit) {
Column(
modifier = Modifier.fillMaxWidth(),
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp),
label = { Text(stringResource(R.string.sip_or_tel_uri)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
uris.forEachIndexed { index, uri ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = uri,
placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
onValueChange = { newUri ->
val newList = uris.toMutableList()
newList[index] = newUri
onUrisChange(newList.toList())
},
modifier = Modifier.weight(1f),
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp),
label = { Text("${stringResource(R.string.sip_or_tel_uri)} ${index + 1}") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
if (uris.size > 1) {
IconButton(
onClick = {
val newList = uris.toMutableList()
newList.removeAt(index)
onUrisChange(newList.toList())
},
modifier = Modifier.padding(start = 4.dp)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
IconButton(
onClick = {
val newList = uris.toMutableList()
newList.add("")
onUrisChange(newList.toList())
},
modifier = Modifier.align(Alignment.Start)
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add",
tint = MaterialTheme.colorScheme.primary
)
}
}
}
@Composable
@ -520,22 +568,34 @@ private fun checkOnClick(
uriOrNameArg: String
): Boolean {
var newUri = currentState.uri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
newUri = if (Utils.isTelNumber(newUri))
"tel:$newUri"
else
"sip:$newUri"
val newUris = ArrayList<String>()
for (uri in currentState.uris) {
var u = uri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
if (u == "") continue
if (!u.startsWith("sip:") && !u.startsWith("tel:"))
u = if (Utils.isTelNumber(u))
"tel:$u"
else
"sip:$u"
if (!Utils.checkUri(newUri)) {
if (!Utils.checkUri(u)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), u)
showAlert.value = true
return false
}
newUris.add(u)
}
if (newUris.isEmpty()) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newUri)
alertMessage.value = ctx.getString(R.string.sip_or_tel_uri)
showAlert.value = true
return false
}
var newName = currentState.name.trim()
if (newName == "") newName = newUri.substringAfter(":")
if (newName == "") newName = newUris[0].substringAfter(":")
if (!Utils.checkName(newName)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName)
@ -572,7 +632,7 @@ private fun checkOnClick(
val contact: Contact.BaresipContact =
Contact.BaresipContact(
newName,
newUri,
newUris,
currentState.color,
idToUse,
currentState.favorite
@ -683,17 +743,19 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
.build())
val mimeType = if (contact.uri.startsWith("sip:"))
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
else
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
ops.add(
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, mimeType)
.withValue(Data.DATA1, contact.uri.substringAfter(":"))
.build())
for (uri in contact.uris) {
val mimeType = if (uri.startsWith("sip:"))
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
else
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
ops.add(
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, mimeType)
.withValue(Data.DATA1, uri.substringAfter(":"))
.build())
}
if (contact.avatarImage != null) {
val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
@ -717,8 +779,9 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
}
private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) {
if (updateAndroidUri(ctx, rawContactId, contact.uri) == 0)
addAndroidUri(ctx, rawContactId, contact.uri)
for (uri in contact.uris)
if (updateAndroidUri(ctx, rawContactId, uri) == 0)
addAndroidUri(ctx, rawContactId, uri)
if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0)
if (contact.avatarImage != null)
addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!)

View File

@ -72,6 +72,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import coil.compose.AsyncImage
import com.tutpro.baresip.CustomElements.AlertDialog
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
import com.tutpro.baresip.CustomElements.verticalScrollbar
fun NavGraphBuilder.callsScreenRoute(navController: NavController, viewModel: ViewModel) {
@ -303,6 +304,15 @@ private fun Calls(
lastButtonText = stringResource(R.string.ok),
)
SelectableAlertDialog(
openDialog = CustomElements.showSelectItemDialog,
title = stringResource(R.string.choose_destination_uri),
items = CustomElements.selectItems.value,
onItemClicked = CustomElements.selectItemAction.value,
neutralButtonText = stringResource(R.string.cancel),
onNeutralClicked = {}
)
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
@ -339,32 +349,88 @@ private fun Calls(
secondButtonText.value = ctx.getString(R.string.call)
secondAction.value = {
if (ua != null) {
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
val contact = Contact.findContact(peerUri)
val uris = if (contact is Contact.BaresipContact) {
if (ua.account.isMobile)
contact.uris.filter { it.startsWith("tel:") }
else
contact.uris
} else {
listOf(peerUri)
}
if (uris.size > 1) {
CustomElements.selectItems.value = uris
CustomElements.selectItemAction.value = { index ->
intent.putExtra("peer", uris[index])
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
}
CustomElements.showSelectItemDialog.value = false
}
CustomElements.showSelectItemDialog.value = true
} else if (uris.size == 1) {
intent.putExtra("peer", uris[0])
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
}
}
}
}
lastButtonText.value = ctx.getString(R.string.send_message)
lastAction.value = {
if (account.isMobile) {
if (!Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
if (ua != null) {
val contact = Contact.findContact(peerUri)
val uris = if (contact is Contact.BaresipContact) {
if (ua.account.isMobile)
contact.uris.filter { it.startsWith("tel:") }
else
contact.uris
} else {
if (ua != null) {
listOf(peerUri)
}
if (uris.size > 1) {
CustomElements.selectItems.value = uris
CustomElements.selectItemAction.value = { index ->
intent.putExtra("peer", uris[index])
if (ua.account.isMobile) {
if (!Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
} else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
} else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
CustomElements.showSelectItemDialog.value = false
}
CustomElements.showSelectItemDialog.value = true
} else if (uris.size == 1) {
intent.putExtra("peer", uris[0])
if (ua.account.isMobile) {
if (!Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
} else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
} else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
}
}
else
if (ua != null) {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
}
showDialog.value = true
},

View File

@ -13,7 +13,7 @@ import java.util.ArrayList
sealed class Contact {
class BaresipContact(var name: String, var uri: String, var color: Int, var id: Long,
class BaresipContact(var name: String, val uris: ArrayList<String>, var color: Int, var id: Long,
var favorite: Boolean): Contact() {
var avatarImage: Bitmap? = null
}
@ -57,7 +57,7 @@ sealed class Contact {
fun copy(): Contact {
val copy = when (this) {
is BaresipContact ->
BaresipContact(name, uri, color, id, favorite)
BaresipContact(name, ArrayList(uris), color, id, favorite)
is AndroidContact ->
AndroidContact(name, color, thumbnailUri, id, favorite)
}
@ -107,9 +107,10 @@ sealed class Contact {
when (c) {
is BaresipContact -> {
if (c.name.equals(name, ignoreCase = true)) {
uris.add(c.uri.removePrefix("<")
.replaceAfter(">", "")
.replace(">", ""))
for (u in c.uris)
uris.add(u.removePrefix("<")
.replaceAfter(">", "")
.replace(">", ""))
return uris
}
}
@ -128,8 +129,9 @@ sealed class Contact {
for (c in BaresipService.contacts)
when (c) {
is BaresipContact -> {
if (Utils.uriMatch(c.uri, uri))
return c
for (u in c.uris)
if (Utils.uriMatch(u, uri))
return c
}
is AndroidContact -> {
val cleanUri = uri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
@ -153,7 +155,7 @@ sealed class Contact {
val avatarFiles = avatarFileNames()
var contents = ""
for (c in BaresipService.baresipContacts.value) {
contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" +
contents += "\"${c.name}\" <${c.uris.joinToString(",")}>;id=${c.id};color=${c.color}" +
";favorite=${if (c.favorite) "yes" else "no"}\n"
avatarFiles.remove(c.id.toString() + ".png")
}
@ -236,7 +238,8 @@ sealed class Contact {
contactNo++
val name = parts[1]
val uriParams = parts[2].trim()
val uri = uriParams.substringAfter("<").substringBefore(">")
val urisPart = uriParams.substringAfter("<").substringBefore(">")
val uris = ArrayList(urisPart.split(","))
val params = uriParams.substringAfter(">;")
val colorValue = Utils.paramValue(params, "color" )
val color: Int = if (colorValue != "")
@ -249,8 +252,8 @@ sealed class Contact {
else
baseId + contactNo
val favorite = Utils.paramValue(params, "favorite" ) == "yes"
Log.d(TAG, "Restoring contact $name, $uri, $color, $id")
val contact = BaresipContact(name, uri, color, id, favorite)
Log.d(TAG, "Restoring contact $name, $urisPart, $color, $id")
val contact = BaresipContact(name, uris, color, id, favorite)
val avatarFilePath = BaresipService.filesPath + "/$id.png"
if (File(avatarFilePath).exists()) {
try {

View File

@ -77,6 +77,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import coil.compose.AsyncImage
import com.tutpro.baresip.CustomElements.AlertDialog
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
import com.tutpro.baresip.CustomElements.TextAvatar
import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.io.File
@ -209,6 +210,15 @@ private fun ContactsScreen(
}
)
SelectableAlertDialog(
openDialog = CustomElements.showSelectItemDialog,
title = stringResource(R.string.choose_destination_uri),
items = CustomElements.selectItems.value,
onItemClicked = CustomElements.selectItemAction.value,
neutralButtonText = stringResource(R.string.cancel),
onNeutralClicked = {}
)
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
containerColor = MaterialTheme.colorScheme.background,
@ -508,7 +518,6 @@ private fun ContactsContent(
val intent = Intent(ctx, MainActivity::class.java)
if (ua != null) {
intent.putExtra("uap", ua.uap)
intent.putExtra("peer", contact.uri)
}
else
Log.w(TAG, "onClickListener did not find UA for $aor")
@ -519,30 +528,79 @@ private fun ContactsContent(
secondText.value = ctx.getString(R.string.call)
secondAction.value = {
if (ua != null) {
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
val uris = if (ua.account.isMobile)
contact.uris.filter { it.startsWith("tel:") }
else
contact.uris
if (uris.size > 1) {
CustomElements.selectItems.value = uris
CustomElements.selectItemAction.value = { index: Int ->
intent.putExtra("peer", uris[index])
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
}
CustomElements.showSelectItemDialog.value = false
}
CustomElements.showSelectItemDialog.value = true
} else if (uris.size == 1) {
intent.putExtra("peer", uris[0])
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
}
}
}
}
lastText.value = ctx.getString(R.string.send_message)
lastAction.value = {
if (ua != null) {
if (ua.account.isMobile) {
if (!Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
} else {
val uris = if (ua.account.isMobile)
contact.uris.filter { it.startsWith("tel:") }
else
contact.uris
if (uris.size > 1) {
CustomElements.selectItems.value = uris
CustomElements.selectItemAction.value = { index: Int ->
intent.putExtra("peer", uris[index])
if (ua.account.isMobile) {
if (!Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
} else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
}
else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
CustomElements.showSelectItemDialog.value = false
}
CustomElements.showSelectItemDialog.value = true
} else if (uris.size == 1) {
intent.putExtra("peer", uris[0])
if (ua.account.isMobile) {
if (!Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
} else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
}
else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
}
else {
handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp()
}
}
}
showDialog.value = true

View File

@ -82,6 +82,10 @@ import androidx.compose.ui.window.DialogProperties
object CustomElements {
val selectItems = mutableStateOf(listOf<String>())
val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
val showSelectItemDialog = mutableStateOf(false)
@Composable
fun Button(
onClick: () -> Unit,
@ -424,7 +428,7 @@ object CustomElements {
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
@ -449,8 +453,8 @@ object CustomElements {
modifier = Modifier.fillMaxWidth()
) {
Text(
text = item,
color = MaterialTheme.colorScheme.onSurface,
text = stringResource(R.string.bullet_item, item),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)

View File

@ -6,7 +6,6 @@ import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.app.KeyguardManager
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
@ -180,9 +179,9 @@ private val showPasswordsDialog = mutableStateOf(false)
private var passwordAccounts = mutableListOf<String>()
private var password = mutableStateOf("")
private val selectItems = mutableStateOf(listOf<String>())
private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
private val showSelectItemDialog = mutableStateOf(false)
private val selectItems = CustomElements.selectItems
private val selectItemAction = CustomElements.selectItemAction
private val showSelectItemDialog = CustomElements.showSelectItemDialog
fun NavGraphBuilder.mainScreenRoute(
navController: NavController,