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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
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.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -111,6 +113,7 @@ private data class ScreenState(
|
|||||||
val newId: Long = 0,
|
val newId: Long = 0,
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val uris: List<String> = emptyList(),
|
val uris: List<String> = emptyList(),
|
||||||
|
val email: String = "",
|
||||||
val color: Int = 0,
|
val color: Int = 0,
|
||||||
val avatarImageUri: String? = null,
|
val avatarImageUri: String? = null,
|
||||||
val tmpAvatarFile: File? = null,
|
val tmpAvatarFile: File? = null,
|
||||||
@@ -141,6 +144,7 @@ private fun ContactScreen(
|
|||||||
new = true,
|
new = true,
|
||||||
name = "",
|
name = "",
|
||||||
uris = if (uriOrNameArg == "") emptyList() else listOf(uriOrNameArg),
|
uris = if (uriOrNameArg == "") emptyList() else listOf(uriOrNameArg),
|
||||||
|
email = "",
|
||||||
favorite = false,
|
favorite = false,
|
||||||
android = BaresipService.contactsMode == "android",
|
android = BaresipService.contactsMode == "android",
|
||||||
color = Utils.randomColor(),
|
color = Utils.randomColor(),
|
||||||
@@ -156,6 +160,7 @@ private fun ContactScreen(
|
|||||||
new = false,
|
new = false,
|
||||||
name = uriOrNameArg,
|
name = uriOrNameArg,
|
||||||
uris = contact.uris,
|
uris = contact.uris,
|
||||||
|
email = contact.email,
|
||||||
favorite = contact.favorite,
|
favorite = contact.favorite,
|
||||||
android = false,
|
android = false,
|
||||||
color = contact.color,
|
color = contact.color,
|
||||||
@@ -186,8 +191,10 @@ private fun ContactScreen(
|
|||||||
currentState = screenState,
|
currentState = screenState,
|
||||||
uriOrNameArg = uriOrNameArg,
|
uriOrNameArg = uriOrNameArg,
|
||||||
)
|
)
|
||||||
if (result)
|
if (result) {
|
||||||
|
navController.previousBackStackEntry?.savedStateHandle?.set("scrollToContact", screenState.name)
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = true) {
|
BackHandler(enabled = true) {
|
||||||
@@ -279,12 +286,14 @@ private fun ContactContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.padding(contentPadding)
|
.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),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
@@ -326,6 +335,10 @@ private fun ContactContent(
|
|||||||
uris = screenState.uris,
|
uris = screenState.uris,
|
||||||
onUrisChange = { newUris -> onStateChange(screenState.copy(uris = newUris)) }
|
onUrisChange = { newUris -> onStateChange(screenState.copy(uris = newUris)) }
|
||||||
)
|
)
|
||||||
|
ContactEmail(
|
||||||
|
email = screenState.email,
|
||||||
|
onEmailChange = { newEmail -> onStateChange(screenState.copy(email = newEmail)) }
|
||||||
|
)
|
||||||
Favorite(
|
Favorite(
|
||||||
ctx = ctx,
|
ctx = ctx,
|
||||||
favorite = screenState.favorite,
|
favorite = screenState.favorite,
|
||||||
@@ -491,8 +504,8 @@ private fun ContactUris(uris: List<String>, onUrisChange: (List<String>) -> Unit
|
|||||||
modifier = Modifier.padding(start = 4.dp)
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Delete,
|
imageVector = Icons.Filled.Remove,
|
||||||
contentDescription = "Delete",
|
contentDescription = "Remove",
|
||||||
tint = MaterialTheme.colorScheme.error
|
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
|
@Composable
|
||||||
private fun Favorite(ctx: Context, favorite: Boolean, onFavoriteChange: (Boolean) -> Unit) {
|
private fun Favorite(ctx: Context, favorite: Boolean, onFavoriteChange: (Boolean) -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
@@ -633,6 +659,7 @@ private fun checkOnClick(
|
|||||||
Contact.BaresipContact(
|
Contact.BaresipContact(
|
||||||
newName,
|
newName,
|
||||||
newUris,
|
newUris,
|
||||||
|
currentState.email.trim(),
|
||||||
currentState.color,
|
currentState.color,
|
||||||
idToUse,
|
idToUse,
|
||||||
currentState.favorite
|
currentState.favorite
|
||||||
@@ -743,6 +770,16 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
|
|||||||
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||||
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
||||||
.build())
|
.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) {
|
for (uri in contact.uris) {
|
||||||
val mimeType = if (uri.startsWith("sip:"))
|
val mimeType = if (uri.startsWith("sip:"))
|
||||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
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) {
|
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)
|
for (uri in contact.uris)
|
||||||
if (updateAndroidUri(ctx, rawContactId, uri) == 0)
|
if (updateAndroidUri(ctx, rawContactId, uri) == 0)
|
||||||
addAndroidUri(ctx, rawContactId, uri)
|
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 {
|
private fun updateAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap?): Int {
|
||||||
val photoBytes = if (photoBits == null)
|
val photoBytes = if (photoBits == null)
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import androidx.navigation.NavType
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||||
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
||||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||||
@@ -279,6 +280,8 @@ private fun Calls(
|
|||||||
val message = remember { mutableStateOf("") }
|
val message = remember { mutableStateOf("") }
|
||||||
val secondButtonText = remember { mutableStateOf("") }
|
val secondButtonText = remember { mutableStateOf("") }
|
||||||
val secondAction = remember { mutableStateOf({}) }
|
val secondAction = remember { mutableStateOf({}) }
|
||||||
|
val thirdButtonText = remember { mutableStateOf("") }
|
||||||
|
val thirdAction = remember { mutableStateOf({}) }
|
||||||
val lastButtonText = remember { mutableStateOf("") }
|
val lastButtonText = remember { mutableStateOf("") }
|
||||||
val lastAction = remember { mutableStateOf({}) }
|
val lastAction = remember { mutableStateOf({}) }
|
||||||
|
|
||||||
@@ -289,6 +292,8 @@ private fun Calls(
|
|||||||
firstButtonText = stringResource(R.string.cancel),
|
firstButtonText = stringResource(R.string.cancel),
|
||||||
secondButtonText = secondButtonText.value,
|
secondButtonText = secondButtonText.value,
|
||||||
onSecondClicked = secondAction.value,
|
onSecondClicked = secondAction.value,
|
||||||
|
thirdButtonText = thirdButtonText.value,
|
||||||
|
onThirdClicked = thirdAction.value,
|
||||||
lastButtonText = lastButtonText.value,
|
lastButtonText = lastButtonText.value,
|
||||||
onLastClicked = lastAction.value,
|
onLastClicked = lastAction.value,
|
||||||
)
|
)
|
||||||
@@ -345,11 +350,20 @@ private fun Calls(
|
|||||||
else
|
else
|
||||||
Log.w(TAG, "onClickListener did not find UA for $aor")
|
Log.w(TAG, "onClickListener did not find UA for $aor")
|
||||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
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)
|
secondButtonText.value = ctx.getString(R.string.call)
|
||||||
secondAction.value = {
|
secondAction.value = {
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
val contact = Contact.findContact(peerUri)
|
|
||||||
val uris = if (contact is Contact.BaresipContact) {
|
val uris = if (contact is Contact.BaresipContact) {
|
||||||
if (ua.account.isMobile)
|
if (ua.account.isMobile)
|
||||||
contact.uris.filter { it.startsWith("tel:") }
|
contact.uris.filter { it.startsWith("tel:") }
|
||||||
@@ -381,10 +395,9 @@ private fun Calls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastButtonText.value = ctx.getString(R.string.send_message)
|
thirdButtonText.value = ctx.getString(R.string.send_message)
|
||||||
lastAction.value = {
|
thirdAction.value = {
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
val contact = Contact.findContact(peerUri)
|
|
||||||
val uris = if (contact is Contact.BaresipContact) {
|
val uris = if (contact is Contact.BaresipContact) {
|
||||||
if (ua.account.isMobile)
|
if (ua.account.isMobile)
|
||||||
contact.uris.filter { it.startsWith("tel:") }
|
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
|
showDialog.value = true
|
||||||
},
|
},
|
||||||
onLongClick = {
|
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.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.outlined.Call
|
import androidx.compose.material.icons.outlined.Call
|
||||||
import androidx.compose.material.icons.outlined.Clear
|
import androidx.compose.material.icons.outlined.Clear
|
||||||
|
import androidx.compose.material.icons.outlined.Email
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
@@ -216,7 +218,6 @@ private fun TopAppBar(
|
|||||||
peerUri: String
|
peerUri: String
|
||||||
) {
|
) {
|
||||||
val aor = account.aor
|
val aor = account.aor
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -245,10 +246,10 @@ private fun TopAppBar(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val ua = UserAgent.ofAor(account.aor)
|
val ua = UserAgent.ofAor(account.aor)
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
val intent = Intent(ctx, MainActivity::class.java)
|
val callIntent = Intent(ctx, MainActivity::class.java)
|
||||||
intent.putExtra("uap", ua.uap)
|
.putExtra("uap", ua.uap)
|
||||||
intent.putExtra("peer", peerUri)
|
.putExtra("peer", peerUri)
|
||||||
handleIntent(ctx, viewModel, intent, "call")
|
handleIntent(ctx, viewModel, callIntent, "call")
|
||||||
navController.navigate("main") {
|
navController.navigate("main") {
|
||||||
popUpTo("main")
|
popUpTo("main")
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
@@ -263,6 +264,34 @@ private fun TopAppBar(
|
|||||||
contentDescription = "Call",
|
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
|
else
|
||||||
chatPeer
|
chatPeer
|
||||||
val uri = if (Utils.isTelUri(peerUri)) {
|
val uri = if (Utils.isTelUri(peerUri)) {
|
||||||
if (account.telProvider == "") {
|
if (account.isMobile)
|
||||||
alertTitle.value = ctx.getString(R.string.notice)
|
peerUri
|
||||||
alertMessage.value =
|
else
|
||||||
String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
|
if (account.telProvider == "") {
|
||||||
showAlert.value = true
|
alertTitle.value = ctx.getString(R.string.notice)
|
||||||
""
|
alertMessage.value =
|
||||||
} else
|
String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
|
||||||
Utils.telToSip(peerUri, account)
|
showAlert.value = true
|
||||||
} else
|
""
|
||||||
|
} else
|
||||||
|
Utils.telToSip(peerUri, account)
|
||||||
|
}
|
||||||
|
else
|
||||||
Utils.uriComplete(peerUri, account.aor)
|
Utils.uriComplete(peerUri, account.aor)
|
||||||
if (alertMessage.value.isEmpty()) {
|
if (alertMessage.value.isEmpty()) {
|
||||||
if (!Utils.checkUri(uri)) {
|
if (!Utils.checkUri(uri)) {
|
||||||
@@ -589,7 +593,7 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
|
|||||||
showSuggestions = false
|
showSuggestions = false
|
||||||
val peerText = newPeer.trim()
|
val peerText = newPeer.trim()
|
||||||
if (peerText.isNotEmpty()) {
|
if (peerText.isNotEmpty()) {
|
||||||
val uris = Contact.contactUris(peerText)
|
val uris = Contact.contactUris(peerText, account.isMobile)
|
||||||
if (uris.isEmpty())
|
if (uris.isEmpty())
|
||||||
makeChat(ctx, navController, account, peerText)
|
makeChat(ctx, navController, account, peerText)
|
||||||
else if (uris.size == 1)
|
else if (uris.size == 1)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import java.util.ArrayList
|
|||||||
|
|
||||||
sealed class Contact {
|
sealed class Contact {
|
||||||
|
|
||||||
class BaresipContact(var name: String, val uris: ArrayList<String>, var color: Int, var id: Long,
|
class BaresipContact(var name: String, val uris: ArrayList<String>, var email: String, var color: Int,
|
||||||
var favorite: Boolean): Contact() {
|
var id: Long, var favorite: Boolean): Contact() {
|
||||||
var avatarImage: Bitmap? = null
|
var avatarImage: Bitmap? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ sealed class Contact {
|
|||||||
fun copy(): Contact {
|
fun copy(): Contact {
|
||||||
val copy = when (this) {
|
val copy = when (this) {
|
||||||
is BaresipContact ->
|
is BaresipContact ->
|
||||||
BaresipContact(name, ArrayList(uris), color, id, favorite)
|
BaresipContact(name, ArrayList(uris), email, color, id, favorite)
|
||||||
is AndroidContact ->
|
is AndroidContact ->
|
||||||
AndroidContact(name, color, thumbnailUri, id, favorite)
|
AndroidContact(name, color, thumbnailUri, id, favorite)
|
||||||
}
|
}
|
||||||
@@ -101,16 +101,17 @@ sealed class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return URIs of contact name
|
// Return URIs of contact name
|
||||||
fun contactUris(name: String): ArrayList<String> {
|
fun contactUris(name: String, tel: Boolean = false): ArrayList<String> {
|
||||||
val uris = ArrayList<String>()
|
val uris = ArrayList<String>()
|
||||||
for (c in BaresipService.contacts)
|
for (c in BaresipService.contacts)
|
||||||
when (c) {
|
when (c) {
|
||||||
is BaresipContact -> {
|
is BaresipContact -> {
|
||||||
if (c.name.equals(name, ignoreCase = true)) {
|
if (c.name.equals(name, ignoreCase = true)) {
|
||||||
for (u in c.uris)
|
for (u in c.uris) {
|
||||||
uris.add(u.removePrefix("<")
|
if (tel && !u.startsWith("tel:"))
|
||||||
.replaceAfter(">", "")
|
continue
|
||||||
.replace(">", ""))
|
uris.add(u)
|
||||||
|
}
|
||||||
return uris
|
return uris
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,8 +156,8 @@ sealed class Contact {
|
|||||||
val avatarFiles = avatarFileNames()
|
val avatarFiles = avatarFileNames()
|
||||||
var contents = ""
|
var contents = ""
|
||||||
for (c in BaresipService.baresipContacts.value) {
|
for (c in BaresipService.baresipContacts.value) {
|
||||||
contents += "\"${c.name}\" <${c.uris.joinToString(",")}>;id=${c.id};color=${c.color}" +
|
contents += "\"${c.name}\" <${c.uris.joinToString(",")}>;email=${c.email};id=${c.id}" +
|
||||||
";favorite=${if (c.favorite) "yes" else "no"}\n"
|
";color=${c.color};favorite=${if (c.favorite) "yes" else "no"}\n"
|
||||||
avatarFiles.remove(c.id.toString() + ".png")
|
avatarFiles.remove(c.id.toString() + ".png")
|
||||||
}
|
}
|
||||||
Utils.putFileContents(BaresipService.filesPath + "/contacts",
|
Utils.putFileContents(BaresipService.filesPath + "/contacts",
|
||||||
@@ -166,8 +167,8 @@ sealed class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadAndroidContacts(ctx: Context) {
|
fun loadAndroidContacts(ctx: Context) {
|
||||||
// If phone type is needed, add DATA2 to projection. Then phone type can be get from
|
// If phone type is needed, add DATA2 to projection. Then phone type can be got from
|
||||||
// cursor using getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE
|
// cursor using getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)
|
||||||
val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME,
|
val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME,
|
||||||
ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1,
|
ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1,
|
||||||
/* ContactsContract.Data.DATA2 ,*/ ContactsContract.Data.PHOTO_THUMBNAIL_URI,
|
/* ContactsContract.Data.DATA2 ,*/ ContactsContract.Data.PHOTO_THUMBNAIL_URI,
|
||||||
@@ -241,6 +242,7 @@ sealed class Contact {
|
|||||||
val urisPart = uriParams.substringAfter("<").substringBefore(">")
|
val urisPart = uriParams.substringAfter("<").substringBefore(">")
|
||||||
val uris = ArrayList(urisPart.split(","))
|
val uris = ArrayList(urisPart.split(","))
|
||||||
val params = uriParams.substringAfter(">;")
|
val params = uriParams.substringAfter(">;")
|
||||||
|
val email = Utils.paramValue(params, "email")
|
||||||
val colorValue = Utils.paramValue(params, "color" )
|
val colorValue = Utils.paramValue(params, "color" )
|
||||||
val color: Int = if (colorValue != "")
|
val color: Int = if (colorValue != "")
|
||||||
colorValue.toInt()
|
colorValue.toInt()
|
||||||
@@ -253,7 +255,7 @@ sealed class Contact {
|
|||||||
baseId + contactNo
|
baseId + contactNo
|
||||||
val favorite = Utils.paramValue(params, "favorite" ) == "yes"
|
val favorite = Utils.paramValue(params, "favorite" ) == "yes"
|
||||||
Log.d(TAG, "Restoring contact $name, $urisPart, $color, $id")
|
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"
|
val avatarFilePath = BaresipService.filesPath + "/$id.png"
|
||||||
if (File(avatarFilePath).exists()) {
|
if (File(avatarFilePath).exists()) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -51,6 +52,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -82,6 +84,7 @@ import com.tutpro.baresip.CustomElements.TextAvatar
|
|||||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
const val avatarSize: Int = 96
|
const val avatarSize: Int = 96
|
||||||
|
|
||||||
@@ -115,6 +118,103 @@ private fun ContactsScreen(
|
|||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
val both = stringResource(R.string.both)
|
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 contactNames = remember(BaresipService.contactsMode) {
|
||||||
val names = mutableListOf("baresip", "Android", both)
|
val names = mutableListOf("baresip", "Android", both)
|
||||||
val values = listOf("baresip", "android", "both")
|
val values = listOf("baresip", "android", "both")
|
||||||
@@ -264,9 +364,13 @@ private fun ContactsScreen(
|
|||||||
CustomElements.DropdownMenu(
|
CustomElements.DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
items = contactNames,
|
items = contactNames + import,
|
||||||
onItemClick = { name ->
|
onItemClick = { name ->
|
||||||
expanded = false
|
expanded = false
|
||||||
|
if (name == import) {
|
||||||
|
vcfImportLauncher.launch(arrayOf("text/vcard", "text/x-vcard"))
|
||||||
|
return@DropdownMenu
|
||||||
|
}
|
||||||
val mode = contactValues[contactNames.indexOf(name)]
|
val mode = contactValues[contactNames.indexOf(name)]
|
||||||
val contactsPermissions = arrayOf(
|
val contactsPermissions = arrayOf(
|
||||||
Manifest.permission.READ_CONTACTS,
|
Manifest.permission.READ_CONTACTS,
|
||||||
@@ -404,6 +508,8 @@ private fun ContactsContent(
|
|||||||
val secondAction = remember { mutableStateOf({}) }
|
val secondAction = remember { mutableStateOf({}) }
|
||||||
val lastText = remember { mutableStateOf("") }
|
val lastText = remember { mutableStateOf("") }
|
||||||
val lastAction = remember { mutableStateOf({}) }
|
val lastAction = remember { mutableStateOf({}) }
|
||||||
|
val thirdText = remember { mutableStateOf("") }
|
||||||
|
val thirdAction = remember { mutableStateOf({}) }
|
||||||
|
|
||||||
if (showDialog.value)
|
if (showDialog.value)
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -413,6 +519,8 @@ private fun ContactsContent(
|
|||||||
firstButtonText = stringResource(R.string.cancel),
|
firstButtonText = stringResource(R.string.cancel),
|
||||||
secondButtonText = secondText.value,
|
secondButtonText = secondText.value,
|
||||||
onSecondClicked = secondAction.value,
|
onSecondClicked = secondAction.value,
|
||||||
|
thirdButtonText = thirdText.value,
|
||||||
|
onThirdClicked = thirdAction.value,
|
||||||
lastButtonText = lastText.value,
|
lastButtonText = lastText.value,
|
||||||
onLastClicked = lastAction.value,
|
onLastClicked = lastAction.value,
|
||||||
)
|
)
|
||||||
@@ -430,10 +538,17 @@ private fun ContactsContent(
|
|||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
val scrollToContact = navController.currentBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.getLiveData<String>("scrollToContact")
|
||||||
|
?.observeAsState()
|
||||||
|
|
||||||
|
var lastSearchQuery by remember { mutableStateOf("") }
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
if (searchQuery.isBlank()) {
|
if (searchQuery.isBlank() && lastSearchQuery.isNotBlank()) {
|
||||||
lazyListState.scrollToItem(0)
|
lazyListState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
|
lastSearchQuery = searchQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredContacts = remember(BaresipService.contacts, 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(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -521,10 +646,17 @@ private fun ContactsContent(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
Log.w(TAG, "onClickListener did not find UA for $aor")
|
Log.w(TAG, "onClickListener did not find UA for $aor")
|
||||||
dialogMessage.value = String.format(
|
val peerName = contact.name()
|
||||||
ctx.getString(R.string.contact_action_question),
|
if (contact.email.isNotEmpty())
|
||||||
contact.name()
|
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)
|
secondText.value = ctx.getString(R.string.call)
|
||||||
secondAction.value = {
|
secondAction.value = {
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
@@ -555,8 +687,8 @@ private fun ContactsContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastText.value = ctx.getString(R.string.send_message)
|
thirdText.value = ctx.getString(R.string.send_message)
|
||||||
lastAction.value = {
|
thirdAction.value = {
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
val uris = if (ua.account.isMobile)
|
val uris = if (ua.account.isMobile)
|
||||||
contact.uris.filter { it.startsWith("tel:") }
|
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
|
showDialog.value = true
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
|
|||||||
@@ -304,11 +304,12 @@ object CustomElements {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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(
|
if (buttonCount > 0) {
|
||||||
firstButtonText, secondButtonText, thirdButtonText, lastButtonText
|
|
||||||
).count { it.isNotEmpty() }
|
|
||||||
|
|
||||||
if (buttonCount >= 3) {
|
if (buttonCount >= 3) {
|
||||||
// Use a Column for 3-4 buttons, aligned to the end (right)
|
// Use a Column for 3-4 buttons, aligned to the end (right)
|
||||||
@@ -316,26 +317,28 @@ object CustomElements {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = {
|
if (firstButtonText.isNotEmpty())
|
||||||
onFirstClicked()
|
TextButton(onClick = {
|
||||||
showDialog.value = false
|
onFirstClicked()
|
||||||
}) {
|
showDialog.value = false
|
||||||
Text(
|
}) {
|
||||||
text = firstButtonText.uppercase(),
|
Text(
|
||||||
fontSize = 14.sp,
|
text = firstButtonText.uppercase(),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
fontSize = 14.sp,
|
||||||
)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
}
|
)
|
||||||
TextButton(onClick = {
|
}
|
||||||
onSecondClicked()
|
if (secondButtonText.isNotEmpty())
|
||||||
showDialog.value = false
|
TextButton(onClick = {
|
||||||
}) {
|
onSecondClicked()
|
||||||
Text(
|
showDialog.value = false
|
||||||
text = secondButtonText.uppercase(),
|
}) {
|
||||||
fontSize = 14.sp,
|
Text(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
text = secondButtonText.uppercase(),
|
||||||
)
|
fontSize = 14.sp,
|
||||||
}
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (thirdButtonText.isNotEmpty())
|
if (thirdButtonText.isNotEmpty())
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onThirdClicked()
|
onThirdClicked()
|
||||||
@@ -347,16 +350,17 @@ object CustomElements {
|
|||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextButton(onClick = {
|
if (lastButtonText.isNotEmpty())
|
||||||
onLastClicked()
|
TextButton(onClick = {
|
||||||
showDialog.value = false
|
onLastClicked()
|
||||||
}) {
|
showDialog.value = false
|
||||||
Text(
|
}) {
|
||||||
text = lastButtonText.uppercase(),
|
Text(
|
||||||
fontSize = 14.sp,
|
text = lastButtonText.uppercase(),
|
||||||
color = MaterialTheme.colorScheme.primary
|
fontSize = 14.sp,
|
||||||
)
|
color = MaterialTheme.colorScheme.primary
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the existing Row for 1 or 2 buttons
|
// Use the existing Row for 1 or 2 buttons
|
||||||
@@ -386,16 +390,28 @@ object CustomElements {
|
|||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextButton(onClick = {
|
if (thirdButtonText.isNotEmpty())
|
||||||
onLastClicked()
|
TextButton(onClick = {
|
||||||
showDialog.value = false
|
onThirdClicked()
|
||||||
}) {
|
showDialog.value = false
|
||||||
Text(
|
}) {
|
||||||
text = lastButtonText.uppercase(),
|
Text(
|
||||||
fontSize = 14.sp,
|
text = thirdButtonText.uppercase(),
|
||||||
color = MaterialTheme.colorScheme.primary
|
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 {
|
fun sendSms(ctx: Context, destination: String, message: String): Boolean {
|
||||||
return try {
|
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)
|
ctx.getSystemService(android.telephony.SmsManager::class.java)
|
||||||
} else {
|
else
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
android.telephony.SmsManager.getDefault()
|
android.telephony.SmsManager.getDefault()
|
||||||
}
|
smsManager.sendTextMessage(
|
||||||
smsManager.sendTextMessage(destination, null, message, null, null)
|
destination,
|
||||||
|
null,
|
||||||
|
message,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
Log.d(TAG, "Sent SMS message to $destination")
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send SMS: ${e.message}")
|
Log.e(TAG, "Failed to send SMS: ${e.message}")
|
||||||
|
|||||||
@@ -483,21 +483,30 @@
|
|||||||
<!-- Contact Activity -->
|
<!-- Contact Activity -->
|
||||||
<string name="new_contact">Uusi yhteystieto</string>
|
<string name="new_contact">Uusi yhteystieto</string>
|
||||||
<string name="contact_name">Nimi</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="sip_or_tel_uri">SIP tai tel URI</string>
|
||||||
<string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string>
|
<string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string>
|
||||||
<string name="favorite">Suosikki</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="invalid_contact">Virheellinen yhteystiedon nimi \'%1$s\'</string>
|
||||||
<string name="contact_already_exists">Yhteystieto \'%1$s\' on jo olemassa.</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="android_contact_help">Jos merkitty, tämä yhteystieto lisätään Androidin yhteystietoihin.</string>
|
||||||
<string name="avatar_image">Profiilikuva</string>
|
<string name="avatar_image">Profiilikuva</string>
|
||||||
<!-- Contacts Activity -->
|
<!-- Contacts Activity -->
|
||||||
<string name="contacts">Yhteystiedot</string>
|
<string name="contacts">Yhteystiedot</string>
|
||||||
<string name="contact_action_question">Haluatko soittaa tai lähettää
|
<string name="contact_action_question">Haluatko soittaa tai lähettää viestin
|
||||||
viestin ositteeseen \'%1$s\'\?</string>
|
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_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="contact_delete_question">Haluatko poistaa yhteystiedon \'%1$s\'\?</string>
|
||||||
<string name="search">Etsi</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 -->
|
<!-- Generic -->
|
||||||
<string name="alert">Varoitus</string>
|
<string name="alert">Varoitus</string>
|
||||||
<string name="info">Tieto</string>
|
<string name="info">Tieto</string>
|
||||||
|
|||||||
@@ -456,6 +456,7 @@
|
|||||||
<!-- Contact Activity -->
|
<!-- Contact Activity -->
|
||||||
<string name="new_contact">New Contact</string>
|
<string name="new_contact">New Contact</string>
|
||||||
<string name="contact_name">Name</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="sip_or_tel_uri">SIP or tel URI</string>
|
||||||
<string name="user_domain_or_number">user@domain or telephone number</string>
|
<string name="user_domain_or_number">user@domain or telephone number</string>
|
||||||
<string name="favorite">Favorite</string>
|
<string name="favorite">Favorite</string>
|
||||||
@@ -468,9 +469,14 @@
|
|||||||
<!-- Contacts Activity -->
|
<!-- Contacts Activity -->
|
||||||
<string name="contacts">Contacts</string>
|
<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_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_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="contact_delete_question">Do you want to delete contact \'%1$s\'\?</string>
|
||||||
<string name="search">Search</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 -->
|
<!-- Generic -->
|
||||||
<string name="alert">Alert</string>
|
<string name="alert">Alert</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user