Added "Delete" action to contacts menu
Import and export of baresip contacts now support also avatar images
This commit is contained in:
@ -4,7 +4,10 @@ import android.Manifest
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
@ -55,8 +58,6 @@ 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
|
||||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -73,6 +74,8 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
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.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@ -81,7 +84,9 @@ import com.tutpro.baresip.CustomElements.AlertDialog
|
|||||||
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
||||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
const val avatarSize: Int = 96
|
const val avatarSize: Int = 96
|
||||||
@ -112,6 +117,11 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
val both = stringResource(R.string.both)
|
val both = stringResource(R.string.both)
|
||||||
val import = stringResource(R.string.import_contacts)
|
val import = stringResource(R.string.import_contacts)
|
||||||
val export = stringResource(R.string.export_contacts)
|
val export = stringResource(R.string.export_contacts)
|
||||||
|
val delete = stringResource(R.string.delete)
|
||||||
|
val confirmation = stringResource(R.string.confirmation)
|
||||||
|
val cancel = stringResource(R.string.cancel)
|
||||||
|
val contactsDeleteQuestion = stringResource(R.string.contacts_delete_question)
|
||||||
|
val contactDeleteQuestion = stringResource(R.string.contact_delete_question)
|
||||||
|
|
||||||
val vcfExportLauncher = rememberLauncherForActivityResult(
|
val vcfExportLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.CreateDocument("text/vcard")
|
contract = ActivityResultContracts.CreateDocument("text/vcard")
|
||||||
@ -131,6 +141,12 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
writer.write("N:;${contact.name};;;\n")
|
writer.write("N:;${contact.name};;;\n")
|
||||||
if (contact.email.isNotEmpty())
|
if (contact.email.isNotEmpty())
|
||||||
writer.write("EMAIL:${contact.email}\n")
|
writer.write("EMAIL:${contact.email}\n")
|
||||||
|
if (contact.avatarImage != null) {
|
||||||
|
val outputStreamPhoto = ByteArrayOutputStream()
|
||||||
|
contact.avatarImage!!.compress(Bitmap.CompressFormat.JPEG, 100, outputStreamPhoto)
|
||||||
|
val base64Image = Base64.encodeToString(outputStreamPhoto.toByteArray(), Base64.NO_WRAP)
|
||||||
|
writer.write("PHOTO;ENCODING=BASE64;JPEG:$base64Image\n")
|
||||||
|
}
|
||||||
for (u in contact.uris) {
|
for (u in contact.uris) {
|
||||||
if (u.startsWith("tel:"))
|
if (u.startsWith("tel:"))
|
||||||
writer.write("TEL:${u.substring(4)}\n")
|
writer.write("TEL:${u.substring(4)}\n")
|
||||||
@ -156,15 +172,31 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
try {
|
try {
|
||||||
ctx.contentResolver.openInputStream(uri)?.use { inputStream ->
|
ctx.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
val reader = inputStream.bufferedReader()
|
val reader = inputStream.bufferedReader()
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
reader.forEachLine { line ->
|
||||||
|
if (line.startsWith(" ") || line.startsWith("\t")) {
|
||||||
|
if (lines.isNotEmpty()) {
|
||||||
|
lines[lines.size - 1] = lines.last() + line.trim()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.add(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var name = ""
|
var name = ""
|
||||||
var email = ""
|
var email = ""
|
||||||
val uris = mutableListOf<String>()
|
val uris = mutableListOf<String>()
|
||||||
reader.forEachLine { line ->
|
var photoBase64 = ""
|
||||||
|
var contactNo = 0
|
||||||
|
val newBaresipContacts = BaresipService.baresipContacts.value.toMutableList()
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
when {
|
when {
|
||||||
line.startsWith("BEGIN:VCARD", ignoreCase = true) -> {
|
line.startsWith("BEGIN:VCARD", ignoreCase = true) -> {
|
||||||
name = ""
|
name = ""
|
||||||
email = ""
|
email = ""
|
||||||
uris.clear()
|
uris.clear()
|
||||||
|
photoBase64 = ""
|
||||||
}
|
}
|
||||||
line.startsWith("FN:", ignoreCase = true) -> {
|
line.startsWith("FN:", ignoreCase = true) -> {
|
||||||
name = line.substring(3).trim()
|
name = line.substring(3).trim()
|
||||||
@ -191,32 +223,44 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
if (fullSipUri !in uris) uris.add(fullSipUri)
|
if (fullSipUri !in uris) uris.add(fullSipUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
line.startsWith("PHOTO", ignoreCase = true) && line.contains("BASE64", ignoreCase = true) -> {
|
||||||
|
photoBase64 = line.substringAfter(":").trim()
|
||||||
|
}
|
||||||
line.startsWith("END:VCARD", ignoreCase = true) -> {
|
line.startsWith("END:VCARD", ignoreCase = true) -> {
|
||||||
if (name.isNotEmpty() && (uris.isNotEmpty() || email.isNotEmpty())) {
|
if (name.isNotEmpty() && (uris.isNotEmpty() || email.isNotEmpty())) {
|
||||||
val existingContact = Contact.baresipContact(name)
|
val existingContact = newBaresipContacts.find { it.name == name }
|
||||||
|
val contactId = existingContact?.id ?: (System.currentTimeMillis() + contactNo++)
|
||||||
|
if (photoBase64.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val decodedString = Base64.decode(photoBase64, Base64.DEFAULT)
|
||||||
|
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
|
||||||
|
if (decodedByte != null) {
|
||||||
|
val avatarFile = File(BaresipService.filesPath, "$contactId.png")
|
||||||
|
FileOutputStream(avatarFile).use { out ->
|
||||||
|
decodedByte.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to decode photo for $name: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (existingContact != null) {
|
if (existingContact != null) {
|
||||||
var updated = false
|
|
||||||
for (u in uris) {
|
for (u in uris) {
|
||||||
if (u !in existingContact.uris) {
|
if (u !in existingContact.uris) {
|
||||||
existingContact.uris.add(u)
|
existingContact.uris.add(u)
|
||||||
updated = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (existingContact.email.isEmpty() && email.isNotEmpty()) {
|
if (existingContact.email.isEmpty() && email.isNotEmpty()) {
|
||||||
existingContact.email = email
|
existingContact.email = email
|
||||||
updated = true
|
|
||||||
}
|
|
||||||
if (updated) {
|
|
||||||
Contact.updateBaresipContact(existingContact.id, existingContact)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Contact.addBaresipContact(
|
newBaresipContacts.add(
|
||||||
Contact.BaresipContact(
|
Contact.BaresipContact(
|
||||||
name,
|
name,
|
||||||
ArrayList(uris),
|
ArrayList(uris),
|
||||||
email,
|
email,
|
||||||
Utils.randomColor(),
|
Utils.randomColor(),
|
||||||
System.currentTimeMillis(),
|
contactId,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -225,7 +269,9 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BaresipService.baresipContacts.value = newBaresipContacts.toList()
|
||||||
Contact.saveBaresipContacts()
|
Contact.saveBaresipContacts()
|
||||||
|
Contact.restoreBaresipContacts()
|
||||||
Contact.contactsUpdate()
|
Contact.contactsUpdate()
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
ctx,
|
ctx,
|
||||||
@ -393,7 +439,7 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
CustomElements.DropdownMenu(
|
CustomElements.DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
items = contactNames + import + export,
|
items = contactNames + import + export + delete,
|
||||||
onItemClick = { name ->
|
onItemClick = { name ->
|
||||||
expanded = false
|
expanded = false
|
||||||
if (name == import) {
|
if (name == import) {
|
||||||
@ -404,6 +450,24 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
vcfExportLauncher.launch("baresip_contacts.vcf")
|
vcfExportLauncher.launch("baresip_contacts.vcf")
|
||||||
return@DropdownMenu
|
return@DropdownMenu
|
||||||
}
|
}
|
||||||
|
if (name == delete) {
|
||||||
|
title.value = confirmation
|
||||||
|
message.value = contactsDeleteQuestion
|
||||||
|
firstButtonText.value = cancel
|
||||||
|
onFirstClicked.value = { }
|
||||||
|
lastButtonText.value = delete
|
||||||
|
onLastClicked.value = {
|
||||||
|
for (contact in BaresipService.baresipContacts.value) {
|
||||||
|
val avatarFile = File(BaresipService.filesPath, "${contact.id}.png")
|
||||||
|
if (avatarFile.exists()) avatarFile.delete()
|
||||||
|
}
|
||||||
|
BaresipService.baresipContacts.value = mutableListOf()
|
||||||
|
Contact.saveBaresipContacts()
|
||||||
|
Contact.contactsUpdate()
|
||||||
|
}
|
||||||
|
showDialog.value = true
|
||||||
|
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,
|
||||||
@ -462,7 +526,23 @@ private fun ContactsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = { contentPadding ->
|
content = { contentPadding ->
|
||||||
ContactsContent(ctx, navController, contentPadding, searchContactName)
|
ContactsContent(
|
||||||
|
ctx,
|
||||||
|
navController,
|
||||||
|
contentPadding,
|
||||||
|
searchContactName,
|
||||||
|
showDialog,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
firstButtonText,
|
||||||
|
onFirstClicked,
|
||||||
|
lastButtonText,
|
||||||
|
onLastClicked,
|
||||||
|
confirmation,
|
||||||
|
cancel,
|
||||||
|
delete,
|
||||||
|
contactDeleteQuestion
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -532,33 +612,19 @@ private fun ContactsContent(
|
|||||||
ctx: Context,
|
ctx: Context,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
searchQuery: String
|
searchQuery: String,
|
||||||
|
showDialog: androidx.compose.runtime.MutableState<Boolean>,
|
||||||
|
title: androidx.compose.runtime.MutableState<String>,
|
||||||
|
message: androidx.compose.runtime.MutableState<String>,
|
||||||
|
firstButtonText: androidx.compose.runtime.MutableState<String>,
|
||||||
|
onFirstClicked: androidx.compose.runtime.MutableState<() -> Unit>,
|
||||||
|
lastButtonText: androidx.compose.runtime.MutableState<String>,
|
||||||
|
onLastClicked: androidx.compose.runtime.MutableState<() -> Unit>,
|
||||||
|
confirmationText: String,
|
||||||
|
cancelText: String,
|
||||||
|
deleteText: String,
|
||||||
|
contactDeleteQuestion: String
|
||||||
) {
|
) {
|
||||||
val showConfirmationDialog = remember { mutableStateOf(false) }
|
|
||||||
val confirmationDialogMessage = remember { mutableStateOf("") }
|
|
||||||
val lastAction = remember { mutableStateOf({}) }
|
|
||||||
|
|
||||||
if (showConfirmationDialog.value)
|
|
||||||
AlertDialog(
|
|
||||||
showDialog = showConfirmationDialog,
|
|
||||||
title = stringResource(R.string.confirmation),
|
|
||||||
message = confirmationDialogMessage.value,
|
|
||||||
firstButtonText = stringResource(R.string.cancel),
|
|
||||||
lastButtonText = stringResource(R.string.delete),
|
|
||||||
onLastClicked = lastAction.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
val alertTitle = remember { mutableStateOf("") }
|
|
||||||
val alertMessage = remember { mutableStateOf("") }
|
|
||||||
val showAlert = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
showDialog = showAlert,
|
|
||||||
title = alertTitle.value,
|
|
||||||
message = alertMessage.value,
|
|
||||||
lastButtonText = stringResource(R.string.ok),
|
|
||||||
)
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
val scrollToContact = navController.currentBackStackEntry
|
val scrollToContact = navController.currentBackStackEntry
|
||||||
@ -666,11 +732,15 @@ private fun ContactsContent(
|
|||||||
navController.navigate("contact/${contact.name()}/old")
|
navController.navigate("contact/${contact.name()}/old")
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
confirmationDialogMessage.value = String.format(
|
title.value = confirmationText
|
||||||
ctx.getString(R.string.contact_delete_question),
|
message.value = String.format(
|
||||||
|
contactDeleteQuestion,
|
||||||
contact.name()
|
contact.name()
|
||||||
)
|
)
|
||||||
lastAction.value = {
|
firstButtonText.value = cancelText
|
||||||
|
onFirstClicked.value = { }
|
||||||
|
lastButtonText.value = deleteText
|
||||||
|
onLastClicked.value = {
|
||||||
val id = contact.id
|
val id = contact.id
|
||||||
val avatarFile = File(
|
val avatarFile = File(
|
||||||
BaresipService.filesPath,
|
BaresipService.filesPath,
|
||||||
@ -688,7 +758,7 @@ private fun ContactsContent(
|
|||||||
}
|
}
|
||||||
Contact.removeBaresipContact(contact)
|
Contact.removeBaresipContact(contact)
|
||||||
}
|
}
|
||||||
showConfirmationDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -706,18 +776,22 @@ private fun ContactsContent(
|
|||||||
navController.navigate("contact/${contact.name()}/old")
|
navController.navigate("contact/${contact.name()}/old")
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
confirmationDialogMessage.value = String.format(
|
title.value = confirmationText
|
||||||
ctx.getString(R.string.contact_delete_question),
|
message.value = String.format(
|
||||||
|
contactDeleteQuestion,
|
||||||
contact.name()
|
contact.name()
|
||||||
)
|
)
|
||||||
lastAction.value = {
|
firstButtonText.value = cancelText
|
||||||
|
onFirstClicked.value = { }
|
||||||
|
lastButtonText.value = deleteText
|
||||||
|
onLastClicked.value = {
|
||||||
ctx.contentResolver.delete(
|
ctx.contentResolver.delete(
|
||||||
ContactsContract.RawContacts.CONTENT_URI,
|
ContactsContract.RawContacts.CONTENT_URI,
|
||||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + contact.name() + "'",
|
ContactsContract.Contacts.DISPLAY_NAME + "='" + contact.name() + "'",
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
showConfirmationDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -475,6 +475,7 @@
|
|||||||
<string name="send_message">Send Message</string>
|
<string name="send_message">Send Message</string>
|
||||||
<string name="send_email">Send Email</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="contacts_delete_question">Do you want to delete all contacts?</string>
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
<string name="import_contacts">Import</string>
|
<string name="import_contacts">Import</string>
|
||||||
<string name="export_contacts">Export</string>
|
<string name="export_contacts">Export</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user