Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Setting for vCard Export Version ([#344])

## [1.6.0] - 2026-01-30
### Added
Expand Down Expand Up @@ -124,6 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#289]: https://github.com/FossifyOrg/Contacts/issues/289
[#321]: https://github.com/FossifyOrg/Contacts/issues/321
[#339]: https://github.com/FossifyOrg/Contacts/issues/339
[#344]: https://github.com/FossifyOrg/Contacts/issues/344
[#360]: https://github.com/FossifyOrg/Contacts/issues/360
[#415]: https://github.com/FossifyOrg/Contacts/issues/415
[#452]: https://github.com/FossifyOrg/Contacts/issues/452
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.fossify.contacts.R
import org.fossify.contacts.databinding.ActivitySettingsBinding
import org.fossify.contacts.dialogs.ExportContactsDialog
import org.fossify.contacts.dialogs.ManageAutoBackupsDialog
import org.fossify.contacts.dialogs.ManageVCardVersionDialog
import org.fossify.contacts.dialogs.ManageVisibleFieldsDialog
import org.fossify.contacts.dialogs.ManageVisibleTabsDialog
import org.fossify.contacts.extensions.*
Expand Down Expand Up @@ -64,6 +65,7 @@ class SettingsActivity : SimpleActivity() {
setupManageAutomaticBackups()
setupExportContacts()
setupImportContacts()
setupVCardVersion()
updateTextColors(binding.settingsHolder)

arrayOf(
Expand Down Expand Up @@ -304,6 +306,12 @@ class SettingsActivity : SimpleActivity() {
}
}

private fun setupVCardVersion() {
binding.vcardVersionHolder.setOnClickListener {
ManageVCardVersionDialog(this)
}
}

private fun tryImportContacts() {
if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.fossify.contacts.dialogs

import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.commons.extensions.getAlertDialogBuilder
import org.fossify.commons.extensions.setupDialogStuff
import org.fossify.contacts.R
import org.fossify.contacts.extensions.config
import org.fossify.contacts.helpers.DEFAULT_VCARD_VERSION

class ManageVCardVersionDialog(val activity: BaseSimpleActivity) {
private var view = activity.layoutInflater.inflate(R.layout.dialog_vcard_version, null)
private val versionRadioButtons = LinkedHashMap<String, Int>()

init {
versionRadioButtons.apply {
put("2.1", R.id.manage_vcard_version_2_1)
put("3", R.id.manage_vcard_version_3)
put("4", R.id.manage_vcard_version_4)
}

val currentVersion = activity.config.vCardVersion
versionRadioButtons[currentVersion]?.let { radioButtonId ->
view.findViewById<android.widget.RadioButton>(radioButtonId).isChecked = true
}

activity.getAlertDialogBuilder()
.setPositiveButton(org.fossify.commons.R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(org.fossify.commons.R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this)
}
}

private fun dialogConfirmed() {
val selectedVersion = versionRadioButtons.entries.find { entry ->
view.findViewById<android.widget.RadioButton>(entry.value).isChecked
}?.key ?: DEFAULT_VCARD_VERSION

activity.config.vCardVersion = selectedVersion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.fossify.contacts.helpers

import android.content.Context
import androidx.core.net.toUri
import ezvcard.VCard
import ezvcard.parameter.ImageType
import ezvcard.property.Organization
import ezvcard.property.Title
import ezvcard.property.Photo
import ezvcard.property.Categories
import org.fossify.commons.models.contacts.Contact

object CardPropertyAdderAdditional {
fun addProperties(context: Context, card: VCard, contact: Contact) {
addOrganization(card, contact)
addWebsites(card, contact)
addPhoto(context, card, contact)
addGroups(card, contact)
}

private fun addOrganization(card: VCard, contact: Contact) {
if (contact.organization.isNotEmpty()) {
val org = Organization().apply { values.add(contact.organization.company) }
card.organization = org
card.titles.add(Title(contact.organization.jobPosition))
}
}

private fun addWebsites(card: VCard, contact: Contact) {
contact.websites.forEach { card.addUrl(it) }
}

private fun addPhoto(context: Context, card: VCard, contact: Contact) {
try {
context.contentResolver.openInputStream(contact.photoUri.toUri())?.use {
card.addPhoto(Photo(it.readBytes(), ImageType.JPEG))
}
} catch (_: Exception) {}
}

private fun addGroups(card: VCard, contact: Contact) {
if (contact.groups.isNotEmpty()) {
card.categories = Categories().apply { contact.groups.forEach { values.add(it.title) } }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.fossify.contacts.helpers

import android.content.Context
import android.provider.ContactsContract.CommonDataKinds.Event
import android.provider.ContactsContract.CommonDataKinds.Im
import ezvcard.VCard
import ezvcard.property.Telephone
import ezvcard.property.Email
import ezvcard.property.Birthday
import ezvcard.property.Anniversary
import ezvcard.util.PartialDate
import org.fossify.commons.extensions.getDateTimeFromDateString
import org.fossify.commons.models.contacts.Contact
import java.time.LocalDate

object CardPropertyAdderMain {
fun addProperties(card: VCard, contact: Contact) {
addPhoneNumbers(card, contact)
addEmails(card, contact)
addEvents(card, contact)
addAddress(card, contact)
addIMs(card, contact)
addNotes(card, contact)
}

private fun addPhoneNumbers(card: VCard, contact: Contact) {
contact.phoneNumbers.forEach {
val phone = Telephone(it.value).apply {
parameters.addType(TypeLabelMapper.getPhoneNumberTypeLabel(it.type, it.label))
if (it.isPrimary) parameters.addType(TypeLabelMapper.getPreferredType(1))
}
card.addTelephoneNumber(phone)
}
}

private fun addEmails(card: VCard, contact: Contact) {
contact.emails.forEach {
card.addEmail(Email(it.value).apply {
parameters.addType(TypeLabelMapper.getEmailTypeLabel(it.type, it.label))
})
}
}

private fun addEvents(card: VCard, contact: Contact) {
contact.events
.filter { it.type == Event.TYPE_BIRTHDAY || it.type == Event.TYPE_ANNIVERSARY }
.forEach { addEventToCard(card, it) }
}

private fun addEventToCard(card: VCard, event: org.fossify.commons.models.contacts.Event) {
val dateTime = event.value.getDateTimeFromDateString(false)
val isBirthday = event.type == Event.TYPE_BIRTHDAY

if (event.value.startsWith("--")) {
val partial = PartialDate.builder()
.month(dateTime.monthOfYear)
.date(dateTime.dayOfMonth)
.build()
if (isBirthday) card.birthdays.add(Birthday(partial))
else card.anniversaries.add(Anniversary(partial))
return
}

val date = LocalDate.of(dateTime.year, dateTime.monthOfYear, dateTime.dayOfMonth)
if (isBirthday) card.birthdays.add(Birthday(date))
else card.anniversaries.add(Anniversary(date))
}

private fun addAddress(card: VCard, contact: Contact) {
contact.addresses.forEach { addr ->
val address = ezvcard.property.Address().apply {
if (listOf(
addr.country,
addr.region,
addr.city,
addr.postcode,
addr.pobox,
addr.street,
addr.neighborhood)
.any { it.isNotEmpty() }) {
country = addr.country
region = addr.region
locality = addr.city
postalCode = addr.postcode
poBox = addr.pobox
streetAddress = addr.street
extendedAddress = addr.neighborhood
} else streetAddress = addr.value
parameters.addType(TypeLabelMapper.getAddressTypeLabel(addr.type, addr.label))
}
card.addAddress(address)
}
}

private fun addIMs(card: VCard, contact: Contact) {
contact.IMs.forEach {
val impp = when (it.type) {
Im.PROTOCOL_AIM -> ezvcard.property.Impp.aim(it.value)
Im.PROTOCOL_YAHOO -> ezvcard.property.Impp.yahoo(it.value)
Im.PROTOCOL_MSN -> ezvcard.property.Impp.msn(it.value)
Im.PROTOCOL_ICQ -> ezvcard.property.Impp.icq(it.value)
Im.PROTOCOL_SKYPE -> ezvcard.property.Impp.skype(it.value)
Im.PROTOCOL_GOOGLE_TALK -> ezvcard.property.Impp(HANGOUTS, it.value)
Im.PROTOCOL_QQ -> ezvcard.property.Impp(QQ, it.value)
Im.PROTOCOL_JABBER -> ezvcard.property.Impp(JABBER, it.value)
else -> ezvcard.property.Impp(it.label, it.value)
}
card.addImpp(impp)
}
}

private fun addNotes(card: VCard, contact: Contact) {
if (contact.notes.isNotEmpty()) card.addNote(contact.notes)
}
}
3 changes: 3 additions & 0 deletions app/src/main/kotlin/org/fossify/contacts/helpers/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ class Config(context: Context) : BaseConfig(context) {
set(autoBackupContactSources) = prefs.edit().remove(AUTO_BACKUP_CONTACT_SOURCES).putStringSet(AUTO_BACKUP_CONTACT_SOURCES, autoBackupContactSources)
.apply()

var vCardVersion: String
get() = prefs.getString(VCARD_VERSION, DEFAULT_VCARD_VERSION) ?: DEFAULT_VCARD_VERSION
set(vcardVersion) = prefs.edit().putString(VCARD_VERSION, vcardVersion).apply()
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ const val SIGNAL = "signal"
const val VIBER = "viber"
const val TELEGRAM = "telegram"
const val THREEMA = "threema"

const val VCARD_VERSION = "4"
const val DEFAULT_VCARD_VERSION = "4"

// 6 am is the hardcoded automatic backup time, intervals shorter than 1 day are not yet supported.
fun getNextAutoBackupTime(): DateTime {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.fossify.contacts.helpers

object TypeLabelMapper {
fun getPhoneNumberTypeLabel(type: Int, label: String) = when (type) {
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> "CELL"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> "HOME"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> "WORK"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_MAIN -> "MAIN"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK -> "WORK_FAX"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME -> "HOME_FAX"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_PAGER -> "PAGER"
android.provider.ContactsContract.CommonDataKinds.Phone.TYPE_OTHER -> "OTHER"
else -> label
}

fun getEmailTypeLabel(type: Int, label: String) = when (type) {
android.provider.ContactsContract.CommonDataKinds.Email.TYPE_HOME -> "HOME"
android.provider.ContactsContract.CommonDataKinds.Email.TYPE_WORK -> "WORK"
android.provider.ContactsContract.CommonDataKinds.Email.TYPE_MOBILE -> "MOBILE"
android.provider.ContactsContract.CommonDataKinds.Email.TYPE_OTHER -> "OTHER"
else -> label
}

fun getAddressTypeLabel(type: Int, label: String) = when (type) {
android.provider.ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME -> "HOME"
android.provider.ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK -> "WORK"
android.provider.ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER -> "OTHER"
else -> label
}

fun getPreferredType(value: Int) = "PREF=$value"
}
Loading
Loading