Added contacts export to vcf file

Fixed crash when entering empty contacts
This commit is contained in:
Juha Heinanen
2026-05-20 13:39:45 +03:00
parent 98cab8e2bc
commit 06730b4cfa
3 changed files with 51 additions and 4 deletions
@@ -29,14 +29,13 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
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.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.Edit
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -112,6 +111,43 @@ private fun ContactsScreen(navController: NavController) {
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 import = stringResource(R.string.import_contacts)
val export = stringResource(R.string.export_contacts)
val vcfExportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/vcard")
) { uri: android.net.Uri? ->
if (uri != null) {
try {
ctx.contentResolver.openOutputStream(uri)?.use { outputStream ->
val writer = outputStream.bufferedWriter()
for (contact in BaresipService.baresipContacts.value) {
writer.write("BEGIN:VCARD\n")
writer.write("VERSION:3.0\n")
writer.write("FN:${contact.name}\n")
val nameParts = contact.name.split(" ", limit = 2)
if (nameParts.size == 2)
writer.write("N:${nameParts[1]};${nameParts[0]};;;\n")
else
writer.write("N:;${contact.name};;;\n")
if (contact.email.isNotEmpty())
writer.write("EMAIL:${contact.email}\n")
for (u in contact.uris) {
if (u.startsWith("tel:"))
writer.write("TEL:${u.substring(4)}\n")
else if (u.startsWith("sip:"))
writer.write("X-SIP:${u.substring(4)}\n")
}
writer.write("END:VCARD\n")
}
writer.flush()
}
Toast.makeText(ctx, R.string.contact_export_success, Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e(TAG, "Failed to export VCF: ${e.message}")
Toast.makeText(ctx, R.string.contact_export_failure, Toast.LENGTH_SHORT).show()
}
}
}
val vcfImportLauncher = rememberLauncherForActivityResult( val vcfImportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
@@ -357,13 +393,17 @@ private fun ContactsScreen(navController: NavController) {
CustomElements.DropdownMenu( CustomElements.DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false }, onDismissRequest = { expanded = false },
items = contactNames + import, items = contactNames + import + export,
onItemClick = { name -> onItemClick = { name ->
expanded = false expanded = false
if (name == import) { if (name == import) {
vcfImportLauncher.launch(arrayOf("text/vcard", "text/x-vcard")) vcfImportLauncher.launch(arrayOf("text/vcard", "text/x-vcard"))
return@DropdownMenu return@DropdownMenu
} }
if (name == export) {
vcfExportLauncher.launch("baresip_contacts.vcf")
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,
@@ -571,7 +611,9 @@ private fun ContactsContent(
state = lazyListState, state = lazyListState,
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
items(filteredContacts, key = { it.first.id() }) { (contact, annotatedName) -> itemsIndexed(
filteredContacts, key = { index, (contact, _) -> "${contact.id()}_$index" }
) { _, (contact, annotatedName) ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
+2
View File
@@ -508,6 +508,8 @@
<string name="import_contacts">Tuo</string> <string name="import_contacts">Tuo</string>
<string name="contact_import_success">Yhteystietojen tuonti onnistui</string> <string name="contact_import_success">Yhteystietojen tuonti onnistui</string>
<string name="contact_import_failure">Yhteystietojen tuonti epäonnistui</string> <string name="contact_import_failure">Yhteystietojen tuonti epäonnistui</string>
<string name="contact_export_success">Yhteistietojen vienti onnistui</string>
<string name="contact_export_failure">Yhteistietojen vienti epäonnistui</string>
<!-- Generic --> <!-- Generic -->
<string name="alert">Varoitus</string> <string name="alert">Varoitus</string>
+3
View File
@@ -477,8 +477,11 @@
<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="import_contacts">Import</string>
<string name="export_contacts">Export</string>
<string name="contact_import_success">Contacts imported successfully</string> <string name="contact_import_success">Contacts imported successfully</string>
<string name="contact_import_failure">Failed to import contacts</string> <string name="contact_import_failure">Failed to import contacts</string>
<string name="contact_export_success">Contacts exported successfully</string>
<string name="contact_export_failure">Failed to export contacts</string>
<!-- Generic --> <!-- Generic -->
<string name="alert">Alert</string> <string name="alert">Alert</string>
<string name="info">Info</string> <string name="info">Info</string>