From 934656921f63a722d9e0a1c8f23b7d4789043917 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 31 Jul 2021 21:33:19 +0200 Subject: Support all Android and Apple Relations/Related types --- .../at/bitfire/vcard4android/AndroidContactTest.kt | 57 ++++++++++++ .../at/bitfire/vcard4android/AndroidContact.kt | 100 +++++++++++++++------ .../at/bitfire/vcard4android/BatchOperation.kt | 2 +- src/main/java/at/bitfire/vcard4android/Contact.kt | 11 +-- .../java/at/bitfire/vcard4android/ContactReader.kt | 60 +++++++++++-- .../java/at/bitfire/vcard4android/ContactWriter.kt | 58 +++++++++++- .../vcard4android/property/CustomRelatedType.kt | 19 ++++ .../vcard4android/property/CustomScribes.kt | 18 ++-- .../at/bitfire/vcard4android/property/XAbLabel.kt | 7 ++ .../vcard4android/property/XAbRelatedNames.kt | 31 +++++++ .../at/bitfire/vcard4android/ContactReaderTest.kt | 50 ++++++++++- .../java/at/bitfire/vcard4android/ContactTest.kt | 2 +- .../at/bitfire/vcard4android/ContactWriterTest.kt | 95 +++++++++++++++++++- 13 files changed, 452 insertions(+), 58 deletions(-) create mode 100644 src/main/java/at/bitfire/vcard4android/property/CustomRelatedType.kt create mode 100644 src/main/java/at/bitfire/vcard4android/property/XAbRelatedNames.kt diff --git a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt index 5afd43d..342f94e 100644 --- a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt +++ b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt @@ -12,18 +12,22 @@ import android.Manifest import android.accounts.Account import android.content.ContentProviderClient import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.Relation import android.util.Base64 import androidx.test.filters.MediumTest import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.vcard4android.impl.TestAddressBook +import at.bitfire.vcard4android.property.CustomRelatedType import at.bitfire.vcard4android.property.XAbDate import ezvcard.VCardVersion import ezvcard.parameter.EmailType +import ezvcard.parameter.RelatedType import ezvcard.property.Address import ezvcard.property.Birthday import ezvcard.property.Email +import ezvcard.property.Related import ezvcard.util.PartialDate import org.junit.After import org.junit.Assert.* @@ -322,4 +326,57 @@ class AndroidContactTest { assertEquals("CyanogenModForum", AndroidContact.toURIScheme("CyanogenMod_Forum")) } + + // specific data rows + + @Test + fun testInsertRelation_Assistant() { + val batch = BatchOperation(provider) + AndroidContact(addressBook).insertRelation(batch, Related().apply { + text = "My Assistant" + types.add(RelatedType.CO_WORKER) + types.add(CustomRelatedType.ASSISTANT) + }) + assertEquals(1, batch.queue.size) + assertEquals("My Assistant", batch.queue[0].values[Relation.NAME]) + assertEquals(Relation.TYPE_ASSISTANT, batch.queue[0].values[Relation.TYPE]) + } + + @Test + fun testInsertRelation_Custom() { + val batch = BatchOperation(provider) + AndroidContact(addressBook).insertRelation(batch, Related().apply { + text = "Someone" + types.add(RelatedType.get("Some Relationship")) + }) + assertEquals(1, batch.queue.size) + assertEquals("Someone", batch.queue[0].values[Relation.NAME]) + assertEquals(Relation.TYPE_CUSTOM, batch.queue[0].values[Relation.TYPE]) + assertEquals("Some Relationship", batch.queue[0].values[Relation.LABEL]) + } + + @Test + fun testInsertRelation_Custom_TwoValues() { + val batch = BatchOperation(provider) + AndroidContact(addressBook).insertRelation(batch, Related().apply { + text = "Someone" + types.add(RelatedType.get("type1")) + types.add(RelatedType.get("type2")) + }) + assertEquals(1, batch.queue.size) + assertEquals("Someone", batch.queue[0].values[Relation.NAME]) + assertEquals(Relation.TYPE_CUSTOM, batch.queue[0].values[Relation.TYPE]) + assertEquals("Type1, Type2", batch.queue[0].values[Relation.LABEL]) + } + + @Test + fun testInsertRelation_NoType_Email() { + val batch = BatchOperation(provider) + AndroidContact(addressBook).insertRelation(batch, Related.email("test@example.com")) + assertEquals(1, batch.queue.size) + assertEquals("mailto:test@example.com", batch.queue[0].values[Relation.NAME]) + assertEquals(Relation.TYPE_CUSTOM, batch.queue[0].values[Relation.TYPE]) + assertEquals("Other", batch.queue[0].values[Relation.LABEL]) + } + } diff --git a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt index 2d5172d..1d8d19f 100644 --- a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt +++ b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt @@ -26,6 +26,7 @@ import android.provider.ContactsContract.CommonDataKinds.StructuredName import android.provider.ContactsContract.RawContacts import android.provider.ContactsContract.RawContacts.Data import androidx.annotation.CallSuper +import at.bitfire.vcard4android.property.CustomRelatedType import at.bitfire.vcard4android.property.XAbDate import ezvcard.parameter.* import ezvcard.property.* @@ -33,6 +34,7 @@ import ezvcard.util.PartialDate import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.builder.ToStringBuilder +import org.apache.commons.text.WordUtils import java.io.ByteArrayOutputStream import java.io.FileNotFoundException import java.io.IOException @@ -472,29 +474,52 @@ open class AndroidContact( related.text = name when (row.getAsInteger(Relation.TYPE)) { - Relation.TYPE_ASSISTANT, - Relation.TYPE_MANAGER -> + Relation.TYPE_ASSISTANT -> { + related.types += CustomRelatedType.ASSISTANT related.types += RelatedType.CO_WORKER - Relation.TYPE_BROTHER, - Relation.TYPE_SISTER -> + } + Relation.TYPE_BROTHER -> { + related.types += CustomRelatedType.BROTHER related.types += RelatedType.SIBLING + } Relation.TYPE_CHILD -> related.types += RelatedType.CHILD + Relation.TYPE_DOMESTIC_PARTNER -> { + related.types += CustomRelatedType.DOMESTIC_PARTNER + related.types += RelatedType.SPOUSE + } + Relation.TYPE_FATHER -> { + related.types += CustomRelatedType.FATHER + related.types += RelatedType.PARENT + } Relation.TYPE_FRIEND -> related.types += RelatedType.FRIEND - Relation.TYPE_FATHER, - Relation.TYPE_MOTHER, + Relation.TYPE_MANAGER -> { + related.types += CustomRelatedType.MANAGER + related.types += RelatedType.CO_WORKER + } + Relation.TYPE_MOTHER -> { + related.types += CustomRelatedType.MOTHER + related.types += RelatedType.PARENT + } Relation.TYPE_PARENT -> related.types += RelatedType.PARENT - Relation.TYPE_DOMESTIC_PARTNER, - Relation.TYPE_PARTNER, - Relation.TYPE_SPOUSE -> - related.types += RelatedType.SPOUSE + Relation.TYPE_PARTNER -> + related.types += CustomRelatedType.PARTNER + Relation.TYPE_REFERRED_BY -> + related.types += CustomRelatedType.REFERRED_BY Relation.TYPE_RELATIVE -> related.types += RelatedType.KIN + Relation.TYPE_SISTER -> { + related.types += CustomRelatedType.SISTER + related.types += RelatedType.SIBLING + } + Relation.TYPE_SPOUSE -> + related.types += RelatedType.SPOUSE Relation.TYPE_CUSTOM -> - row.getAsString(Relation.LABEL)?.split(",")?.forEach { - related.types += RelatedType.get(it.trim()) + StringUtils.trimToNull(row.getAsString(Relation.LABEL))?.let { typeStr -> + for (type in typeStr.split(',')) + related.types += RelatedType.get(type.lowercase().trim()) } } @@ -1039,28 +1064,47 @@ open class AndroidContact( batch.enqueue(builder) } - protected open fun insertRelation(batch: BatchOperation, related: Related) { - if (related.text.isNullOrEmpty()) + internal open fun insertRelation(batch: BatchOperation, related: Related) { + val name = StringUtils.trimToNull(related.text) ?: StringUtils.trimToNull(related.uri) + if (name.isNullOrEmpty()) return - var typeCode = Relation.TYPE_CUSTOM - - val labels = LinkedList() - for (type in related.types) - when (type) { - RelatedType.CHILD -> typeCode = Relation.TYPE_CHILD - RelatedType.SPOUSE -> typeCode = Relation.TYPE_PARTNER - RelatedType.FRIEND -> typeCode = Relation.TYPE_FRIEND - RelatedType.KIN -> typeCode = Relation.TYPE_RELATIVE - RelatedType.PARENT -> typeCode = Relation.TYPE_PARENT - else -> labels += type.value - } + var typeCode = when { + // specific Android types (not defined in RFC 6350) + related.types.contains(CustomRelatedType.ASSISTANT) -> Relation.TYPE_ASSISTANT + related.types.contains(CustomRelatedType.BROTHER) -> Relation.TYPE_BROTHER + related.types.contains(CustomRelatedType.DOMESTIC_PARTNER) -> Relation.TYPE_DOMESTIC_PARTNER + related.types.contains(CustomRelatedType.FATHER) -> Relation.TYPE_FATHER + related.types.contains(CustomRelatedType.MANAGER) -> Relation.TYPE_MANAGER + related.types.contains(CustomRelatedType.MOTHER) -> Relation.TYPE_MOTHER + related.types.contains(CustomRelatedType.PARTNER) -> Relation.TYPE_PARTNER + related.types.contains(CustomRelatedType.REFERRED_BY) -> Relation.TYPE_REFERRED_BY + related.types.contains(CustomRelatedType.SISTER) -> Relation.TYPE_SISTER + + // standard types (defined in RFC 6350) supported by Android + related.types.contains(RelatedType.FRIEND) -> Relation.TYPE_FRIEND + related.types.contains(RelatedType.KIN) -> Relation.TYPE_RELATIVE + related.types.contains(RelatedType.PARENT) -> Relation.TYPE_PARENT + related.types.contains(RelatedType.SPOUSE) -> Relation.TYPE_SPOUSE + + // other standard types are set as TYPE_CUSTOM + else -> Relation.TYPE_CUSTOM + } val builder = insertDataBuilder(Relation.RAW_CONTACT_ID) .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE) - .withValue(Relation.NAME, related.text) + .withValue(Relation.NAME, name) .withValue(Relation.TYPE, typeCode) - .withValue(Relation.LABEL, StringUtils.trimToNull(labels.joinToString(", "))) + + if (typeCode == Relation.TYPE_CUSTOM) { + if (related.types.isEmpty()) + related.types += CustomRelatedType.OTHER + + val types = related.types.map { type -> WordUtils.capitalize(type.value) } + val typesStr = types.joinToString(", ") + builder.withValue(Relation.LABEL, typesStr) + } + batch.enqueue(builder) } diff --git a/src/main/java/at/bitfire/vcard4android/BatchOperation.kt b/src/main/java/at/bitfire/vcard4android/BatchOperation.kt index 0ac1109..b7c9f9f 100644 --- a/src/main/java/at/bitfire/vcard4android/BatchOperation.kt +++ b/src/main/java/at/bitfire/vcard4android/BatchOperation.kt @@ -30,7 +30,7 @@ class BatchOperation( } - private val queue = LinkedList() + internal val queue = LinkedList() private var results = arrayOfNulls(0) diff --git a/src/main/java/at/bitfire/vcard4android/Contact.kt b/src/main/java/at/bitfire/vcard4android/Contact.kt index e94322a..5145833 100644 --- a/src/main/java/at/bitfire/vcard4android/Contact.kt +++ b/src/main/java/at/bitfire/vcard4android/Contact.kt @@ -8,7 +8,7 @@ package at.bitfire.vcard4android -import at.bitfire.vcard4android.property.CustomScribes +import at.bitfire.vcard4android.property.CustomScribes.registerCustomScribes import at.bitfire.vcard4android.property.XAbDate import ezvcard.VCardVersion import ezvcard.io.text.VCardReader @@ -110,9 +110,6 @@ class Contact { const val DATE_PARAMETER_OMIT_YEAR = "X-APPLE-OMIT-YEAR" const val DATE_PARAMETER_OMIT_YEAR_DEFAULT = 1604 - const val DATE_LABEL_ANNIVERSARY = "_\$!!\$_" - const val DATE_LABEL_OTHER = "_\$!!\$_" - /** * Parses an InputStream that contains a vCard. @@ -125,9 +122,9 @@ class Contact { */ fun fromReader(reader: Reader, downloader: Downloader?): List { // create new vCard reader and add custom scribes - val vCardReader = VCardReader(reader, VCardVersion.V3_0) // CardDAV requires vCard 3 or newer - CustomScribes.registerAt(vCardReader) - val vCards = vCardReader.readAll() + val vCards = VCardReader(reader, VCardVersion.V3_0) // CardDAV requires vCard 3 or newer + .registerCustomScribes() + .readAll() return vCards.map { vCard -> // convert every vCard to a Contact data object diff --git a/src/main/java/at/bitfire/vcard4android/ContactReader.kt b/src/main/java/at/bitfire/vcard4android/ContactReader.kt index 21c2078..46c78c2 100644 --- a/src/main/java/at/bitfire/vcard4android/ContactReader.kt +++ b/src/main/java/at/bitfire/vcard4android/ContactReader.kt @@ -1,8 +1,10 @@ package at.bitfire.vcard4android import at.bitfire.vcard4android.property.* +import at.bitfire.vcard4android.property.CustomScribes.registerCustomScribes import ezvcard.Ezvcard import ezvcard.VCard +import ezvcard.parameter.RelatedType import ezvcard.property.* import ezvcard.util.PartialDate import org.apache.commons.lang3.StringUtils @@ -157,10 +159,10 @@ class ContactReader internal constructor(val vCard: VCard, val downloader: Conta is XAbDate -> { checkPartialDate(prop) var label = findAndRemoveLabel(prop.group) - if (label == Contact.DATE_LABEL_OTHER) // drop Apple "Other" label + if (label == XAbLabel.APPLE_OTHER) // drop Apple "Other" label label = null - if (label == Contact.DATE_LABEL_ANNIVERSARY) { // convert custom date with Apple "Anniversary" label to real anniversary + if (label == XAbLabel.APPLE_ANNIVERSARY) { // convert custom date with Apple "Anniversary" label to real anniversary if (prop.date != null) c.anniversary = Anniversary(prop.date) else if (prop.partialDate != null) @@ -172,6 +174,54 @@ class ContactReader internal constructor(val vCard: VCard, val downloader: Conta is Related -> if (!prop.uri.isNullOrBlank() || !prop.text.isNullOrBlank()) c.relations += prop + is XAbRelatedNames -> { + val relation = Related() + relation.text = prop.value + + val labelStr = findAndRemoveLabel(prop.group) + when (labelStr) { + XAbRelatedNames.APPLE_ASSISTANT -> { + relation.types.add(CustomRelatedType.ASSISTANT) + relation.types.add(RelatedType.CO_WORKER) + } + XAbRelatedNames.APPLE_BROTHER -> { + relation.types.add(CustomRelatedType.BROTHER) + relation.types.add(RelatedType.SIBLING) + } + XAbRelatedNames.APPLE_CHILD -> + relation.types.add(RelatedType.CHILD) + XAbRelatedNames.APPLE_FATHER -> { + relation.types.add(CustomRelatedType.FATHER) + relation.types.add(RelatedType.PARENT) + } + XAbRelatedNames.APPLE_FRIEND -> + relation.types.add(RelatedType.FRIEND) + XAbRelatedNames.APPLE_MANAGER -> { + relation.types.add(CustomRelatedType.MANAGER) + relation.types.add(RelatedType.CO_WORKER) + } + XAbRelatedNames.APPLE_MOTHER -> { + relation.types.add(CustomRelatedType.MOTHER) + relation.types.add(RelatedType.PARENT) + } + XAbRelatedNames.APPLE_SISTER -> { + relation.types.add(CustomRelatedType.SISTER) + relation.types.add(RelatedType.SIBLING) + } + XAbRelatedNames.APPLE_PARENT -> + relation.types.add(RelatedType.PARENT) + XAbRelatedNames.APPLE_PARTNER -> + relation.types.add(CustomRelatedType.PARTNER) + XAbRelatedNames.APPLE_SPOUSE -> + relation.types.add(RelatedType.SPOUSE) + + is String /* label != null */ -> { + for (label in labelStr.split(',')) + relation.types.add(RelatedType.get(label.trim().lowercase())) + } + } + c.relations += relation + } is Note -> { StringUtils.trimToNull(prop.value)?.let { note -> @@ -223,12 +273,12 @@ class ContactReader internal constructor(val vCard: VCard, val downloader: Conta if (vCard.properties.isNotEmpty() || vCard.extendedProperties.isNotEmpty()) try { - val writer = Ezvcard + c.unknownProperties = Ezvcard .write(vCard) .prodId(false) .version(vCard.version) - CustomScribes.registerAt(writer) - c.unknownProperties = writer.go() + .registerCustomScribes() + .go() } catch(e: Exception) { Constants.log.log(Level.WARNING, "Couldn't serialize unknown properties, dropping them", e) } diff --git a/src/main/java/at/bitfire/vcard4android/ContactWriter.kt b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt index 7e882c2..eb95808 100644 --- a/src/main/java/at/bitfire/vcard4android/ContactWriter.kt +++ b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt @@ -1,13 +1,16 @@ package at.bitfire.vcard4android import at.bitfire.vcard4android.property.* +import at.bitfire.vcard4android.property.CustomScribes.registerCustomScribes import ezvcard.Ezvcard import ezvcard.VCard import ezvcard.VCardVersion import ezvcard.io.text.VCardWriter import ezvcard.parameter.ImageType +import ezvcard.parameter.RelatedType import ezvcard.property.* import org.apache.commons.lang3.StringUtils +import org.apache.commons.text.WordUtils import java.io.OutputStream import java.util.* import java.util.logging.Level @@ -71,7 +74,7 @@ class ContactWriter private constructor(val contact: Contact, val version: VCard addDates() for (relation in contact.relations) - vCard.addRelated(relation) + addRelation(relation) contact.note?.let { note -> vCard.addNote(note) } @@ -99,8 +102,8 @@ class ContactWriter private constructor(val contact: Contact, val version: VCard } else /* version == VCardVersion.V3_0 */ { // vCard3 doesn't support partial dates rewritePartialDate(anniversary) - // vCard3 doesn't support ANNIVERSARY, rewrite to X-ABDate - addLabeledProperty(LabeledProperty(XAbDate(anniversary.date), Contact.DATE_LABEL_ANNIVERSARY)) + // vCard3 doesn't support ANNIVERSARY, rewrite to X-ABDATE + addLabeledProperty(LabeledProperty(XAbDate(anniversary.date), XAbLabel.APPLE_ANNIVERSARY)) vCard.anniversary = null } } @@ -157,6 +160,53 @@ class ContactWriter private constructor(val contact: Contact, val version: VCard contact.jobDescription?.let { vCard.addRole(it) } } + private fun addRelation(relation: Related) { + if (version == VCardVersion.V4_0) + vCard.addRelated(relation) + + else /* version == VCardVersion.V3_0 */ { + val name = XAbRelatedNames(relation.text ?: relation.uri) + var label: String? = null + + val types = LinkedList(relation.types) + types.remove(CustomRelatedType.OTHER) // ignore this type (has to be inserted by ContactReader when no type is set) + + when { + types.contains(CustomRelatedType.ASSISTANT) -> + label = XAbRelatedNames.APPLE_ASSISTANT + types.contains(CustomRelatedType.BROTHER) -> + label = XAbRelatedNames.APPLE_BROTHER + types.contains(RelatedType.CHILD) -> + label = XAbRelatedNames.APPLE_CHILD + types.contains(CustomRelatedType.FATHER) -> + label = XAbRelatedNames.APPLE_FATHER + types.contains(RelatedType.FRIEND) -> + label = XAbRelatedNames.APPLE_FRIEND + types.contains(CustomRelatedType.MANAGER) -> + label = XAbRelatedNames.APPLE_MANAGER + types.contains(CustomRelatedType.MOTHER) -> + label = XAbRelatedNames.APPLE_MOTHER + types.contains(RelatedType.PARENT) -> + label = XAbRelatedNames.APPLE_PARENT + types.contains(CustomRelatedType.PARTNER) -> + label = XAbRelatedNames.APPLE_PARTNER + types.contains(CustomRelatedType.SISTER) -> + label = XAbRelatedNames.APPLE_SISTER + types.contains(RelatedType.SPOUSE) -> + label = XAbRelatedNames.APPLE_SPOUSE + + else -> { + if (relation.types.isEmpty()) + name.addParameter("TYPE", "other") + else + label = relation.types.map { type -> WordUtils.capitalize(type.value) }.joinToString(", ") + } + } + + addLabeledProperty(LabeledProperty(name, label)) + } + } + private fun addStructuredName() { val n = StructuredName() @@ -278,7 +328,7 @@ class ContactWriter private constructor(val contact: Contact, val version: VCard val writer = VCardWriter(stream, version).apply { isAddProdId = Contact.productID == null - CustomScribes.registerAt(scribeIndex) + registerCustomScribes() // include trailing semicolons for maximum compatibility isIncludeTrailingSemicolons = true diff --git a/src/main/java/at/bitfire/vcard4android/property/CustomRelatedType.kt b/src/main/java/at/bitfire/vcard4android/property/CustomRelatedType.kt new file mode 100644 index 0000000..74f426d --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/CustomRelatedType.kt @@ -0,0 +1,19 @@ +package at.bitfire.vcard4android.property + +import ezvcard.parameter.RelatedType + +object CustomRelatedType { + + val ASSISTANT = RelatedType.get("assistant") + val BROTHER = RelatedType.get("brother") + val DOMESTIC_PARTNER = RelatedType.get("domestic-partner") + val FATHER = RelatedType.get("father") + val MANAGER = RelatedType.get("manager") + val MOTHER = RelatedType.get("mother") + val PARTNER = RelatedType.get("partner") + val REFERRED_BY = RelatedType.get("referred-by") + val SISTER = RelatedType.get("sister") + + val OTHER = RelatedType.get("other") + +} \ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt b/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt index 84fc41b..6a35f17 100644 --- a/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt +++ b/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt @@ -3,6 +3,7 @@ package at.bitfire.vcard4android.property import ezvcard.io.chain.ChainingTextWriter import ezvcard.io.scribe.ScribeIndex import ezvcard.io.text.VCardReader +import ezvcard.io.text.VCardWriter object CustomScribes { @@ -10,6 +11,7 @@ object CustomScribes { val customScribes = arrayOf( XAbDate.Scribe, XAbLabel.Scribe, + XAbRelatedNames.Scribe, XAddressBookServerKind.Scribe, XAddressBookServerMember.Scribe, XPhoneticFirstName.Scribe, @@ -18,20 +20,22 @@ object CustomScribes { XSip.Scribe ) - fun registerAt(writer: ChainingTextWriter) { + + fun ChainingTextWriter.registerCustomScribes(): ChainingTextWriter { for (scribe in customScribes) - writer.register(scribe) + register(scribe) + return this } - fun registerAt(index: ScribeIndex) { + fun VCardReader.registerCustomScribes(): VCardReader { for (scribe in customScribes) - index.register(scribe) + scribeIndex.register(scribe) + return this } - fun registerAt(reader: VCardReader): VCardReader { + fun VCardWriter.registerCustomScribes() { for (scribe in customScribes) - reader.scribeIndex.register(scribe) - return reader + scribeIndex.register(scribe) } } diff --git a/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt b/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt index 103f153..1d07f64 100644 --- a/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt +++ b/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt @@ -5,6 +5,13 @@ import ezvcard.property.TextProperty class XAbLabel(value: String?): TextProperty(value) { + companion object { + + const val APPLE_ANNIVERSARY = "_\$!!\$_" + const val APPLE_OTHER = "_\$!!\$_" + + } + object Scribe : StringPropertyScribe(XAbLabel::class.java, "X-ABLABEL") { diff --git a/src/main/java/at/bitfire/vcard4android/property/XAbRelatedNames.kt b/src/main/java/at/bitfire/vcard4android/property/XAbRelatedNames.kt new file mode 100644 index 0000000..7875cdd --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XAbRelatedNames.kt @@ -0,0 +1,31 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.property.TextProperty + +class XAbRelatedNames(value: String?): TextProperty(value) { + + companion object { + + const val APPLE_ASSISTANT = "_\$!!\$_" + const val APPLE_BROTHER = "_\$!!\$_" + const val APPLE_CHILD = "_\$!!\$_" + const val APPLE_FATHER = "_\$!!\$_" + const val APPLE_FRIEND = "_\$!!\$_" + const val APPLE_MANAGER = "_\$!!\$_" + const val APPLE_MOTHER = "_\$!!\$_" + const val APPLE_PARENT = "_\$!!\$_" + const val APPLE_PARTNER = "_\$!!\$_" + const val APPLE_SISTER = "_\$!!\$_" + const val APPLE_SPOUSE = "_\$!!\$_" + + } + + object Scribe : + StringPropertyScribe(XAbRelatedNames::class.java, "X-ABRELATEDNAMES") { + + override fun _parseValue(value: String?) = XAbRelatedNames(value) + + } + +} \ No newline at end of file diff --git a/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt b/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt index 51007dc..9cfd836 100644 --- a/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt +++ b/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt @@ -422,7 +422,7 @@ class ContactReaderTest { val date = Date(101, 6, 30, 0, 0, 0) val c = ContactReader.fromVCard(VCard().apply { addProperty(XAbDate(date).apply { group = "test1" }) - addProperty(XAbLabel(Contact.DATE_LABEL_ANNIVERSARY).apply { group = "test1" }) + addProperty(XAbLabel(XAbLabel.APPLE_ANNIVERSARY).apply { group = "test1" }) }) assertEquals(0, c.customDates.size) assertEquals(Anniversary(date), c.anniversary) @@ -433,7 +433,7 @@ class ContactReaderTest { val date = Date(101, 6, 30, 0, 0, 0) val c = ContactReader.fromVCard(VCard().apply { addProperty(XAbDate(date).apply { group = "test1" }) - addProperty(XAbLabel(Contact.DATE_LABEL_OTHER).apply { group = "test1" }) + addProperty(XAbLabel(XAbLabel.APPLE_OTHER).apply { group = "test1" }) }) assertEquals(date, c.customDates.first.property.date) assertNull(c.customDates.first.label) @@ -451,6 +451,52 @@ class ContactReaderTest { } + @Test + fun testXAbRelatedNames_Sister() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbRelatedNames("My Sis").apply { group = "item1" }) + addProperty(XAbLabel(XAbRelatedNames.APPLE_SISTER).apply { group = "item1" }) + }) + assertEquals("My Sis", c.relations.first.text) + assertTrue(c.relations.first.types.contains(RelatedType.SIBLING)) + assertTrue(c.relations.first.types.contains(CustomRelatedType.SISTER)) + } + + @Test + fun testXAbRelatedNames_Custom() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbRelatedNames("Someone Other").apply { group = "item1" }) + addProperty(XAbLabel("Someone").apply { group = "item1" }) + }) + assertEquals("Someone Other", c.relations.first.text) + assertEquals(1, c.relations.first.types.size) + assertTrue(c.relations.first.types.contains(RelatedType.get("someone"))) + } + + @Test + fun testXAbRelatedNames_Custom_Acquitance() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbRelatedNames("Someone Other").apply { group = "item1" }) + addProperty(XAbLabel(RelatedType.ACQUAINTANCE.value).apply { group = "item1" }) + }) + assertEquals("Someone Other", c.relations.first.text) + assertEquals(1, c.relations.size) + assertTrue(c.relations.first.types.contains(RelatedType.ACQUAINTANCE)) + } + + @Test + fun testXAbRelatedNames_Custom_TwoValues() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbRelatedNames("Someone Other").apply { group = "item1" }) + addProperty(XAbLabel("dog, cat").apply { group = "item1" }) + }) + assertEquals("Someone Other", c.relations.first.text) + assertEquals(2, c.relations.first.types.size) + assertTrue(c.relations.first.types.contains(RelatedType.get("cat"))) + assertTrue(c.relations.first.types.contains(RelatedType.get("dog"))) + } + + @Test fun testXAddressBookServerKind_Group() { val c = ContactReader.fromVCard(VCard().apply { diff --git a/src/test/java/at/bitfire/vcard4android/ContactTest.kt b/src/test/java/at/bitfire/vcard4android/ContactTest.kt index f8aa19a..d003f26 100644 --- a/src/test/java/at/bitfire/vcard4android/ContactTest.kt +++ b/src/test/java/at/bitfire/vcard4android/ContactTest.kt @@ -200,7 +200,7 @@ class ContactTest { assertEquals("Ägidius", rel.text) rel = c.relations[1] assertTrue(rel.types.contains(RelatedType.PARENT)) - assertEquals("muuum@example.com", rel.uri) + assertEquals("muuum@example.com", rel.text) // PHOTO javaClass.classLoader!!.getResourceAsStream("lol.jpg").use { photo -> diff --git a/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt index bab087b..9331786 100644 --- a/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt +++ b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt @@ -2,9 +2,9 @@ package at.bitfire.vcard4android import at.bitfire.vcard4android.property.* import ezvcard.VCard -import ezvcard.VCardDataType import ezvcard.VCardVersion import ezvcard.parameter.ImageType +import ezvcard.parameter.RelatedType import ezvcard.property.* import ezvcard.util.PartialDate import org.junit.Assert.* @@ -278,9 +278,98 @@ class ContactWriterTest { @Test - fun testRelation() { + fun testRelation_vCard3_Assistant() { // combination of custom type (assistant) and vCard4 standard type (co-worker) + val vCard = generate(version = VCardVersion.V3_0) { + relations += Related().apply { + text = "My Assistant" + types.add(CustomRelatedType.ASSISTANT) + types.add(RelatedType.CO_WORKER) + } + } + vCard.getProperty(XAbRelatedNames::class.java).apply { + assertEquals("My Assistant", value) + assertEquals("item1", group) + } + vCard.getProperty(XAbLabel::class.java).apply { + assertEquals(XAbRelatedNames.APPLE_ASSISTANT, value) + assertEquals("item1", group) + } + assertTrue(vCard.relations.isEmpty()) + } + + @Test + fun testRelation_vCard3_Child() { // vCard4 standard type + val vCard = generate(version = VCardVersion.V3_0) { + relations += Related().apply { + text = "My Child" + types.add(RelatedType.CHILD) + } + } + vCard.getProperty(XAbRelatedNames::class.java).apply { + assertEquals("My Child", value) + assertEquals("item1", group) + } + vCard.getProperty(XAbLabel::class.java).apply { + assertEquals(XAbRelatedNames.APPLE_CHILD, value) + assertEquals("item1", group) + } + assertTrue(vCard.relations.isEmpty()) + } + + @Test + fun testRelation_vCard3_Custom() { + val vCard = generate(version = VCardVersion.V3_0) { + relations += Related().apply { + text = "Someone" + types.add(RelatedType.get("Custom Relationship")) + } + } + vCard.getProperty(XAbRelatedNames::class.java).apply { + assertEquals("Someone", value) + assertEquals("item1", group) + } + vCard.getProperty(XAbLabel::class.java).apply { + assertEquals("Custom Relationship", value) + assertEquals("item1", group) + } + assertTrue(vCard.relations.isEmpty()) + } + + @Test + fun testRelation_vCard3_Other() { + val rel = Related.email("bigbrother@example.com") + val vCard = generate(version = VCardVersion.V3_0) { relations += rel } + vCard.getProperty(XAbRelatedNames::class.java).apply { + assertEquals("mailto:bigbrother@example.com", value) + assertEquals("other", getParameter("TYPE")) + assertNull(group) + } + assertTrue(vCard.relations.isEmpty()) + } + + @Test + fun testRelation_vCard3_Partner() { // custom type + val vCard = generate(version = VCardVersion.V3_0) { + relations += Related().apply { + text = "My Partner" + types.add(CustomRelatedType.PARTNER) + } + } + vCard.getProperty(XAbRelatedNames::class.java).apply { + assertEquals("My Partner", value) + assertEquals("item1", group) + } + vCard.getProperty(XAbLabel::class.java).apply { + assertEquals(XAbRelatedNames.APPLE_PARTNER, value) + assertEquals("item1", group) + } + assertTrue(vCard.relations.isEmpty()) + } + + @Test + fun testRelation_vCard4() { val rel = Related.email("bigbrother@example.com") - val vCard = generate { relations += rel } + val vCard = generate(version = VCardVersion.V4_0) { relations += rel } assertEquals(rel, vCard.relations.first()) } -- cgit v1.2.3