Added email field to baresip contact

Added possibility to send email to contact and call or chat peer
Added possibility to import contacts from vcf file to baresip contacts
This commit is contained in:
Juha Heinanen
2026-05-18 12:04:52 +03:00
parent aa0098b16b
commit 8f95b3609c
10 changed files with 415 additions and 97 deletions

View File

@ -33,13 +33,15 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
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.material.icons.filled.Remove
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -111,6 +113,7 @@ private data class ScreenState(
val newId: Long = 0,
val name: String = "",
val uris: List<String> = emptyList(),
val email: String = "",
val color: Int = 0,
val avatarImageUri: String? = null,
val tmpAvatarFile: File? = null,
@ -141,6 +144,7 @@ private fun ContactScreen(
new = true,
name = "",
uris = if (uriOrNameArg == "") emptyList() else listOf(uriOrNameArg),
email = "",
favorite = false,
android = BaresipService.contactsMode == "android",
color = Utils.randomColor(),
@ -156,6 +160,7 @@ private fun ContactScreen(
new = false,
name = uriOrNameArg,
uris = contact.uris,
email = contact.email,
favorite = contact.favorite,
android = false,
color = contact.color,
@ -186,8 +191,10 @@ private fun ContactScreen(
currentState = screenState,
uriOrNameArg = uriOrNameArg,
)
if (result)
if (result) {
navController.previousBackStackEntry?.savedStateHandle?.set("scrollToContact", screenState.name)
navController.navigateUp()
}
}
BackHandler(enabled = true) {
@ -279,12 +286,14 @@ private fun ContactContent(
)
}
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(contentPadding)
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Avatar(
@ -326,6 +335,10 @@ private fun ContactContent(
uris = screenState.uris,
onUrisChange = { newUris -> onStateChange(screenState.copy(uris = newUris)) }
)
ContactEmail(
email = screenState.email,
onEmailChange = { newEmail -> onStateChange(screenState.copy(email = newEmail)) }
)
Favorite(
ctx = ctx,
favorite = screenState.favorite,
@ -491,8 +504,8 @@ private fun ContactUris(uris: List<String>, onUrisChange: (List<String>) -> Unit
modifier = Modifier.padding(start = 4.dp)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete",
imageVector = Icons.Filled.Remove,
contentDescription = "Remove",
tint = MaterialTheme.colorScheme.error
)
}
@ -516,6 +529,19 @@ private fun ContactUris(uris: List<String>, onUrisChange: (List<String>) -> Unit
}
}
@Composable
private fun ContactEmail(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
placeholder = { Text(stringResource(R.string.email)) },
onValueChange = onEmailChange,
modifier = Modifier.fillMaxWidth(),
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp),
label = { Text(stringResource(R.string.email)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
)
}
@Composable
private fun Favorite(ctx: Context, favorite: Boolean, onFavoriteChange: (Boolean) -> Unit) {
Row(
@ -633,6 +659,7 @@ private fun checkOnClick(
Contact.BaresipContact(
newName,
newUris,
currentState.email.trim(),
currentState.color,
idToUse,
currentState.favorite
@ -743,6 +770,16 @@ 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())
if (contact.email.isNotEmpty()) {
ops.add(
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.Email.ADDRESS, contact.email)
.withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_HOME)
.build())
}
for (uri in contact.uris) {
val mimeType = if (uri.startsWith("sip:"))
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
@ -779,6 +816,10 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
}
private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) {
if (contact.email.isNotEmpty()) {
if (updateAndroidEmail(ctx, rawContactId, contact.email) == 0)
addAndroidEmail(ctx, rawContactId, contact.email)
}
for (uri in contact.uris)
if (updateAndroidUri(ctx, rawContactId, uri) == 0)
addAndroidUri(ctx, rawContactId, uri)
@ -843,6 +884,36 @@ private fun addAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap)
}
}
private fun addAndroidEmail(ctx: Context, rawContactId: Long, email: String) {
val ops = ArrayList<ContentProviderOperation>()
ops.add(
ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValue(Data.RAW_CONTACT_ID, rawContactId)
.withValue(Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.Email.ADDRESS, email)
.withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_HOME)
.build())
try {
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
} catch (e: Exception) {
Log.e(TAG, "Adding of Android email failed: ${e.message}")
}
}
private fun updateAndroidEmail(ctx: Context, rawContactId: Long, email: String): Int {
val contentValues = ContentValues()
contentValues.put(CommonDataKinds.Email.ADDRESS, email)
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Email.CONTENT_ITEM_TYPE}'"
return try {
ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
} catch (e: Exception) {
Log.e(TAG, "updateAndroidEmail failed: ${e.message}")
0
}
}
private fun updateAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap?): Int {
val photoBytes = if (photoBits == null)
null

View File

@ -71,6 +71,7 @@ import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import coil.compose.AsyncImage
import androidx.core.net.toUri
import com.tutpro.baresip.CustomElements.AlertDialog
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
import com.tutpro.baresip.CustomElements.verticalScrollbar
@ -279,6 +280,8 @@ private fun Calls(
val message = remember { mutableStateOf("") }
val secondButtonText = remember { mutableStateOf("") }
val secondAction = remember { mutableStateOf({}) }
val thirdButtonText = remember { mutableStateOf("") }
val thirdAction = remember { mutableStateOf({}) }
val lastButtonText = remember { mutableStateOf("") }
val lastAction = remember { mutableStateOf({}) }
@ -289,6 +292,8 @@ private fun Calls(
firstButtonText = stringResource(R.string.cancel),
secondButtonText = secondButtonText.value,
onSecondClicked = secondAction.value,
thirdButtonText = thirdButtonText.value,
onThirdClicked = thirdAction.value,
lastButtonText = lastButtonText.value,
onLastClicked = lastAction.value,
)
@ -345,11 +350,20 @@ private fun Calls(
else
Log.w(TAG, "onClickListener did not find UA for $aor")
val peerName = Utils.friendlyUri(ctx, peerUri, account)
message.value = String.format(ctx.getString(R.string.contact_action_question), peerName)
val contact = Contact.findContact(peerUri)
if (contact is Contact.BaresipContact && contact.email.isNotEmpty())
message.value = String.format(
ctx.getString(R.string.contact_email_action_question),
peerName
)
else
message.value = String.format(
ctx.getString(R.string.contact_action_question),
peerName
)
secondButtonText.value = ctx.getString(R.string.call)
secondAction.value = {
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:") }
@ -381,10 +395,9 @@ private fun Calls(
}
}
}
lastButtonText.value = ctx.getString(R.string.send_message)
lastAction.value = {
thirdButtonText.value = ctx.getString(R.string.send_message)
thirdAction.value = {
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:") }
@ -432,6 +445,21 @@ private fun Calls(
}
}
}
if (contact is Contact.BaresipContact && contact.email.isNotEmpty()) {
lastButtonText.value = ctx.getString(R.string.send_email)
lastAction.value = {
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:${contact.email}".toUri()
}
try {
ctx.startActivity(emailIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to start email activity: ${e.message}")
}
}
} else {
lastButtonText.value = ""
}
showDialog.value = true
},
onLongClick = {

View File

@ -37,6 +37,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.outlined.Call
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@ -74,6 +75,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.Observer
@ -216,7 +218,6 @@ private fun TopAppBar(
peerUri: String
) {
val aor = account.aor
TopAppBar(
title = {
Text(
@ -245,10 +246,10 @@ private fun TopAppBar(
onClick = {
val ua = UserAgent.ofAor(account.aor)
if (ua != null) {
val intent = Intent(ctx, MainActivity::class.java)
intent.putExtra("uap", ua.uap)
intent.putExtra("peer", peerUri)
handleIntent(ctx, viewModel, intent, "call")
val callIntent = Intent(ctx, MainActivity::class.java)
.putExtra("uap", ua.uap)
.putExtra("peer", peerUri)
handleIntent(ctx, viewModel, callIntent, "call")
navController.navigate("main") {
popUpTo("main")
launchSingleTop = true
@ -263,6 +264,34 @@ private fun TopAppBar(
contentDescription = "Call",
)
}
val contact = Contact.findContact(peerUri)
if (contact != null && contact is Contact.BaresipContact && contact.email.isNotEmpty())
IconButton(
onClick = {
val ua = UserAgent.ofAor(account.aor)
if (ua != null) {
val contact = Contact.findContact(peerUri)
if (contact != null && contact is Contact.BaresipContact &&
contact.email.isNotEmpty()) {
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:${contact.email}".toUri()
}
try {
ctx.startActivity(emailIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to start email activity: ${e.message}")
}
}
}
else
Log.w(TAG, "Call button onClick listener did not find UA for $aor")
}
) {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = "Email",
)
}
}
)
}

View File

@ -433,15 +433,19 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
else
chatPeer
val uri = if (Utils.isTelUri(peerUri)) {
if (account.telProvider == "") {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value =
String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
showAlert.value = true
""
} else
Utils.telToSip(peerUri, account)
} else
if (account.isMobile)
peerUri
else
if (account.telProvider == "") {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value =
String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
showAlert.value = true
""
} else
Utils.telToSip(peerUri, account)
}
else
Utils.uriComplete(peerUri, account.aor)
if (alertMessage.value.isEmpty()) {
if (!Utils.checkUri(uri)) {
@ -589,7 +593,7 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
showSuggestions = false
val peerText = newPeer.trim()
if (peerText.isNotEmpty()) {
val uris = Contact.contactUris(peerText)
val uris = Contact.contactUris(peerText, account.isMobile)
if (uris.isEmpty())
makeChat(ctx, navController, account, peerText)
else if (uris.size == 1)

View File

@ -13,8 +13,8 @@ import java.util.ArrayList
sealed class Contact {
class BaresipContact(var name: String, val uris: ArrayList<String>, var color: Int, var id: Long,
var favorite: Boolean): Contact() {
class BaresipContact(var name: String, val uris: ArrayList<String>, var email: 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, ArrayList(uris), color, id, favorite)
BaresipContact(name, ArrayList(uris), email, color, id, favorite)
is AndroidContact ->
AndroidContact(name, color, thumbnailUri, id, favorite)
}
@ -101,16 +101,17 @@ sealed class Contact {
}
// Return URIs of contact name
fun contactUris(name: String): ArrayList<String> {
fun contactUris(name: String, tel: Boolean = false): ArrayList<String> {
val uris = ArrayList<String>()
for (c in BaresipService.contacts)
when (c) {
is BaresipContact -> {
if (c.name.equals(name, ignoreCase = true)) {
for (u in c.uris)
uris.add(u.removePrefix("<")
.replaceAfter(">", "")
.replace(">", ""))
for (u in c.uris) {
if (tel && !u.startsWith("tel:"))
continue
uris.add(u)
}
return uris
}
}
@ -155,8 +156,8 @@ sealed class Contact {
val avatarFiles = avatarFileNames()
var contents = ""
for (c in BaresipService.baresipContacts.value) {
contents += "\"${c.name}\" <${c.uris.joinToString(",")}>;id=${c.id};color=${c.color}" +
";favorite=${if (c.favorite) "yes" else "no"}\n"
contents += "\"${c.name}\" <${c.uris.joinToString(",")}>;email=${c.email};id=${c.id}" +
";color=${c.color};favorite=${if (c.favorite) "yes" else "no"}\n"
avatarFiles.remove(c.id.toString() + ".png")
}
Utils.putFileContents(BaresipService.filesPath + "/contacts",
@ -166,8 +167,8 @@ sealed class Contact {
}
fun loadAndroidContacts(ctx: Context) {
// If phone type is needed, add DATA2 to projection. Then phone type can be get from
// cursor using getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE
// If phone type is needed, add DATA2 to projection. Then phone type can be got from
// cursor using getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)
val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1,
/* ContactsContract.Data.DATA2 ,*/ ContactsContract.Data.PHOTO_THUMBNAIL_URI,
@ -241,6 +242,7 @@ sealed class Contact {
val urisPart = uriParams.substringAfter("<").substringBefore(">")
val uris = ArrayList(urisPart.split(","))
val params = uriParams.substringAfter(">;")
val email = Utils.paramValue(params, "email")
val colorValue = Utils.paramValue(params, "color" )
val color: Int = if (colorValue != "")
colorValue.toInt()
@ -253,7 +255,7 @@ sealed class Contact {
baseId + contactNo
val favorite = Utils.paramValue(params, "favorite" ) == "yes"
Log.d(TAG, "Restoring contact $name, $urisPart, $color, $id")
val contact = BaresipContact(name, uris, color, id, favorite)
val contact = BaresipContact(name, uris, email, color, id, favorite)
val avatarFilePath = BaresipService.filesPath + "/$id.png"
if (File(avatarFilePath).exists()) {
try {

View File

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.provider.ContactsContract
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@ -51,6 +52,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -82,6 +84,7 @@ import com.tutpro.baresip.CustomElements.TextAvatar
import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.io.File
import java.io.IOException
import androidx.core.net.toUri
const val avatarSize: Int = 96
@ -115,6 +118,103 @@ private fun ContactsScreen(
var expanded by remember { mutableStateOf(false) }
val both = stringResource(R.string.both)
val import = stringResource(R.string.import_contacts)
val vcfImportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri: android.net.Uri? ->
if (uri != null) {
try {
ctx.contentResolver.openInputStream(uri)?.use { inputStream ->
val reader = inputStream.bufferedReader()
var name = ""
var email = ""
val uris = mutableListOf<String>()
reader.forEachLine { line ->
when {
line.startsWith("BEGIN:VCARD", ignoreCase = true) -> {
name = ""
email = ""
uris.clear()
}
line.startsWith("FN:", ignoreCase = true) -> {
name = line.substring(3).trim()
}
line.startsWith("N:", ignoreCase = true) && name.isEmpty() -> {
name = line.substring(2).trim().replace(";", " ").trim()
}
line.startsWith("EMAIL", ignoreCase = true) -> {
if (email.isEmpty())
email = line.substringAfter(":").trim()
}
line.startsWith("TEL", ignoreCase = true) -> {
val value = line.substringAfter(":").trim()
val cleanValue = value.filterNot { setOf('-', ' ', '(', ')').contains(it) }
if (cleanValue.isNotEmpty()) {
val telUri = if (cleanValue.startsWith("tel:")) cleanValue else "tel:$cleanValue"
if (telUri !in uris) uris.add(telUri)
}
}
line.startsWith("X-SIP:", ignoreCase = true) -> {
val sipUri = line.substring(6).trim()
if (sipUri.isNotEmpty()) {
val fullSipUri = if (sipUri.startsWith("sip:")) sipUri else "sip:$sipUri"
if (fullSipUri !in uris) uris.add(fullSipUri)
}
}
line.startsWith("END:VCARD", ignoreCase = true) -> {
if (name.isNotEmpty() && uris.isNotEmpty()) {
val existingContact = Contact.baresipContact(name)
if (existingContact != null) {
var updated = false
for (u in uris) {
if (u !in existingContact.uris) {
existingContact.uris.add(u)
updated = true
}
}
if (existingContact.email.isEmpty() && email.isNotEmpty()) {
existingContact.email = email
updated = true
}
if (updated) {
Contact.updateBaresipContact(existingContact.id, existingContact)
}
} else {
Contact.addBaresipContact(
Contact.BaresipContact(
name,
ArrayList(uris),
email,
Utils.randomColor(),
System.currentTimeMillis(),
false
)
)
}
}
}
}
}
Contact.saveBaresipContacts()
Contact.contactsUpdate()
Toast.makeText(
ctx,
R.string.contact_import_success,
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import VCF: ${e.message}")
Toast.makeText(
ctx,
R.string.contact_import_failure,
Toast.LENGTH_SHORT
).show()
}
}
}
val contactNames = remember(BaresipService.contactsMode) {
val names = mutableListOf("baresip", "Android", both)
val values = listOf("baresip", "android", "both")
@ -264,9 +364,13 @@ private fun ContactsScreen(
CustomElements.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
items = contactNames,
items = contactNames + import,
onItemClick = { name ->
expanded = false
if (name == import) {
vcfImportLauncher.launch(arrayOf("text/vcard", "text/x-vcard"))
return@DropdownMenu
}
val mode = contactValues[contactNames.indexOf(name)]
val contactsPermissions = arrayOf(
Manifest.permission.READ_CONTACTS,
@ -404,6 +508,8 @@ private fun ContactsContent(
val secondAction = remember { mutableStateOf({}) }
val lastText = remember { mutableStateOf("") }
val lastAction = remember { mutableStateOf({}) }
val thirdText = remember { mutableStateOf("") }
val thirdAction = remember { mutableStateOf({}) }
if (showDialog.value)
AlertDialog(
@ -413,6 +519,8 @@ private fun ContactsContent(
firstButtonText = stringResource(R.string.cancel),
secondButtonText = secondText.value,
onSecondClicked = secondAction.value,
thirdButtonText = thirdText.value,
onThirdClicked = thirdAction.value,
lastButtonText = lastText.value,
onLastClicked = lastAction.value,
)
@ -430,10 +538,17 @@ private fun ContactsContent(
val lazyListState = rememberLazyListState()
val scrollToContact = navController.currentBackStackEntry
?.savedStateHandle
?.getLiveData<String>("scrollToContact")
?.observeAsState()
var lastSearchQuery by remember { mutableStateOf("") }
LaunchedEffect(searchQuery) {
if (searchQuery.isBlank()) {
if (searchQuery.isBlank() && lastSearchQuery.isNotBlank()) {
lazyListState.scrollToItem(0)
}
lastSearchQuery = searchQuery
}
val filteredContacts = remember(BaresipService.contacts, searchQuery) {
@ -453,6 +568,16 @@ private fun ContactsContent(
}
}
LaunchedEffect(scrollToContact?.value, filteredContacts) {
scrollToContact?.value?.let { name ->
val index = filteredContacts.indexOfFirst { it.first.name() == name }
if (index != -1) {
lazyListState.scrollToItem(index)
}
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("scrollToContact")
}
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
@ -521,10 +646,17 @@ private fun ContactsContent(
}
else
Log.w(TAG, "onClickListener did not find UA for $aor")
dialogMessage.value = String.format(
ctx.getString(R.string.contact_action_question),
contact.name()
)
val peerName = contact.name()
if (contact.email.isNotEmpty())
dialogMessage.value = String.format(
ctx.getString(R.string.contact_email_action_question),
peerName
)
else
dialogMessage.value = String.format(
ctx.getString(R.string.contact_action_question),
peerName
)
secondText.value = ctx.getString(R.string.call)
secondAction.value = {
if (ua != null) {
@ -555,8 +687,8 @@ private fun ContactsContent(
}
}
}
lastText.value = ctx.getString(R.string.send_message)
lastAction.value = {
thirdText.value = ctx.getString(R.string.send_message)
thirdAction.value = {
if (ua != null) {
val uris = if (ua.account.isMobile)
contact.uris.filter { it.startsWith("tel:") }
@ -603,6 +735,21 @@ private fun ContactsContent(
}
}
}
if (contact.email.isNotEmpty()) {
lastText.value = ctx.getString(R.string.send_email)
lastAction.value = {
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:${contact.email}".toUri()
}
try {
ctx.startActivity(emailIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to start email activity: ${e.message}")
}
}
} else {
lastText.value = ""
}
showDialog.value = true
},
onLongClick = {

View File

@ -304,11 +304,12 @@ object CustomElements {
)
Spacer(modifier = Modifier.height(16.dp))
if (lastButtonText.isNotEmpty()) {
val buttons = listOf(
firstButtonText, secondButtonText, thirdButtonText, lastButtonText
)
val buttonCount = buttons.count { it.isNotEmpty() }
val buttonCount = listOf(
firstButtonText, secondButtonText, thirdButtonText, lastButtonText
).count { it.isNotEmpty() }
if (buttonCount > 0) {
if (buttonCount >= 3) {
// Use a Column for 3-4 buttons, aligned to the end (right)
@ -316,26 +317,28 @@ object CustomElements {
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
TextButton(onClick = {
onFirstClicked()
showDialog.value = false
}) {
Text(
text = firstButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
TextButton(onClick = {
onSecondClicked()
showDialog.value = false
}) {
Text(
text = secondButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary,
)
}
if (firstButtonText.isNotEmpty())
TextButton(onClick = {
onFirstClicked()
showDialog.value = false
}) {
Text(
text = firstButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (secondButtonText.isNotEmpty())
TextButton(onClick = {
onSecondClicked()
showDialog.value = false
}) {
Text(
text = secondButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary,
)
}
if (thirdButtonText.isNotEmpty())
TextButton(onClick = {
onThirdClicked()
@ -347,16 +350,17 @@ object CustomElements {
color = MaterialTheme.colorScheme.primary,
)
}
TextButton(onClick = {
onLastClicked()
showDialog.value = false
}) {
Text(
text = lastButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary
)
}
if (lastButtonText.isNotEmpty())
TextButton(onClick = {
onLastClicked()
showDialog.value = false
}) {
Text(
text = lastButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary
)
}
}
} else {
// Use the existing Row for 1 or 2 buttons
@ -386,16 +390,28 @@ object CustomElements {
color = MaterialTheme.colorScheme.primary
)
}
TextButton(onClick = {
onLastClicked()
showDialog.value = false
}) {
Text(
text = lastButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary
)
}
if (thirdButtonText.isNotEmpty())
TextButton(onClick = {
onThirdClicked()
showDialog.value = false
}) {
Text(
text = thirdButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary
)
}
if (lastButtonText.isNotEmpty())
TextButton(onClick = {
onLastClicked()
showDialog.value = false
}) {
Text(
text = lastButtonText.uppercase(),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}

View File

@ -1406,13 +1406,19 @@ object Utils {
fun sendSms(ctx: Context, destination: String, message: String): Boolean {
return try {
val smsManager = if (Build.VERSION.SDK_INT >= 31) {
val smsManager = if (Build.VERSION.SDK_INT >= 31)
ctx.getSystemService(android.telephony.SmsManager::class.java)
} else {
else
@Suppress("DEPRECATION")
android.telephony.SmsManager.getDefault()
}
smsManager.sendTextMessage(destination, null, message, null, null)
smsManager.sendTextMessage(
destination,
null,
message,
null,
null
)
Log.d(TAG, "Sent SMS message to $destination")
true
} catch (e: Exception) {
Log.e(TAG, "Failed to send SMS: ${e.message}")

View File

@ -483,21 +483,30 @@
<!-- Contact Activity -->
<string name="new_contact">Uusi yhteystieto</string>
<string name="contact_name">Nimi</string>
<string name="email">Sähköposti</string>
<string name="sip_or_tel_uri">SIP tai tel URI</string>
<string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string>
<string name="favorite">Suosikki</string>
<string name="favorite_help">Jos ruksattu, kontakti näytetään muiden suosikkien kanssa yhteystietolistan alussa.</string>
<string name="favorite_help">Jos ruksattu, kontakti näytetään muiden suosikkien kanssa
yhteystietolistan alussa.</string>
<string name="invalid_contact">Virheellinen yhteystiedon nimi \'%1$s\'</string>
<string name="contact_already_exists">Yhteystieto \'%1$s\' on jo olemassa.</string>
<string name="android_contact_help">Jos merkitty, tämä yhteystieto lisätään Androidin yhteystietoihin.</string>
<string name="avatar_image">Profiilikuva</string>
<!-- Contacts Activity -->
<string name="contacts">Yhteystiedot</string>
<string name="contact_action_question">Haluatko soittaa tai lähettää
viestin ositteeseen \'%1$s\'\?</string>
<string name="contact_action_question">Haluatko soittaa tai lähettää viestin
osoitteeseen \'%1$s\'\?</string>
<string name="contact_email_action_question">Haluatko soittaa tai lähettää viestin tai sähköpostin
osoitteeseen \'%1$s\'\?</string>
<string name="send_message">Lähetä viesti</string>
<string name="send_email">Lähetä sähköposti</string>
<string name="contact_delete_question">Haluatko poistaa yhteystiedon \'%1$s\'\?</string>
<string name="search">Etsi</string>
<string name="import_contacts">Tuo</string>
<string name="contact_import_success">Yhteystietojen tuonti onnistui</string>
<string name="contact_import_failure">Yhteystietojen tuonti epäonnistui</string>
<!-- Generic -->
<string name="alert">Varoitus</string>
<string name="info">Tieto</string>

View File

@ -456,6 +456,7 @@
<!-- Contact Activity -->
<string name="new_contact">New Contact</string>
<string name="contact_name">Name</string>
<string name="email">Email</string>
<string name="sip_or_tel_uri">SIP or tel URI</string>
<string name="user_domain_or_number">user@domain or telephone number</string>
<string name="favorite">Favorite</string>
@ -468,9 +469,14 @@
<!-- Contacts Activity -->
<string name="contacts">Contacts</string>
<string name="contact_action_question">Do you want to call or send message to \'%1$s\'\?</string>
<string name="contact_email_action_question">Do you want to call, send message, or email to \'%1$s\'\?</string>
<string name="send_message">Send Message</string>
<string name="send_email">Send Email</string>
<string name="contact_delete_question">Do you want to delete contact \'%1$s\'\?</string>
<string name="search">Search</string>
<string name="import_contacts">Import</string>
<string name="contact_import_success">Contacts imported successfully</string>
<string name="contact_import_failure">Failed to import contacts</string>
<!-- Generic -->
<string name="alert">Alert</string>
<string name="info">Info</string>