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:
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user