Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/bitfireAT/vcard4android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRicki Hirner <hirner@bitfire.at>2021-07-30 19:15:07 +0300
committerRicki Hirner <hirner@bitfire.at>2021-07-31 12:15:59 +0300
commit07f62f52cc374c584e9263a25c72cf10a54b6dc7 (patch)
tree976b6c3084751b034abd6df3d0b9946e0022f38e
parentf95c5b0fb29346976b319282c514496b50335e12 (diff)
Better conversion of Contact <-> VCard
* add converter classes: ContactReader, ContactWriter * consequently use custom scribes whenever possible * add tests
-rw-r--r--README.md10
-rw-r--r--src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt6
-rw-r--r--src/main/java/at/bitfire/vcard4android/AndroidContact.kt18
-rw-r--r--src/main/java/at/bitfire/vcard4android/Contact.kt433
-rw-r--r--src/main/java/at/bitfire/vcard4android/ContactReader.kt269
-rw-r--r--src/main/java/at/bitfire/vcard4android/ContactWriter.kt295
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/AbLabel.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt19
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt18
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt37
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XAbDate.kt23
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt15
-rw-r--r--src/main/java/at/bitfire/vcard4android/property/XSip.kt19
-rw-r--r--src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt604
-rw-r--r--src/test/java/at/bitfire/vcard4android/ContactTest.kt177
-rw-r--r--src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt492
-rw-r--r--src/test/java/at/bitfire/vcard4android/EzVCardTest.kt19
-rw-r--r--src/test/resources/allfields-vcard3.vcf6
-rw-r--r--src/test/resources/invalid-rev.vcf21
27 files changed, 1921 insertions, 695 deletions
diff --git a/README.md b/README.md
index 06e89f7..80e7072 100644
--- a/README.md
+++ b/README.md
@@ -18,23 +18,23 @@ by Google LLC. Android is a trademark of Google LLC._
Generated KDoc: https://bitfireAT.gitlab.io/vcard4android/dokka/vcard4android/
-Discussion: https://forums.bitfire.at/category/18/libraries
-
## Contact
```
-bitfire web engineering – Stockmann, Hirner GesnbR
+bitfire web engineering GmbH
Florastraße 27
2540 Bad Vöslau, AUSTRIA
```
-Email: [play@bitfire.at](mailto:play@bitfire.at) (do not use this)
+Email: [play@bitfire.at](mailto:play@bitfire.at) (please do not use this)
+
+Discussion: https://forums.bitfire.at/category/18/libraries
## License
-Copyright (C) bitfire web engineering (Ricki Hirner, Bernhard Stockmann).
+Copyright (C) Ricki Hirner (bitfire web engineering GmbH) and contributors.
This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome
to redistribute it under the conditions of the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.html).
diff --git a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt
index 2244a26..5afd43d 100644
--- a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt
+++ b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt
@@ -18,6 +18,7 @@ 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.XAbDate
import ezvcard.VCardVersion
import ezvcard.parameter.EmailType
import ezvcard.property.Address
@@ -75,6 +76,7 @@ class AndroidContactTest {
vcard.phoneticMiddleName = "Mittelerde"
vcard.phoneticFamilyName = "Fämilie"
vcard.birthDay = Birthday(SimpleDateFormat("yyyy-MM-dd").parse("1980-04-16"))
+ vcard.customDates += LabeledProperty(XAbDate(PartialDate.parse("--0102")), "Custom Date")
vcard.photo = samplePhoto
val contact = AndroidContact(addressBook, vcard, null, null)
@@ -92,6 +94,7 @@ class AndroidContactTest {
assertEquals(vcard.phoneticMiddleName, vcard2.phoneticMiddleName)
assertEquals(vcard.phoneticFamilyName, vcard2.phoneticFamilyName)
assertEquals(vcard.birthDay, vcard2.birthDay)
+ assertArrayEquals(vcard.customDates.toArray(), vcard2.customDates.toArray())
assertNotNull(vcard.photo)
} finally {
contact2.delete()
@@ -179,8 +182,7 @@ class AndroidContactTest {
* So, ADR value components may contain DQUOTE (0x22) and don't have to be encoded as defined in RFC 6868 */
val os = ByteArrayOutputStream()
- contact.write(VCardVersion.V4_0, GroupMethod.GROUP_VCARDS, os)
- Constants.log.info(os.toString())
+ contact.writeVCard(VCardVersion.V4_0, GroupMethod.GROUP_VCARDS, os)
assertTrue(os.toString().contains("ADR;LABEL=My ^'Label^'\\nLine 2:;;Street \"Address\";;;;"))
}
diff --git a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt
index acce3b6..2d5172d 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.XAbDate
import ezvcard.parameter.*
import ezvcard.property.*
import ezvcard.util.PartialDate
@@ -456,6 +457,12 @@ open class AndroidContact(
contact!!.anniversary = if (full != null) Anniversary(full) else Anniversary(partial)
Event.TYPE_BIRTHDAY ->
contact!!.birthDay = if (full != null) Birthday(full) else Birthday(partial)
+ Event.TYPE_OTHER,
+ Event.TYPE_CUSTOM -> {
+ val abDate = if (full != null) XAbDate(full) else XAbDate(partial)
+ val label = StringUtils.trimToNull(row.getAsString(Event.LABEL))
+ contact!!.customDates += LabeledProperty(abDate, label)
+ }
}
}
@@ -641,6 +648,11 @@ open class AndroidContact(
contact.anniversary?.let { insertEvent(batch, Event.TYPE_ANNIVERSARY, it) }
contact.birthDay?.let { insertEvent(batch, Event.TYPE_BIRTHDAY, it) }
+ for (customDate in contact.customDates)
+ if (customDate.label == null)
+ insertEvent(batch, Event.TYPE_OTHER, customDate.property)
+ else
+ insertEvent(batch, Event.TYPE_CUSTOM, customDate.property, label = customDate.label)
}
protected open fun insertStructuredName(batch: BatchOperation) {
@@ -1001,7 +1013,7 @@ open class AndroidContact(
batch.enqueue(builder)
}
- protected open fun insertEvent(batch: BatchOperation, type: Int, dateOrTime: DateOrTimeProperty) {
+ protected open fun insertEvent(batch: BatchOperation, type: Int, dateOrTime: DateOrTimeProperty, label: String? = null) {
val dateStr: String
dateStr = when {
dateOrTime.date != null -> {
@@ -1020,6 +1032,10 @@ open class AndroidContact(
.withValue(Event.MIMETYPE, Event.CONTENT_ITEM_TYPE)
.withValue(Event.TYPE, type)
.withValue(Event.START_DATE, dateStr)
+
+ if (label != null)
+ builder.withValue(Event.LABEL, label)
+
batch.enqueue(builder)
}
diff --git a/src/main/java/at/bitfire/vcard4android/Contact.kt b/src/main/java/at/bitfire/vcard4android/Contact.kt
index 7ccf52c..e94322a 100644
--- a/src/main/java/at/bitfire/vcard4android/Contact.kt
+++ b/src/main/java/at/bitfire/vcard4android/Contact.kt
@@ -8,29 +8,32 @@
package at.bitfire.vcard4android
-import at.bitfire.vcard4android.property.*
-import ezvcard.Ezvcard
-import ezvcard.VCard
+import at.bitfire.vcard4android.property.CustomScribes
+import at.bitfire.vcard4android.property.XAbDate
import ezvcard.VCardVersion
import ezvcard.io.text.VCardReader
-import ezvcard.io.text.VCardWriter
import ezvcard.parameter.EmailType
-import ezvcard.parameter.ImageType
import ezvcard.parameter.TelephoneType
import ezvcard.property.*
-import ezvcard.util.PartialDate
-import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.apache.commons.lang3.builder.ReflectionToStringBuilder
import java.io.IOException
import java.io.OutputStream
import java.io.Reader
-import java.net.URI
-import java.net.URISyntaxException
import java.util.*
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.logging.Level
+/**
+ * Data class for a contact; between vCards and the Android contacts provider.
+ *
+ * Data shall be stored without workarounds in the most appropriate form. For instance,
+ * an anniversary should be stored as [anniversary] and not in [customDates] with a
+ * proprietary label ([Contact.DATE_LABEL_ANNIVERSARY]).
+ *
+ * vCards are parsed to [Contact]s by [ContactReader].
+ * [Contact]s are written to vCards by [ContactWriter].
+ *
+ * [Contact]s are written to and read from the Android storage by [AndroidContact].
+ */
class Contact {
var uid: String? = null
@@ -69,7 +72,7 @@ class Contact {
var anniversary: Anniversary? = null
var birthDay: Birthday? = null
- val customDates = LinkedList<LabeledProperty<DateOrTimeProperty>>()
+ val customDates = LinkedList<LabeledProperty<XAbDate>>()
var photo: ByteArray? = null
@@ -78,22 +81,11 @@ class Contact {
companion object {
-
- /** list of all custom scribes (will be registered to readers/writers) **/
- val customScribes = arrayOf(
- AbLabel.Scribe,
- AddressBookServerKind.Scribe,
- AddressBookServerMember.Scribe,
- PhoneticFirstName.Scribe,
- PhoneticMiddleName.Scribe,
- PhoneticLastName.Scribe
- )
-
// productID (if set) will be used to generate a PRODID property.
// You may set this statically from the calling application.
var productID: String? = null
- const val PROPERTY_SIP = "X-SIP"
+ const val LABEL_GROUP_PREFIX = "item"
// TEL x-types to store Android types
val PHONE_TYPE_CALLBACK = TelephoneType.get("x-callback")!!
@@ -118,6 +110,9 @@ 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 = "_\$!<Anniversary>!\$_"
+ const val DATE_LABEL_OTHER = "_\$!<Other>!\$_"
+
/**
* Parses an InputStream that contains a vCard.
@@ -129,391 +124,24 @@ class Contact {
* @throws ezvcard.io.CannotParseException when the vCard can't be parsed
*/
fun fromReader(reader: Reader, downloader: Downloader?): List<Contact> {
- // create new vCard reader and add custom scribes to tell the reader how to read custom properties
+ // create new vCard reader and add custom scribes
val vCardReader = VCardReader(reader, VCardVersion.V3_0) // CardDAV requires vCard 3 or newer
- for (scribe in customScribes)
- vCardReader.scribeIndex.register(scribe)
-
- val vcards = vCardReader.readAll()
- val contacts = LinkedList<Contact>()
- vcards?.forEach { contacts += fromVCard(it, downloader) }
- return contacts
- }
-
- private fun fromVCard(vCard: VCard, downloader: Downloader?): Contact {
- val c = Contact()
-
- // get X-ABLabels
- val labels = vCard.getProperties(AbLabel::class.java)
-
- fun findAndRemoveLabel(group: String?): String? {
- if (group == null)
- return null
-
- for (label in labels) {
- if (label.group.equals(group, true)) {
- vCard.removeProperty(label)
- return label.value
- }
- }
-
- return null
- }
-
- // process standard properties
- val toRemove = LinkedList<VCardProperty>()
- for (prop in vCard.properties) {
- var remove = true
- when (prop) {
- is Uid -> c.uid = uriToUID(prop.value)
-
- is Kind, is AddressBookServerKind -> {
- val kindProp = prop as Kind
- c.group = kindProp.isGroup
- }
- is Member, is AddressBookServerMember -> {
- val uriProp = prop as Member
- uriToUID(uriProp.uri)?.let { c.members += it }
- }
-
- is FormattedName -> c.displayName = prop.value.trim()
- is PhoneticFirstName -> c.phoneticGivenName = StringUtils.trimToNull(prop.value)
- is PhoneticMiddleName -> c.phoneticMiddleName = StringUtils.trimToNull(prop.value)
- is PhoneticLastName -> c.phoneticFamilyName = StringUtils.trimToNull(prop.value)
- is StructuredName -> {
- c.prefix = StringUtils.trimToNull(prop.prefixes.joinToString(" "))
- c.givenName = StringUtils.trimToNull(prop.given)
- c.middleName = StringUtils.trimToNull(prop.additionalNames.joinToString(" "))
- c.familyName = StringUtils.trimToNull(prop.family)
- c.suffix = StringUtils.trimToNull(prop.suffixes.joinToString(" "))
- }
- is Nickname -> c.nickName = LabeledProperty(prop, findAndRemoveLabel(prop.group))
-
- is Organization -> c.organization = prop
- is Title -> c.jobTitle = StringUtils.trimToNull(prop.value)
- is Role -> c.jobDescription = StringUtils.trimToNull(prop.value)
-
- is Telephone -> if (!prop.text.isNullOrBlank())
- c.phoneNumbers += LabeledProperty(prop, findAndRemoveLabel(prop.group))
- is Email -> if (!prop.value.isNullOrBlank())
- c.emails += LabeledProperty(prop, findAndRemoveLabel(prop.group))
- is Impp -> c.impps += LabeledProperty(prop, findAndRemoveLabel(prop.group))
- is Address -> c.addresses += LabeledProperty(prop, findAndRemoveLabel(prop.group))
-
- is Note -> c.note = if (c.note.isNullOrEmpty()) prop.value else "${c.note}\n\n\n${prop.value}"
- is Url -> c.urls += LabeledProperty(prop, findAndRemoveLabel(prop.group))
- is Categories -> c.categories.addAll(prop.values)
-
- is Birthday -> c.birthDay = checkVCard3PartialDate(prop)
- is Anniversary -> c.anniversary = checkVCard3PartialDate(prop)
-
- is Related -> StringUtils.trimToNull(prop.text)?.let { c.relations += prop }
- is Photo ->
- c.photo = prop.data ?: prop.url?.let { url ->
- downloader?.let {
- Constants.log.info("Downloading photo from $url")
- it.download(url, "image/*")
- }
- }
-
- // remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions
- is Logo, is Sound -> { /* remove = true */ }
-
- // remove properties that don't apply anymore
- is ProductId,
- is Revision,
- is SortString,
- is Source -> {
- /* remove = true */
- }
-
- else -> remove = false // don't remove unknown properties
- }
-
- if (remove)
- toRemove += prop
- }
- toRemove.forEach { vCard.removeProperty(it) }
-
- // process extended properties
- val extToRemove = LinkedList<RawProperty>()
- for (prop in vCard.extendedProperties) {
- var remove = true
- when (prop.propertyName) {
- PROPERTY_SIP -> c.impps += LabeledProperty(Impp("sip", prop.value), findAndRemoveLabel(prop.group))
- else -> remove = false // don't remove unknown extended properties
- }
-
- if (remove)
- extToRemove += prop
- }
- extToRemove.distinct().forEach { vCard.removeExtendedProperty(it.propertyName) }
+ CustomScribes.registerAt(vCardReader)
+ val vCards = vCardReader.readAll()
- if (c.uid == null) {
- Constants.log.warning("Received vCard without UID, generating new one")
- c.uid = UUID.randomUUID().toString()
- }
-
- // remove properties which
- // - couldn't be parsed (and thus are treated as extended/unknown properties), and
- // - must occur once max.
- arrayOf("ANNIVERSARY", "BDAY", "KIND", "N", "PRODID", "REV", "UID").forEach {
- vCard.removeExtendedProperty(it)
- }
-
- // store all remaining properties into unknownProperties
- if (vCard.properties.isNotEmpty() || vCard.extendedProperties.isNotEmpty())
- try {
- val writer = Ezvcard.write(vCard)
- for (scribe in customScribes) // unknwown properties may contain custom scribes like an unmatched X-ABLabel
- writer.register(scribe)
- c.unknownProperties = writer.go()
- } catch(e: Exception) {
- Constants.log.log(Level.WARNING, "Couldn't serialize unknown properties, dropping them", e)
- }
-
- return c
- }
-
- private fun uriToUID(uriString: String?): String? {
- if (uriString == null)
- return null
- return try {
- val uri = URI(uriString)
- when {
- uri.scheme == null ->
- uri.schemeSpecificPart
- uri.scheme.equals("urn", true) && uri.schemeSpecificPart.startsWith("uuid:", true) ->
- uri.schemeSpecificPart.substring(5)
- else ->
- null
- }
- } catch(e: URISyntaxException) {
- Constants.log.warning("Invalid URI for UID: $uriString")
- uriString
+ return vCards.map { vCard ->
+ // convert every vCard to a Contact data object
+ ContactReader.fromVCard(vCard, downloader)
}
}
- private fun<T: DateOrTimeProperty> checkVCard3PartialDate(property: T): T {
- property.date?.let { date ->
- property.getParameter(DATE_PARAMETER_OMIT_YEAR)?.let { omitYearStr ->
- try {
- val omitYear = Integer.parseInt(omitYearStr)
- val cal = GregorianCalendar.getInstance()
- cal.time = date
- if (cal.get(GregorianCalendar.YEAR) == omitYear) {
- val partial = PartialDate.builder()
- .date(cal.get(GregorianCalendar.DAY_OF_MONTH))
- .month(cal.get(GregorianCalendar.MONTH) + 1)
- .build()
- property.partialDate = partial
- }
- } catch(e: NumberFormatException) {
- Constants.log.log(Level.WARNING, "Unparseable $DATE_PARAMETER_OMIT_YEAR")
- } finally {
- property.removeParameter(DATE_PARAMETER_OMIT_YEAR)
- }
- }
- }
- return property
- }
-
}
@Throws(IOException::class)
- fun write(vCardVersion: VCardVersion, groupMethod: GroupMethod, os: OutputStream) {
- var vCard = VCard()
- try {
- unknownProperties?.let { vCard = Ezvcard.parse(unknownProperties).first() }
- } catch (e: Exception) {
- Constants.log.fine("Couldn't parse original vCard with retained properties")
- }
-
- // UID
- uid?.let { vCard.uid = Uid(it) }
- // PRODID
- productID?.let { vCard.setProductId(it) }
-
- // group support
- if (group && groupMethod == GroupMethod.GROUP_VCARDS) {
- if (vCardVersion == VCardVersion.V4_0) {
- vCard.kind = Kind.group()
- members.forEach { vCard.members += Member("urn:uuid:$it") }
- } else { // "vCard4 as vCard3" (Apple-style)
- vCard.setProperty(AddressBookServerKind(Kind.GROUP))
- members.forEach { vCard.addProperty(AddressBookServerMember("urn:uuid:$it")) }
- }
- }
-
- // FN
- var fn = displayName
- if (fn.isNullOrEmpty())
- organization?.let {
- for (part in it.values) {
- fn = part
- if (!fn.isNullOrEmpty())
- break
- }
- }
- if (fn.isNullOrEmpty())
- nickName?.let { fn = it.property.values.firstOrNull() }
- if (fn.isNullOrEmpty())
- emails.firstOrNull()?.let { fn = it.property.value }
- if (fn.isNullOrEmpty())
- phoneNumbers.firstOrNull()?.let { fn = it.property.text }
- if (fn.isNullOrEmpty())
- fn = uid ?: ""
- vCard.setFormattedName(fn)
-
- // N
- if (prefix != null || familyName != null || middleName != null || givenName != null || suffix != null) {
- val n = StructuredName()
- prefix?.let { it.split(' ').forEach { singlePrefix -> n.prefixes += singlePrefix } }
- n.given = givenName
- middleName?.let { it.split(' ').forEach { singleName -> n.additionalNames += singleName } }
- n.family = familyName
- suffix?.let { it.split(' ').forEach { singleSuffix -> n.suffixes += singleSuffix } }
- vCard.structuredName = n
-
- } else if (vCardVersion == VCardVersion.V3_0) {
- // (only) vCard 3 requires N [RFC 2426 3.1.2]
- if (group && groupMethod == GroupMethod.GROUP_VCARDS) {
- val n = StructuredName()
- n.family = fn
- vCard.structuredName = n
- } else
- vCard.structuredName = StructuredName()
- }
-
- // phonetic names
- phoneticGivenName?.let { vCard.addProperty(PhoneticFirstName(it)) }
- phoneticMiddleName?.let { vCard.addProperty(PhoneticMiddleName(it)) }
- phoneticFamilyName?.let { vCard.addProperty(PhoneticLastName(it)) }
-
- // ORG, TITLE, ROLE
- organization?.let { vCard.organization = it }
- jobTitle?.let { vCard.addTitle(it) }
- jobDescription?.let { vCard.addRole(it) }
-
- // will be used to count "itemXX." property groups
- val labelIterator = AtomicInteger()
- // TODO move function outside; make clear that it modifies labeledProperty.property
- fun addLabel(labeledProperty: LabeledProperty<VCardProperty>) {
- labeledProperty.label?.let { label ->
- val group = "group${labelIterator.incrementAndGet()}"
- labeledProperty.property.group = group
-
- val abLabel = AbLabel(label)
- abLabel.group = group
- vCard.addProperty(abLabel)
- }
- }
-
- // NICKNAME
- nickName?.let { labeledNickName ->
- vCard.addNickname(labeledNickName.property)
- addLabel(labeledNickName)
- }
-
- // TEL
- for (labeledPhone in phoneNumbers) {
- vCard.addTelephoneNumber(labeledPhone.property)
- addLabel(labeledPhone)
- }
-
- // EMAIL
- for (labeledEmail in emails) {
- vCard.addEmail(labeledEmail.property)
- addLabel(labeledEmail)
- }
-
- // IMPP
- for (labeledImpp in impps) {
- vCard.addImpp(labeledImpp.property)
- addLabel(labeledImpp)
- }
-
- // ADR
- for (labeledAddress in addresses) {
- val address = labeledAddress.property
- vCard.addAddress(address)
- addLabel(labeledAddress)
- }
-
- // NOTE
- note?.let { vCard.addNote(it) }
-
- // URL
- for (labeledUrl in urls) {
- val url = labeledUrl.property
- vCard.addUrl(url)
- addLabel(labeledUrl)
- }
-
- // CATEGORIES
- if (!categories.isEmpty()) {
- val cat = Categories()
- cat.values.addAll(categories)
- vCard.categories = cat
- }
-
- // ANNIVERSARY, BDAY
- fun<T: DateOrTimeProperty> dateOrPartialDate(prop: T, generator: (Date) -> T): T? {
- if (vCardVersion == VCardVersion.V4_0 || prop.date != null)
- return prop
- else prop.partialDate?.let { partial ->
- // vCard 3: partial date with month and day, but without year
- if (partial.date != null && partial.month != null) {
- return if (partial.year != null)
- // partial date is a complete date
- prop
- else {
- // vCard 3: partial date with month and day, but without year
- val fakeCal = GregorianCalendar.getInstance()
- fakeCal.set(DATE_PARAMETER_OMIT_YEAR_DEFAULT, partial.month - 1, partial.date)
- val fakeProp = generator(fakeCal.time)
- fakeProp.addParameter(DATE_PARAMETER_OMIT_YEAR, DATE_PARAMETER_OMIT_YEAR_DEFAULT.toString())
- fakeProp
- }
- }
- }
- return null
- }
- anniversary?.let { vCard.anniversary = dateOrPartialDate(it) { time -> Anniversary(time, false) } }
- birthDay?.let { vCard.birthday = dateOrPartialDate(it) { time -> Birthday(time, false) } }
-
- // RELATED
- relations.forEach { vCard.addRelated(it) }
-
- // PHOTO
- photo?.let { vCard.addPhoto(Photo(photo, ImageType.JPEG)) }
-
- // REV
- vCard.revision = Revision.now()
-
- // validate vCard and log results
- val validation = vCard.validate(vCardVersion)
- if (!validation.isEmpty) {
- val msgs = LinkedList<String>()
- for ((key, warnings) in validation)
- msgs += " * " + key.javaClass.simpleName + " - " + warnings.joinToString(" | ")
- Constants.log.log(Level.WARNING, "vCard validation warnings", msgs.joinToString(","))
- }
-
- // generate VCARD
- val writer = Ezvcard
- .write(vCard)
- .version(vCardVersion)
- .versionStrict(false) // allow vCard4 properties in vCard3s
- .caretEncoding(true) // enable RFC 6868 support
- .prodId(productID == null)
-
- // tell the writer how to write custom properties
- for (scribe in customScribes)
- writer.register(scribe)
-
- return writer .go(os)
+ fun writeVCard(vCardVersion: VCardVersion, groupMethod: GroupMethod, os: OutputStream) {
+ val generator = ContactWriter.fromContact(this, vCardVersion, groupMethod)
+ generator.writeVCard(os)
}
@@ -525,9 +153,8 @@ class Contact {
phoneticGivenName, phoneticMiddleName, phoneticFamilyName,
nickName,
organization, jobTitle, jobDescription,
- phoneNumbers, emails, impps, addresses,
- /* categories, */ urls, relations,
- note, anniversary, birthDay,
+ phoneNumbers, emails, impps, addresses, /* categories, */ urls, relations,
+ note, anniversary, birthDay, customDates,
photo
/* unknownProperties */
)
diff --git a/src/main/java/at/bitfire/vcard4android/ContactReader.kt b/src/main/java/at/bitfire/vcard4android/ContactReader.kt
new file mode 100644
index 0000000..21c2078
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/ContactReader.kt
@@ -0,0 +1,269 @@
+package at.bitfire.vcard4android
+
+import at.bitfire.vcard4android.property.*
+import ezvcard.Ezvcard
+import ezvcard.VCard
+import ezvcard.property.*
+import ezvcard.util.PartialDate
+import org.apache.commons.lang3.StringUtils
+import java.net.URI
+import java.net.URISyntaxException
+import java.util.*
+import java.util.logging.Level
+
+/**
+ * Responsible for converting a specific vCard with a specific version to
+ * the version-independent data class [Contact].
+ *
+ * Attention: This class works with the original vCard and modifies it!
+ */
+class ContactReader internal constructor(val vCard: VCard, val downloader: Contact.Downloader? = null) {
+
+ companion object {
+
+ /**
+ * Maximum size for binary data like LOGO and SOUND data URIs.
+ *
+ * Data larger than this size has unfortunately to be dropped because of Android's IPC limits
+ * (max 1 MB per transaction, for all rows together).
+ */
+ const val MAX_BINARY_DATA_SIZE = 25*1024
+
+ fun fromVCard(vCard: VCard, downloader: Contact.Downloader? = null) =
+ ContactReader(vCard, downloader).toContact()
+
+ fun checkPartialDate(prop: DateOrTimeProperty) {
+ val date = prop.date
+ if (prop.partialDate == null && date != null) {
+ prop.getParameter(Contact.DATE_PARAMETER_OMIT_YEAR)?.let { omitYearStr ->
+ val cal = GregorianCalendar.getInstance()
+ cal.time = date
+ if (cal.get(GregorianCalendar.YEAR).toString() == omitYearStr) {
+ val partial = PartialDate.builder()
+ .date(cal.get(GregorianCalendar.DAY_OF_MONTH))
+ .month(cal.get(GregorianCalendar.MONTH) + 1)
+ .build()
+ prop.partialDate = partial
+ }
+
+ // X-APPLE-OMIT-YEAR not required anymore because we're now working with PartialDate
+ prop.removeParameter(Contact.DATE_PARAMETER_OMIT_YEAR)
+ }
+ }
+ }
+
+ fun uriToUid(uriString: String?): String? {
+ if (uriString == null)
+ return null
+
+ val uid = try {
+ val uri = URI(uriString)
+ when {
+ uri.scheme == null ->
+ uri.schemeSpecificPart
+ uri.scheme.equals("urn", true) && uri.schemeSpecificPart.startsWith("uuid:", true) ->
+ uri.schemeSpecificPart.substring(5)
+ else ->
+ uriString
+ }
+ } catch(e: URISyntaxException) {
+ Constants.log.warning("Invalid URI for UID: $uriString")
+ uriString
+ }
+
+ return StringUtils.trimToNull(uid)
+ }
+
+ }
+
+
+ /**
+ * Converts the vCard to a [Contact].
+ */
+ private fun toContact(): Contact {
+ val c = Contact()
+
+ // process standard properties; after processing, only unknown properties will remain
+ for (prop in vCard.properties) {
+ var remove = true // assume that this property will be processed and thus shall be removed
+
+ when (prop) {
+ is Uid ->
+ c.uid = uriToUid(prop.value)
+
+ is Kind, is XAddressBookServerKind -> {
+ val kindProp = prop as Kind
+ c.group = kindProp.isGroup
+ }
+ is Member, is XAddressBookServerMember -> {
+ val uriProp = prop as Member
+ uriToUid(uriProp.uri)?.let { c.members += it }
+ }
+
+ is FormattedName ->
+ c.displayName = StringUtils.trimToNull(prop.value)
+ is StructuredName -> {
+ c.prefix = StringUtils.trimToNull(prop.prefixes.joinToString(" "))
+ c.givenName = StringUtils.trimToNull(prop.given)
+ c.middleName = StringUtils.trimToNull(prop.additionalNames.joinToString(" "))
+ c.familyName = StringUtils.trimToNull(prop.family)
+ c.suffix = StringUtils.trimToNull(prop.suffixes.joinToString(" "))
+ }
+ is XPhoneticFirstName ->
+ c.phoneticGivenName = StringUtils.trimToNull(prop.value)
+ is XPhoneticMiddleName ->
+ c.phoneticMiddleName = StringUtils.trimToNull(prop.value)
+ is XPhoneticLastName ->
+ c.phoneticFamilyName = StringUtils.trimToNull(prop.value)
+ is Nickname ->
+ c.nickName = LabeledProperty(prop, findAndRemoveLabel(prop.group))
+
+ is Categories ->
+ c.categories.addAll(prop.values)
+
+ is Organization ->
+ c.organization = prop
+ is Title ->
+ c.jobTitle = StringUtils.trimToNull(prop.value)
+ is Role ->
+ c.jobDescription = StringUtils.trimToNull(prop.value)
+
+ is Telephone ->
+ if (!prop.text.isNullOrBlank())
+ c.phoneNumbers += LabeledProperty(prop, findAndRemoveLabel(prop.group))
+ is Email ->
+ if (!prop.value.isNullOrBlank())
+ c.emails += LabeledProperty(prop, findAndRemoveLabel(prop.group))
+ is Impp ->
+ c.impps += LabeledProperty(prop, findAndRemoveLabel(prop.group))
+ is XSip ->
+ // special case: treat X-SIP:address as IMPP:sip:address
+ c.impps += LabeledProperty(Impp.sip(prop.value))
+ is Url ->
+ c.urls += LabeledProperty(prop, findAndRemoveLabel(prop.group))
+
+ is Address ->
+ c.addresses += LabeledProperty(prop, findAndRemoveLabel(prop.group))
+ is Label -> { /* drop vCard3 formatted address because it can't be associated to a specific address */ }
+
+ is Anniversary -> {
+ checkPartialDate(prop)
+ c.anniversary = prop
+ }
+ is Birthday -> {
+ checkPartialDate(prop)
+ c.birthDay = prop
+ }
+ is XAbDate -> {
+ checkPartialDate(prop)
+ var label = findAndRemoveLabel(prop.group)
+ if (label == Contact.DATE_LABEL_OTHER) // drop Apple "Other" label
+ label = null
+
+ if (label == Contact.DATE_LABEL_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)
+ c.anniversary = Anniversary(prop.partialDate)
+ } else
+ c.customDates += LabeledProperty(prop, label)
+ }
+
+ is Related ->
+ if (!prop.uri.isNullOrBlank() || !prop.text.isNullOrBlank())
+ c.relations += prop
+
+ is Note -> {
+ StringUtils.trimToNull(prop.value)?.let { note ->
+ if (c.note == null)
+ c.note = note
+ else
+ c.note += "\n\n\n" + note
+ }
+ }
+
+ is Photo ->
+ c.photo = getPhotoBytes(prop)
+
+ // drop large binary properties because of potential OutOfMemory / TransactionTooLarge exceptions
+ is Logo -> {
+ remove = prop.data != null && prop.data.size > MAX_BINARY_DATA_SIZE
+ }
+ is Sound -> {
+ remove = prop.data != null && prop.data.size > MAX_BINARY_DATA_SIZE
+ }
+
+ // remove properties that don't apply anymore
+ is ProductId,
+ is Revision,
+ is SortString, // not counterpart in Android; remove it because FN/N may be changed, which would cause inconsistency
+ is Source -> { // when we upload a modified contact, the SOURCE would maybe point to a different version, which would cause inconsistency
+ /* remove = true */
+ }
+
+ else -> // unknown property, keep it in vCard in order to retain it
+ remove = false
+ }
+
+ if (remove)
+ vCard.removeProperty(prop)
+ }
+
+ if (c.uid == null) {
+ Constants.log.warning("Received vCard without UID, generating new one")
+ c.uid = UUID.randomUUID().toString()
+ }
+
+ // remove properties which
+ // - couldn't be parsed (and thus are treated as extended/unknown properties), and
+ // - must not occur more than once
+ arrayOf("ANNIVERSARY", "BDAY", "KIND", "FN", "N", "PRODID", "REV", "UID").forEach {
+ vCard.removeExtendedProperty(it)
+ }
+
+ if (vCard.properties.isNotEmpty() || vCard.extendedProperties.isNotEmpty())
+ try {
+ val writer = Ezvcard
+ .write(vCard)
+ .prodId(false)
+ .version(vCard.version)
+ CustomScribes.registerAt(writer)
+ c.unknownProperties = writer.go()
+ } catch(e: Exception) {
+ Constants.log.log(Level.WARNING, "Couldn't serialize unknown properties, dropping them", e)
+ }
+
+ return c
+ }
+
+
+ // helpers
+
+ fun findAndRemoveLabel(group: String?): String? {
+ if (group == null)
+ return null
+
+ for (label in vCard.getProperties(XAbLabel::class.java)) {
+ if (label.group.equals(group, true)) {
+ vCard.removeProperty(label)
+ return StringUtils.trimToNull(label.value)
+ }
+ }
+
+ return null
+ }
+
+ fun getPhotoBytes(photo: Photo): ByteArray? {
+ if (photo.data != null)
+ return photo.data
+
+ val url = photo.url
+ if (photo.url != null && downloader != null) {
+ Constants.log.info("Downloading photo from $url")
+ return downloader.download(url, "image/*")
+ }
+
+ return null
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/ContactWriter.kt b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt
new file mode 100644
index 0000000..7e882c2
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt
@@ -0,0 +1,295 @@
+package at.bitfire.vcard4android
+
+import at.bitfire.vcard4android.property.*
+import ezvcard.Ezvcard
+import ezvcard.VCard
+import ezvcard.VCardVersion
+import ezvcard.io.text.VCardWriter
+import ezvcard.parameter.ImageType
+import ezvcard.property.*
+import org.apache.commons.lang3.StringUtils
+import java.io.OutputStream
+import java.util.*
+import java.util.logging.Level
+
+/**
+ * Responsible for converting the [Contact] data class (which is not version-specific)
+ * to the vCard that is actually sent to the server.
+ *
+ * Properties which are not supported by the target vCard version have to be converted appropriately.
+ */
+class ContactWriter private constructor(val contact: Contact, val version: VCardVersion, val groupMethod: GroupMethod) {
+
+ private val unknownProperties = LinkedList<VCardProperty>()
+ val vCard = VCard()
+
+ /** counter for item ID of labelled properties: 1 means "item1." etc. */
+ private var currentItemId = 1
+
+ companion object {
+
+ fun fromContact(contact: Contact, version: VCardVersion, groupMethod: GroupMethod) =
+ ContactWriter(contact, version, groupMethod)
+
+ }
+
+ init {
+ parseUnknownProperties()
+ addProperties()
+ }
+
+ fun addProperties() {
+ contact.uid?.let { vCard.uid = Uid(it) }
+ Contact.productID?.let { vCard.setProductId(it) }
+
+ addKindAndMembers()
+
+ addFormattedName()
+ addStructuredName()
+ addPhoneticName()
+ contact.nickName?.let { nickName -> addLabeledProperty(nickName) }
+
+ if (contact.categories.isNotEmpty())
+ vCard.addCategories(Categories().apply {
+ values.addAll(contact.categories)
+ })
+
+ addOrganization()
+
+ for (phone in contact.phoneNumbers)
+ addLabeledProperty(phone)
+ for (email in contact.emails)
+ addLabeledProperty(email)
+ for (impp in contact.impps)
+ addLabeledProperty(impp)
+ for (url in contact.urls)
+ addLabeledProperty(url)
+
+ for (address in contact.addresses)
+ addLabeledProperty(address)
+
+ addDates()
+
+ for (relation in contact.relations)
+ vCard.addRelated(relation)
+
+ contact.note?.let { note -> vCard.addNote(note) }
+
+ for (unknownProperty in unknownProperties)
+ vCard.addProperty(unknownProperty)
+
+ contact.photo?.let { photo -> vCard.addPhoto(Photo(photo, ImageType.JPEG)) }
+
+ vCard.revision = Revision.now()
+ }
+
+ private fun addDates() {
+ contact.birthDay?.let { birthday ->
+ // vCard3 doesn't support partial dates
+ if (version == VCardVersion.V3_0)
+ rewritePartialDate(birthday)
+
+ vCard.birthday = birthday
+ }
+
+ contact.anniversary?.let { anniversary ->
+ if (version == VCardVersion.V4_0) {
+ vCard.anniversary = anniversary
+
+ } 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))
+ vCard.anniversary = null
+ }
+ }
+
+ for (customDate in contact.customDates) {
+ rewritePartialDate(customDate.property)
+ addLabeledProperty(customDate)
+ }
+ }
+
+ private fun addFormattedName() {
+ if (version == VCardVersion.V4_0) {
+ contact.displayName?.let { fn -> vCard.setFormattedName(fn) }
+
+ } else /* version == VCardVersion.V3_0 */ {
+ // vCard 3 REQUIRES FN [RFC 2426 p. 29]
+ var fn =
+ // use display name, if available
+ StringUtils.trimToNull(contact.displayName) ?:
+ // no display name, try organization
+ contact.organization?.let { org ->
+ org.values.joinToString(" / ")
+ } ?:
+ // otherwise, try nickname
+ contact.nickName?.let { nick -> nick.property.values.firstOrNull() } ?:
+ // otherwise, try email address
+ contact.emails.firstOrNull()?.let { email -> email.property.value } ?:
+ // otherwise, try phone number
+ contact.phoneNumbers.firstOrNull()?.let { phone -> phone.property.text } ?:
+ // otherwise, try UID or use empty string
+ contact.uid ?: ""
+ vCard.setFormattedName(fn)
+ }
+ }
+
+ private fun addKindAndMembers() {
+ if (contact.group && groupMethod == GroupMethod.GROUP_VCARDS) {
+ // TODO Use urn:uuid only when applicable
+ if (version == VCardVersion.V4_0) { // vCard4
+ vCard.kind = Kind.group()
+ for (member in contact.members)
+ vCard.addMember(Member("urn:uuid:$member"))
+ } else { // "vCard4 as vCard3" (Apple-style)
+ vCard.setProperty(XAddressBookServerKind(Kind.GROUP))
+ for (member in contact.members)
+ vCard.addProperty(XAddressBookServerMember("urn:uuid:$member"))
+ }
+ }
+ }
+
+ private fun addOrganization() {
+ contact.organization?.let { vCard.organization = it }
+ contact.jobTitle?.let { vCard.addTitle(it) }
+ contact.jobDescription?.let { vCard.addRole(it) }
+ }
+
+ private fun addStructuredName() {
+ val n = StructuredName()
+
+ contact.prefix?.let { prefixesStr ->
+ for (prefix in prefixesStr.split(' '))
+ n.prefixes += prefix
+ }
+
+ n.given = contact.givenName
+ contact.middleName?.let { middleNamesStr ->
+ for (middleName in middleNamesStr.split(' '))
+ n.additionalNames += middleName
+ }
+ n.family = contact.familyName
+
+ contact.suffix?.let { suffixesStr ->
+ for (suffix in suffixesStr.split(' '))
+ n.suffixes += suffix
+ }
+
+ if (version == VCardVersion.V4_0) {
+ // add N only if there's some data in it
+ if (n.prefixes.isNotEmpty() || n.given != null || n.additionalNames.isNotEmpty() || n.family != null || n.suffixes.isNotEmpty())
+ vCard.structuredName = n
+
+ } else /* version == VCardVersion.V3_0 */ {
+ // vCard 3 REQUIRES N [RFC 2426 p. 29]
+ vCard.structuredName = n
+ }
+ }
+
+ private fun addPhoneticName() {
+ contact.phoneticGivenName?.let { firstName ->
+ vCard.addProperty(XPhoneticFirstName(firstName))
+ }
+ contact.phoneticMiddleName?.let { middleName ->
+ vCard.addProperty(XPhoneticMiddleName(middleName))
+ }
+ contact.phoneticFamilyName?.let { lastName ->
+ vCard.addProperty(XPhoneticLastName(lastName))
+ }
+ }
+
+
+ // helpers
+
+ fun addLabeledProperty(labeledProperty: LabeledProperty<*>) {
+ val property = labeledProperty.property
+
+ if (labeledProperty.label != null) {
+ // property with label -> group property and label with item ID
+ val itemId = getNextItemId()
+
+ // 1. add property with item ID
+ property.group = itemId
+
+ // 2. add label with same item ID
+ val label = XAbLabel(labeledProperty.label)
+ label.group = itemId
+ vCard.addProperty(label)
+ }
+
+ vCard.addProperty(property)
+ }
+
+ private fun getNextItemId(): String {
+ var id: String
+
+ // increase ID until there is one which is not already used by an unknown property
+ do {
+ id = "item${currentItemId++}"
+ } while (unknownProperties.any { it.group == id })
+
+ return id
+ }
+
+ private fun parseUnknownProperties() {
+ try {
+ contact.unknownProperties?.let {
+ Ezvcard.parse(it).first()?.let { vCard ->
+ unknownProperties.addAll(vCard.properties)
+ }
+ }
+ } catch (e: Exception) {
+ Constants.log.log(Level.WARNING, "Couldn't parse original vCard with retained properties", e)
+ }
+ }
+
+ fun<T: DateOrTimeProperty> rewritePartialDate(prop: T) {
+ // vCard 3 doesn't understand partial dates:
+ // 1. the syntax is different (vCard 3: 2021-03-07, partial date: 20210307)
+ // 2. vCard 3 doesn't understand dates without year
+ val partial = prop.partialDate
+ if (version == VCardVersion.V3_0 && prop.date == null && partial != null) {
+ val originalYear = partial.year
+ val year = originalYear ?: Contact.DATE_PARAMETER_OMIT_YEAR_DEFAULT
+
+ // use full date format
+ val fakeCal = GregorianCalendar.getInstance()
+ fakeCal.timeInMillis = 0 // reset everything, including milliseconds
+ fakeCal.set(year, partial.month - 1, partial.date, 0, 0, 0)
+ prop.setDate(fakeCal, false)
+
+ if (originalYear == null)
+ prop.addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, Contact.DATE_PARAMETER_OMIT_YEAR_DEFAULT.toString())
+ }
+ }
+
+
+ fun writeVCard(stream: OutputStream) {
+ // validate vCard and log results
+ val validation = vCard.validate(version)
+ if (!validation.isEmpty) {
+ val msgs = LinkedList<String>()
+ for ((key, warnings) in validation)
+ msgs += " * " + key?.javaClass?.simpleName + " - " + warnings?.joinToString(" | ")
+ Constants.log.log(Level.WARNING, "vCard validation warnings", msgs.joinToString(","))
+ }
+
+ val writer = VCardWriter(stream, version).apply {
+ isAddProdId = Contact.productID == null
+ CustomScribes.registerAt(scribeIndex)
+
+ // include trailing semicolons for maximum compatibility
+ isIncludeTrailingSemicolons = true
+
+ // use caret encoding for parameter values (RFC 6868)
+ isCaretEncodingEnabled = true
+
+ isVersionStrict = false
+ }
+ writer.write(vCard)
+ writer.flush()
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/AbLabel.kt b/src/main/java/at/bitfire/vcard4android/property/AbLabel.kt
deleted file mode 100644
index 94a7c6b..0000000
--- a/src/main/java/at/bitfire/vcard4android/property/AbLabel.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package at.bitfire.vcard4android.property
-
-import ezvcard.io.scribe.StringPropertyScribe
-import ezvcard.property.TextProperty
-
-class AbLabel(value: String?): TextProperty(value) {
-
- object Scribe :
- StringPropertyScribe<AbLabel>(AbLabel::class.java, "X-ABLabel") {
-
- override fun _parseValue(value: String?) = AbLabel(value)
-
- }
-
-} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt b/src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt
deleted file mode 100644
index 3124a75..0000000
--- a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package at.bitfire.vcard4android.property
-
-import ezvcard.io.scribe.KindScribe
-import ezvcard.io.scribe.StringPropertyScribe
-import ezvcard.io.scribe.UriPropertyScribe
-import ezvcard.property.Kind
-import ezvcard.property.TextProperty
-import ezvcard.property.UriProperty
-
-class AddressBookServerKind(value: String?): Kind(value) {
-
- object Scribe :
- StringPropertyScribe<AddressBookServerKind>(AddressBookServerKind::class.java, "X-ADDRESSBOOKSERVER-KIND") {
-
- override fun _parseValue(value: String?) = AddressBookServerKind(value)
-
- }
-
-} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt b/src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt
deleted file mode 100644
index b55ec97..0000000
--- a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package at.bitfire.vcard4android.property
-
-import ezvcard.io.scribe.StringPropertyScribe
-import ezvcard.io.scribe.UriPropertyScribe
-import ezvcard.property.Member
-import ezvcard.property.TextProperty
-import ezvcard.property.UriProperty
-
-class AddressBookServerMember(value: String?): Member(value) {
-
- object Scribe :
- UriPropertyScribe<AddressBookServerMember>(AddressBookServerMember::class.java, "X-ADDRESSBOOKSERVER-MEMBER") {
-
- override fun _parseValue(value: String?) = AddressBookServerMember(value)
-
- }
-
-} \ 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
new file mode 100644
index 0000000..84fc41b
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt
@@ -0,0 +1,37 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.chain.ChainingTextWriter
+import ezvcard.io.scribe.ScribeIndex
+import ezvcard.io.text.VCardReader
+
+object CustomScribes {
+
+ /** list of all custom scribes (will be registered to readers/writers) **/
+ val customScribes = arrayOf(
+ XAbDate.Scribe,
+ XAbLabel.Scribe,
+ XAddressBookServerKind.Scribe,
+ XAddressBookServerMember.Scribe,
+ XPhoneticFirstName.Scribe,
+ XPhoneticMiddleName.Scribe,
+ XPhoneticLastName.Scribe,
+ XSip.Scribe
+ )
+
+ fun registerAt(writer: ChainingTextWriter) {
+ for (scribe in customScribes)
+ writer.register(scribe)
+ }
+
+ fun registerAt(index: ScribeIndex) {
+ for (scribe in customScribes)
+ index.register(scribe)
+ }
+
+ fun registerAt(reader: VCardReader): VCardReader {
+ for (scribe in customScribes)
+ reader.scribeIndex.register(scribe)
+ return reader
+ }
+
+}
diff --git a/src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt b/src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt
deleted file mode 100644
index d30a153..0000000
--- a/src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package at.bitfire.vcard4android.property
-
-import ezvcard.io.scribe.StringPropertyScribe
-import ezvcard.property.TextProperty
-
-class PhoneticFirstName(value: String?): TextProperty(value) {
-
- object Scribe :
- StringPropertyScribe<PhoneticFirstName>(PhoneticFirstName::class.java, "X-PHONETIC-FIRST-NAME") {
-
- override fun _parseValue(value: String?) = PhoneticFirstName(value)
-
- }
-
-} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt b/src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt
deleted file mode 100644
index c1dc171..0000000
--- a/src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package at.bitfire.vcard4android.property
-
-import ezvcard.io.scribe.StringPropertyScribe
-import ezvcard.property.TextProperty
-
-class PhoneticLastName(value: String?): TextProperty(value) {
-
- object Scribe :
- StringPropertyScribe<PhoneticLastName>(PhoneticLastName::class.java, "X-PHONETIC-LAST-NAME") {
-
- override fun _parseValue(value: String?) = PhoneticLastName(value)
-
- }
-
-} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt b/src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt
deleted file mode 100644
index fe50a82..0000000
--- a/src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package at.bitfire.vcard4android.property
-
-import ezvcard.io.scribe.StringPropertyScribe
-import ezvcard.property.TextProperty
-
-class PhoneticMiddleName(value: String?): TextProperty(value) {
-
- object Scribe :
- StringPropertyScribe<PhoneticMiddleName>(PhoneticMiddleName::class.java, "X-PHONETIC-MIDDLE-NAME") {
-
- override fun _parseValue(value: String?) = PhoneticMiddleName(value)
-
- }
-
-} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XAbDate.kt b/src/main/java/at/bitfire/vcard4android/property/XAbDate.kt
new file mode 100644
index 0000000..c3de1e5
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XAbDate.kt
@@ -0,0 +1,23 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.DateOrTimePropertyScribe
+import ezvcard.property.DateOrTimeProperty
+import ezvcard.util.PartialDate
+import java.util.*
+
+class XAbDate: DateOrTimeProperty {
+
+ constructor(text: String?): super(text)
+ constructor(date: Date?): super(date, false)
+ constructor(partialDate: PartialDate?): super(partialDate)
+
+
+ object Scribe : DateOrTimePropertyScribe<XAbDate>(XAbDate::class.java, "X-ABDATE") {
+
+ override fun newInstance(text: String?) = XAbDate(text)
+ override fun newInstance(calendar: Calendar?, hasTime: Boolean) = XAbDate(calendar?.time)
+ override fun newInstance(partialDate: PartialDate?) = XAbDate(partialDate)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt b/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt
new file mode 100644
index 0000000..103f153
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt
@@ -0,0 +1,15 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.StringPropertyScribe
+import ezvcard.property.TextProperty
+
+class XAbLabel(value: String?): TextProperty(value) {
+
+ object Scribe :
+ StringPropertyScribe<XAbLabel>(XAbLabel::class.java, "X-ABLABEL") {
+
+ override fun _parseValue(value: String?) = XAbLabel(value)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt
new file mode 100644
index 0000000..4c4b38d
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt
@@ -0,0 +1,15 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.StringPropertyScribe
+import ezvcard.property.Kind
+
+class XAddressBookServerKind(value: String?): Kind(value) {
+
+ object Scribe :
+ StringPropertyScribe<XAddressBookServerKind>(XAddressBookServerKind::class.java, "X-ADDRESSBOOKSERVER-KIND") {
+
+ override fun _parseValue(value: String?) = XAddressBookServerKind(value)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt
new file mode 100644
index 0000000..80f37c9
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt
@@ -0,0 +1,15 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.UriPropertyScribe
+import ezvcard.property.Member
+
+class XAddressBookServerMember(value: String?): Member(value) {
+
+ object Scribe :
+ UriPropertyScribe<XAddressBookServerMember>(XAddressBookServerMember::class.java, "X-ADDRESSBOOKSERVER-MEMBER") {
+
+ override fun _parseValue(value: String?) = XAddressBookServerMember(value)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt b/src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt
new file mode 100644
index 0000000..05c24c6
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt
@@ -0,0 +1,15 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.StringPropertyScribe
+import ezvcard.property.TextProperty
+
+class XPhoneticFirstName(value: String?): TextProperty(value) {
+
+ object Scribe :
+ StringPropertyScribe<XPhoneticFirstName>(XPhoneticFirstName::class.java, "X-PHONETIC-FIRST-NAME") {
+
+ override fun _parseValue(value: String?) = XPhoneticFirstName(value)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt b/src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt
new file mode 100644
index 0000000..04a019b
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt
@@ -0,0 +1,15 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.StringPropertyScribe
+import ezvcard.property.TextProperty
+
+class XPhoneticLastName(value: String?): TextProperty(value) {
+
+ object Scribe :
+ StringPropertyScribe<XPhoneticLastName>(XPhoneticLastName::class.java, "X-PHONETIC-LAST-NAME") {
+
+ override fun _parseValue(value: String?) = XPhoneticLastName(value)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt b/src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt
new file mode 100644
index 0000000..4e69244
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt
@@ -0,0 +1,15 @@
+package at.bitfire.vcard4android.property
+
+import ezvcard.io.scribe.StringPropertyScribe
+import ezvcard.property.TextProperty
+
+class XPhoneticMiddleName(value: String?): TextProperty(value) {
+
+ object Scribe :
+ StringPropertyScribe<XPhoneticMiddleName>(XPhoneticMiddleName::class.java, "X-PHONETIC-MIDDLE-NAME") {
+
+ override fun _parseValue(value: String?) = XPhoneticMiddleName(value)
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/at/bitfire/vcard4android/property/XSip.kt b/src/main/java/at/bitfire/vcard4android/property/XSip.kt
new file mode 100644
index 0000000..30c5ad1
--- /dev/null
+++ b/src/main/java/at/bitfire/vcard4android/property/XSip.kt
@@ -0,0 +1,19 @@
+package at.bitfire.vcard4android.property
+
+import android.net.Uri
+import ezvcard.io.scribe.ImppScribe
+import ezvcard.io.scribe.StringPropertyScribe
+import ezvcard.io.scribe.UriPropertyScribe
+import ezvcard.property.Impp
+import ezvcard.property.TextProperty
+import ezvcard.property.UriProperty
+
+class XSip(value: String?): TextProperty(value) {
+
+ object Scribe : StringPropertyScribe<XSip>(XSip::class.java, "X-SIP") {
+
+ override fun _parseValue(value: String?) = XSip(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
new file mode 100644
index 0000000..51007dc
--- /dev/null
+++ b/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt
@@ -0,0 +1,604 @@
+package at.bitfire.vcard4android
+
+import at.bitfire.vcard4android.property.*
+import ezvcard.VCard
+import ezvcard.VCardVersion
+import ezvcard.parameter.ImageType
+import ezvcard.parameter.RelatedType
+import ezvcard.parameter.SoundType
+import ezvcard.property.*
+import ezvcard.util.PartialDate
+import org.junit.Assert.*
+import org.junit.Test
+import java.net.URI
+import java.util.*
+
+class ContactReaderTest {
+
+ // test specific fields
+
+ @Test
+ fun testAddress() {
+ val address = Address().apply {
+ streetAddress = "Street 101"
+ country = "XX"
+ }
+ val c = ContactReader.fromVCard(VCard().apply {
+ addAddress(address)
+ })
+ assertEquals(LabeledProperty(address), c.addresses.first)
+ }
+
+ @Test
+ fun testAddressLabel_vCard3() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addOrphanedLabel(Label("Formatted Address"))
+ })
+ assertEquals(0, c.addresses.size)
+ assertNull(c.unknownProperties)
+ }
+
+
+
+ @Test
+ fun testAnniversary() {
+ val date = Date(101, 6, 30, 0, 0, 0)
+ val c = ContactReader.fromVCard(VCard().apply {
+ anniversary = Anniversary(date)
+ })
+ assertEquals(Anniversary(date), c.anniversary)
+ }
+
+
+ @Test
+ fun testBirthday_Date() {
+ val date = Date(101, 6, 30, 0, 0, 0)
+ val c = ContactReader.fromVCard(VCard().apply {
+ birthday = Birthday(date)
+ })
+ assertEquals(Birthday(date), c.birthDay)
+ }
+
+ @Test
+ fun testBirthday_vCard3_PartialDate() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ birthday = Birthday(Date(0, 6, 30)).apply {
+ addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, "1900")
+ }
+ })
+ assertEquals(Birthday(PartialDate.parse("--0730")), c.birthDay)
+ }
+
+ @Test
+ fun testBirthday_vCard4_PartialDate() {
+ val b = Birthday(PartialDate.parse("--0730"))
+ val c = ContactReader.fromVCard(VCard().apply {
+ birthday = b
+ })
+ assertEquals(b, c.birthDay)
+ }
+
+
+ @Test
+ fun testCategories() {
+ val cat = Categories().apply {
+ values.add("Cat1")
+ values.add("Cat2")
+ }
+ val c = ContactReader.fromVCard(VCard().apply {
+ addCategories(cat)
+ })
+ assertArrayEquals(arrayOf("Cat1", "Cat2"), c.categories.toTypedArray())
+ }
+
+
+ @Test
+ fun testEmail() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addEmail("test@example.com")
+ })
+ assertEquals("test@example.com", c.emails.first.property.value)
+ }
+
+
+ @Test
+ fun testFn() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ formattedName = FormattedName("Formatted Name")
+ })
+ assertEquals("Formatted Name", c.displayName)
+ }
+
+
+ @Test
+ fun testImpp_Xmpp() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addImpp(Impp.xmpp("test@example.com"))
+ })
+ assertEquals(URI("xmpp:test@example.com"), c.impps.first.property.uri)
+ }
+
+ @Test
+ fun testImpp_XSip() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addProperty(XSip("test@example.com"))
+ })
+ assertEquals(URI("sip:test@example.com"), c.impps.first.property.uri)
+ }
+
+
+ @Test
+ fun testKind_Group() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ kind = Kind.group()
+ })
+ assertTrue(c.group)
+ }
+
+ @Test
+ fun testKind_Individual() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ kind = Kind.individual()
+ })
+ assertFalse(c.group)
+ }
+
+
+ @Test
+ fun testLogo_Url() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply {
+ addLogo(Logo("https://example.com/logo.png", ImageType.PNG))
+ })
+ assertTrue(c.unknownProperties!!.contains("LOGO;MEDIATYPE=image/png:https://example.com/logo.png"))
+ }
+
+ @Test
+ fun testLogo_Url_TooLarge() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply {
+ addLogo(Logo(ByteArray(ContactReader.MAX_BINARY_DATA_SIZE + 1), ImageType.PNG))
+ })
+ assertNull(c.unknownProperties)
+ }
+
+
+ @Test
+ fun testMember_Uid() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ kind = Kind.group()
+ members += Member("member1")
+ })
+ assertEquals("member1", c.members.first)
+ }
+
+ @Test
+ fun testMember_Uid_Empty() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ kind = Kind.group()
+ members += Member("")
+ })
+ assertTrue(c.members.isEmpty())
+ }
+
+ @Test
+ fun testMember_UrnUiid() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ kind = Kind.group()
+ members += Member("urn:uuid:be829cf2-4244-42f8-bd4c-ab39b4b5fcd3")
+ })
+ assertEquals("be829cf2-4244-42f8-bd4c-ab39b4b5fcd3", c.members.first)
+ }
+
+ @Test
+ fun testMember_UrnUiid_Empty() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ kind = Kind.group()
+ members += Member("urn:uuid:")
+ })
+ assertTrue(c.members.isEmpty())
+ }
+
+
+ @Test
+ fun testN() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ structuredName = StructuredName().apply {
+ prefixes.add("P1.")
+ prefixes.add("P2.")
+ given = "Given"
+ additionalNames.add("Middle1")
+ additionalNames.add("Middle2")
+ family = "Family"
+ suffixes.add("S1")
+ suffixes.add("S2")
+ }
+ })
+ assertEquals("P1. P2.", c.prefix)
+ assertEquals("Given", c.givenName)
+ assertEquals("Middle1 Middle2", c.middleName)
+ assertEquals("Family", c.familyName)
+ assertEquals("S1 S2", c.suffix)
+ }
+
+
+ @Test
+ fun testNickname() {
+ val nick = Nickname().apply {
+ values.add("Nick1")
+ values.add("Nick2")
+ }
+ val c = ContactReader.fromVCard(VCard().apply {
+ addNickname(nick)
+ })
+ assertEquals(nick, c.nickName?.property)
+ }
+
+
+ @Test
+ fun testNote() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addNote("Note 1")
+ addNote("Note 2")
+ })
+ assertEquals("Note 1\n\n\nNote 2", c.note)
+ }
+
+
+ @Test
+ fun testOrganization() {
+ val org = Organization().apply {
+ values.add("Org")
+ values.add("Dept")
+ }
+ val c = ContactReader.fromVCard(VCard().apply {
+ setOrganization(org)
+ })
+ assertEquals(org, c.organization)
+ }
+
+
+ @Test
+ fun testProdId() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ productId = ProductId("Test")
+ })
+ assertNull(c.unknownProperties)
+ }
+
+
+ @Test
+ fun testRelated_Uri() {
+ val rel = Related.email("112@example.com")
+ rel.types.add(RelatedType.EMERGENCY)
+ val c = ContactReader.fromVCard(VCard().apply {
+ addRelated(rel)
+ })
+ assertEquals(rel, c.relations.first)
+ }
+
+ @Test
+ fun testRelated_String() {
+ val rel = Related().apply {
+ text = "My Best Friend"
+ types.add(RelatedType.FRIEND)
+ }
+ val c = ContactReader.fromVCard(VCard().apply {
+ addRelated(rel)
+ })
+ assertEquals(rel, c.relations.first)
+ }
+
+
+ @Test
+ fun testRev() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ revision = Revision.now()
+ })
+ assertNull(c.unknownProperties)
+ }
+
+ @Test
+ fun testRev_Invalid() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addExtendedProperty("REV", "+invalid-format!")
+ })
+ assertNull(c.unknownProperties)
+ }
+
+
+ @Test
+ fun testRole() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addRole("Job Description")
+ })
+ assertEquals("Job Description", c.jobDescription)
+ }
+
+
+ @Test
+ fun testSortString() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V3_0).apply {
+ sortString = SortString("Harten")
+ })
+ assertNull(c.unknownProperties)
+ }
+
+
+ @Test
+ fun testSource() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V3_0).apply {
+ addSource("https://example.com/sample.vcf")
+ })
+ assertNull(c.unknownProperties)
+ }
+
+
+ @Test
+ fun testSound_Url() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply {
+ addSound(Sound("https://example.com/ding.wav", SoundType.WAV))
+ })
+ assertTrue(c.unknownProperties!!.contains("SOUND;MEDIATYPE=audio/wav:https://example.com/ding.wav"))
+ }
+
+ @Test
+ fun testSound_Url_TooLarge() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply {
+ addSound(Sound(ByteArray(ContactReader.MAX_BINARY_DATA_SIZE + 1), SoundType.WAV))
+ })
+ assertNull(c.unknownProperties)
+ }
+
+
+ @Test
+ fun testTelephone() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addTelephoneNumber("+1 555 12345")
+ })
+ assertEquals("+1 555 12345", c.phoneNumbers.first.property.text)
+ }
+
+
+ @Test
+ fun testTitle() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addTitle("Job Title")
+ })
+ assertEquals("Job Title", c.jobTitle)
+ }
+
+
+ @Test
+ fun testUid() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ uid = Uid("12345")
+ })
+ assertEquals("12345", c.uid)
+ }
+
+
+ @Test
+ fun testUnkownProperty_vCard3() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V3_0).apply {
+ addProperty(RawProperty("FUTURE-PROPERTY", "12345"))
+ })
+ assertEquals("BEGIN:VCARD\r\n" +
+ "VERSION:3.0\r\n" +
+ "FUTURE-PROPERTY:12345\r\n" +
+ "END:VCARD\r\n", c.unknownProperties)
+ }
+
+ @Test
+ fun testUnkownProperty_vCard4() {
+ val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply {
+ addProperty(RawProperty("FUTURE-PROPERTY", "12345"))
+ })
+ assertEquals("BEGIN:VCARD\r\n" +
+ "VERSION:4.0\r\n" +
+ "FUTURE-PROPERTY:12345\r\n" +
+ "END:VCARD\r\n", c.unknownProperties)
+ }
+
+
+ @Test
+ fun testUrl() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ urls += Url("https://example.com")
+ })
+ assertEquals("https://example.com", c.urls.first.property.value)
+ }
+
+
+ @Test
+ fun testXAbDate_WithoutLabel() {
+ val date = Date(101, 6, 30, 0, 0, 0)
+ val c = ContactReader.fromVCard(VCard().apply {
+ addProperty(XAbDate(date))
+ })
+ assertEquals(LabeledProperty(XAbDate(date)), c.customDates.first)
+ }
+
+ @Test
+ fun testXAbDate_WithLabel_AppleAnniversary() {
+ 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" })
+ })
+ assertEquals(0, c.customDates.size)
+ assertEquals(Anniversary(date), c.anniversary)
+ }
+
+ @Test
+ fun testXAbDate_WithLabel_AppleOther() {
+ 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" })
+ })
+ assertEquals(date, c.customDates.first.property.date)
+ assertNull(c.customDates.first.label)
+ }
+
+ @Test
+ fun testXAbDate_WithLabel_Custom() {
+ val date = Date(101, 6, 30, 0, 0, 0)
+ val c = ContactReader.fromVCard(VCard().apply {
+ addProperty(XAbDate(date).apply { group = "test1" })
+ addProperty(XAbLabel("Test 1").apply { group = "test1" })
+ })
+ assertEquals(date, c.customDates.first.property.date)
+ assertEquals("Test 1", c.customDates.first.label)
+ }
+
+
+ @Test
+ fun testXAddressBookServerKind_Group() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addProperty(XAddressBookServerKind(Kind.GROUP))
+ })
+ assertTrue(c.group)
+ }
+
+ @Test
+ fun testXAddressBookServerKind_Individual() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addProperty(XAddressBookServerKind(Kind.INDIVIDUAL))
+ })
+ assertFalse(c.group)
+ }
+
+
+ @Test
+ fun testXPhoneticName() {
+ val c = ContactReader.fromVCard(VCard().apply {
+ addProperty(XPhoneticFirstName("First"))
+ addProperty(XPhoneticMiddleName("Middle"))
+ addProperty(XPhoneticLastName("Last"))
+ })
+ assertEquals("First", c.phoneticGivenName)
+ assertEquals("Middle", c.phoneticMiddleName)
+ assertEquals("Last", c.phoneticFamilyName)
+ }
+
+
+ // test helper methods
+
+ @Test
+ fun testCheckPartialDate_Date_WithoutOmitYear() {
+ val date = Date(101, 6, 30)
+ val withDate = Anniversary(date)
+ ContactReader.checkPartialDate(withDate)
+ assertEquals(date, withDate.date)
+ assertNull(withDate.partialDate)
+ }
+
+ @Test
+ fun testCheckPartialDate_Date_WithOmitYear_AnotherYear() {
+ val date = Date(10, 6, 30)
+ val withDate = Anniversary(date).apply {
+ addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, "2010")
+ }
+ ContactReader.checkPartialDate(withDate)
+ assertEquals(date, withDate.date)
+ assertNull(withDate.partialDate)
+ assertEquals(0, withDate.parameters.size()) // the year didn't match; we don't need the omit-year parameter anymore
+ }
+
+ @Test
+ fun testCheckPartialDate_Date_WithOmitYear_SameYear() {
+ val date = Date(110, 6, 30)
+ val withDate = Anniversary(date).apply {
+ addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, "2010")
+ }
+ ContactReader.checkPartialDate(withDate)
+ assertNull(withDate.date)
+ assertEquals(PartialDate.parse("--0730"), withDate.partialDate)
+ assertEquals(0, withDate.parameters.size())
+ }
+
+ @Test
+ fun testCheckPartialDate_PartialDate() {
+ val partialDate = PartialDate.parse("--0730")
+ val withDate = Anniversary(partialDate)
+ ContactReader.checkPartialDate(withDate)
+ assertNull(withDate.date)
+ assertEquals(partialDate, withDate.partialDate)
+ }
+
+
+ @Test
+ fun testFindAndRemoveLabel_NoLabel() {
+ val c = ContactReader(VCard())
+ assertNull(c.findAndRemoveLabel("item1"))
+ }
+
+ @Test
+ fun testFindAndRemoveLabel_Label() {
+ val vCard = VCard().apply {
+ addProperty(XAbLabel("Test Label").apply { group = "item1" })
+ }
+ val c = ContactReader(vCard)
+ assertEquals("Test Label", vCard.getProperty(XAbLabel::class.java).value)
+ assertEquals("Test Label", c.findAndRemoveLabel("item1"))
+ assertNull(vCard.getProperty(XAbLabel::class.java))
+ }
+
+ @Test
+ fun testFindAndRemoveLabel_Label_Empty() {
+ val vCard = VCard().apply {
+ addProperty(XAbLabel("").apply { group = "item1" })
+ }
+ val c = ContactReader(vCard)
+ assertEquals("", vCard.getProperty(XAbLabel::class.java).value)
+ assertNull(c.findAndRemoveLabel("item1"))
+ assertNull(vCard.getProperty(XAbLabel::class.java))
+ }
+
+ @Test
+ fun testFindAndRemoveLabel_LabelWithOtherGroup() {
+ val vCard = VCard().apply {
+ addProperty(XAbLabel("Test Label").apply { group = "item1" })
+ }
+ val c = ContactReader(vCard)
+ assertEquals("Test Label", vCard.getProperty(XAbLabel::class.java).value)
+ assertNull(c.findAndRemoveLabel("item2"))
+ assertEquals("Test Label", vCard.getProperty(XAbLabel::class.java).value)
+ }
+
+
+ @Test
+ fun testGetPhotoBytes_Binary() {
+ val sample = ByteArray(128)
+ assertEquals(sample, ContactReader.fromVCard(VCard().apply {
+ addPhoto(Photo(sample, ImageType.JPEG))
+ }).photo)
+ }
+
+ @Test
+ fun testGetPhotoBytes_Downloader() {
+ val sample = ByteArray(128)
+ val sampleUrl = "http://example.com/photo.jpg"
+ val downloader = object: Contact.Downloader {
+ override fun download(url: String, accepts: String): ByteArray? {
+ return if (url == sampleUrl && accepts == "image/*")
+ sample
+ else
+ null
+ }
+ }
+ assertEquals(sample, ContactReader.fromVCard(VCard().apply {
+ addPhoto(Photo(sampleUrl, ImageType.JPEG))
+ }, downloader).photo)
+ }
+
+
+ @Test
+ fun testUriToUid() {
+ assertEquals("uid", ContactReader.uriToUid("uid"))
+ assertEquals("urn:uid", ContactReader.uriToUid("urn:uid"))
+ assertEquals("12345", ContactReader.uriToUid("urn:uuid:12345"))
+ assertNull(ContactReader.uriToUid(""))
+ assertNull(ContactReader.uriToUid("urn:uuid:"))
+ }
+
+} \ No newline at end of file
diff --git a/src/test/java/at/bitfire/vcard4android/ContactTest.kt b/src/test/java/at/bitfire/vcard4android/ContactTest.kt
index 75ffc35..f8aa19a 100644
--- a/src/test/java/at/bitfire/vcard4android/ContactTest.kt
+++ b/src/test/java/at/bitfire/vcard4android/ContactTest.kt
@@ -10,7 +10,7 @@ package at.bitfire.vcard4android
import ezvcard.VCardVersion
import ezvcard.parameter.*
-import ezvcard.property.*
+import ezvcard.property.Birthday
import ezvcard.util.PartialDate
import org.apache.commons.io.IOUtils
import org.junit.Assert.*
@@ -18,9 +18,10 @@ import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStreamReader
-import java.io.StringReader
import java.nio.charset.Charset
import java.text.SimpleDateFormat
+import java.time.ZoneId
+import java.time.ZonedDateTime
import java.util.*
class ContactTest {
@@ -32,175 +33,18 @@ class ContactTest {
private fun regenerate(c: Contact, vCardVersion: VCardVersion): Contact {
val os = ByteArrayOutputStream()
- c.write(vCardVersion, GroupMethod.CATEGORIES, os)
+ c.writeVCard(vCardVersion, GroupMethod.CATEGORIES, os)
return Contact.fromReader(InputStreamReader(ByteArrayInputStream(os.toByteArray()), Charsets.UTF_8), null).first()
}
private fun toString(c: Contact, groupMethod: GroupMethod, vCardVersion: VCardVersion): String {
val os = ByteArrayOutputStream()
- c.write(vCardVersion, groupMethod, os)
+ c.writeVCard(vCardVersion, groupMethod, os)
return os.toString()
}
@Test
- fun testDropEmptyProperties() {
- val vcard = "BEGIN:VCARD\n" +
- "VERSION:4.0\n" +
- "FN:Sample with empty values\n" +
- "TEL:12345\n" +
- "TEL:\n" +
- "EMAIL:test@example.com\n" +
- "EMAIL:\n" +
- "END:VCARD"
- val c = Contact.fromReader(StringReader(vcard), null).first()
- assertEquals(1, c.phoneNumbers.size)
- assertEquals("12345", c.phoneNumbers.first.property.text)
- assertEquals(1, c.emails.size)
- assertEquals("test@example.com", c.emails.first.property.value)
- }
-
- @Test
- fun testGenerateOrganizationOnly() {
- val c = Contact()
- c.uid = UUID.randomUUID().toString()
- val org = Organization()
- org.values.add("My Organization")
- org.values.add("My Department")
- c.organization = org
-
- // vCard 3 needs FN and N
- var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- assertTrue(vCard.contains("\nORG:My Organization;My Department\r\n"))
- assertTrue(vCard.contains("\nFN:My Organization\r\n"))
- assertTrue(vCard.contains("\nN:\r\n"))
-
- // vCard 4 only needs FN
- vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0)
- assertTrue(vCard.contains("\nORG:My Organization;My Department\r\n"))
- assertTrue(vCard.contains("\nFN:My Organization\r\n"))
- assertFalse(vCard.contains("\nN:"))
- }
-
- @Test
- fun testGenerateOrgDepartmentOnly() {
- val c = Contact()
- c.uid = UUID.randomUUID().toString()
- val org = Organization()
- org.values.add("")
- org.values.add("My Department")
- c.organization = org
-
- // vCard 3 needs FN and N
- var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- assertTrue(vCard.contains("\nORG:;My Department\r\n"))
- assertTrue(vCard.contains("\nFN:My Department\r\n"))
- assertTrue(vCard.contains("\nN:\r\n"))
-
- // vCard 4 only needs FN
- vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0)
- assertTrue(vCard.contains("\nORG:;My Department\r\n"))
- assertTrue(vCard.contains("\nFN:My Department\r\n"))
- assertFalse(vCard.contains("\nN:"))
- }
-
- @Test
- fun testGenerateGroup() {
- val c = Contact()
- c.uid = UUID.randomUUID().toString()
- c.displayName = "My Group"
- c.group = true
- c.members += "member1"
- c.members += "member2"
-
- // vCard 3 needs FN and N
- // exception for Apple: "N:<group name>"
- var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- assertTrue(vCard.contains("\nX-ADDRESSBOOKSERVER-KIND:group\r\n"))
- assertTrue(vCard.contains("\nFN:My Group\r\n"))
- assertTrue(vCard.contains("\nN:My Group\r\n"))
- assertTrue(vCard.contains("\nX-ADDRESSBOOKSERVER-MEMBER:urn:uuid:member1\r\n"))
- assertTrue(vCard.contains("\nX-ADDRESSBOOKSERVER-MEMBER:urn:uuid:member2\r\n"))
-
- // vCard 4 only needs FN
- vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0)
- assertTrue(vCard.contains("\nKIND:group\r\n"))
- assertTrue(vCard.contains("\nFN:My Group\r\n"))
- assertFalse(vCard.contains("\nN:"))
- assertTrue(vCard.contains("\nMEMBER:urn:uuid:member1\r\n"))
- assertTrue(vCard.contains("\nMEMBER:urn:uuid:member2\r\n"))
- }
-
- @Test
- fun testGenerateWithoutName() {
- /* no data */
- val c = Contact()
- // vCard 3 needs FN and N
- var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- assertTrue(vCard.contains("\nFN:\r\n"))
- assertTrue(vCard.contains("\nN:\r\n"))
- // vCard 4 only needs FN
- vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0)
- assertTrue(vCard.contains("\nFN:\r\n"))
- assertFalse(vCard.contains("\nN:"))
-
- /* only UID */
- c.uid = UUID.randomUUID().toString()
- vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- // vCard 3 needs FN and N
- assertTrue(vCard.contains("\nFN:${c.uid}\r\n"))
- assertTrue(vCard.contains("\nN:\r\n"))
- // vCard 4 only needs FN
- vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0)
- assertTrue(vCard.contains("\nFN:${c.uid}\r\n"))
- assertFalse(vCard.contains("\nN:"))
-
- // phone number available
- c.phoneNumbers += LabeledProperty(Telephone("12345"))
- assertTrue(toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0).contains("\nFN:12345\r\n"))
-
- // email address available
- c.emails += LabeledProperty(Email("test@example.com"))
- assertTrue(toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0).contains("\nFN:test@example.com\r\n"))
-
- // nick name available
- c.nickName = LabeledProperty(Nickname().apply {
- values += "Nikki"
- })
- assertTrue(toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0).contains("\nFN:Nikki\r\n"))
- }
-
- @Test
- fun testGenerateLabeledProperty() {
- var c = Contact()
- c.uid = UUID.randomUUID().toString()
- c.phoneNumbers += LabeledProperty(Telephone("12345"), "My Phone")
- val vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- assertTrue(vCard.contains("\ngroup1.TEL:12345\r\n"))
- assertTrue(vCard.contains("\ngroup1.X-ABLabel:My Phone\r\n"))
-
- c = regenerate(c, VCardVersion.V4_0)
- assertEquals("12345", c.phoneNumbers.first.property.text)
- assertEquals("My Phone", c.phoneNumbers.first.label)
- }
-
- @Test
- fun testInvalidREV() {
- val c = parseContact("invalid-rev.vcf")
- assertFalse(c.unknownProperties!!.contains("REV"))
- }
-
- @Test
- fun testUnknownPropertiesLabels() {
- // X-ABLabels belonging to unknown properties shouldn't be dropped
- val c = parseContact("unknown-properties-with-labels.vcf")
- assertEquals("Unknown property with label", c.displayName)
- assertTrue(c.unknownProperties!!.contains("item1.X-Unknown:TestValue"))
- assertTrue(c.unknownProperties!!.contains("item1.X-ABLabel:TestLabel"))
- }
-
-
- @Test
fun testVCard3FieldsAsVCard3() {
val c = regenerate(parseContact("allfields-vcard3.vcf"), VCardVersion.V3_0)
@@ -312,7 +156,6 @@ class ContactTest {
assertEquals("Klöster-Reich", addr.property.country)
assertEquals("BEGIN:VCARD\r\n" +
"VERSION:3.0\r\n" +
- "PRODID:ez-vcard 0.11.2\r\n" +
"X-TEST;A=B:Value\r\n" +
"END:VCARD\r\n", c.unknownProperties)
@@ -330,7 +173,7 @@ class ContactTest {
var url1 = false
var url2 = false
for (url in c.urls) {
- if ("https://davdroid.bitfire.at/" == url.property.value && url.property.type == null && url.label == null)
+ if ("https://www.davx5.com/" == url.property.value && url.property.type == null && url.label == null)
url1 = true
if ("http://www.swbyps.restaurant.french/~chezchic.html" == url.property.value && "x-blog" == url.property.type && "blog" == url.label)
url2 = true
@@ -342,6 +185,12 @@ class ContactTest {
assertEquals("1996-04-15", dateFormat.format(c.birthDay!!.date))
// ANNIVERSARY
assertEquals("2014-08-12", dateFormat.format(c.anniversary!!.date))
+ // X-ABDATE
+ assertEquals(1, c.customDates.size)
+ c.customDates.first.also { date ->
+ assertEquals("Custom Date", date.label)
+ assertEquals(ZonedDateTime.of(2021, 7, 29, 0, 0, 0, 0, ZoneId.systemDefault()).toInstant(), date.property.date.toInstant())
+ }
// RELATED
assertEquals(2, c.relations.size)
@@ -351,7 +200,7 @@ class ContactTest {
assertEquals("Ägidius", rel.text)
rel = c.relations[1]
assertTrue(rel.types.contains(RelatedType.PARENT))
- assertEquals("muuuum", rel.text)
+ assertEquals("muuum@example.com", rel.uri)
// 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
new file mode 100644
index 0000000..bab087b
--- /dev/null
+++ b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt
@@ -0,0 +1,492 @@
+package at.bitfire.vcard4android
+
+import at.bitfire.vcard4android.property.*
+import ezvcard.VCard
+import ezvcard.VCardDataType
+import ezvcard.VCardVersion
+import ezvcard.parameter.ImageType
+import ezvcard.property.*
+import ezvcard.util.PartialDate
+import org.junit.Assert.*
+import org.junit.Test
+import java.io.ByteArrayOutputStream
+import java.net.URI
+import java.time.ZoneOffset
+import java.util.*
+
+class ContactWriterTest {
+
+ // test specific fields
+
+ @Test
+ fun testAddress() {
+ val address = Address().apply {
+ streetAddress = "Test Street"
+ country = "XX"
+ }
+ val vCard = generate {
+ addresses.add(LabeledProperty(address))
+ }
+ assertEquals(address, vCard.addresses.first())
+ }
+
+
+ @Test
+ fun testAnniversary_vCard3() {
+ val date = Date(121, 6, 30)
+ val vCard = generate(version = VCardVersion.V3_0) {
+ anniversary = Anniversary(date)
+ }
+ assertNull(vCard.anniversary)
+ assertEquals(date, vCard.getProperty(XAbDate::class.java).date)
+ }
+
+ @Test
+ fun testAnniversary_vCard4() {
+ val ann = Anniversary(Date(121, 6, 30))
+ val vCard = generate(version = VCardVersion.V4_0) {
+ anniversary = ann
+ }
+ assertEquals(ann, vCard.anniversary)
+ }
+
+
+ @Test
+ fun testBirthday() {
+ val bday = Birthday(Date(121, 6, 30))
+ val vCard = generate {
+ birthDay = bday
+ }
+ assertEquals(bday, vCard.birthday)
+ }
+
+
+ @Test
+ fun testCustomDate() {
+ val date = XAbDate(Date(121, 6, 30))
+ val vCard = generate {
+ customDates += LabeledProperty(date)
+ }
+ assertEquals(date, vCard.getProperty(XAbDate::class.java))
+ }
+
+
+ @Test
+ fun testCategories_Some() {
+ val vCard = generate {
+ categories += "cat1"
+ categories += "cat2"
+ }
+ assertEquals("cat1", vCard.categories.values[0])
+ assertEquals("cat2", vCard.categories.values[1])
+ }
+
+ @Test
+ fun testCategories_None() {
+ val vCard = generate { }
+ assertNull(vCard.categories)
+ }
+
+
+ @Test
+ fun testEmail() {
+ val vCard = generate {
+ emails.add(LabeledProperty(Email("test@example.com")))
+ }
+ assertEquals("test@example.com", vCard.emails.first().value)
+ }
+
+
+ @Test
+ fun testFn_vCard3_NoFn_Organization() {
+ val vCard = generate(version = VCardVersion.V3_0) {
+ organization = Organization().apply {
+ values.add("org")
+ values.add("dept")
+ }
+ // other values should be ignored because organization is available
+ nickName = LabeledProperty(Nickname().apply {
+ values.add("nick1")
+ })
+ }
+ assertEquals("org / dept", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard3_NoFn_NickName() {
+ val vCard = generate(version = VCardVersion.V3_0) {
+ nickName = LabeledProperty(Nickname().apply {
+ values.add("nick1")
+ })
+ // other values should be ignored because nickname is available
+ emails += LabeledProperty(Email("test@example.com"))
+ }
+ assertEquals("nick1", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard3_NoFn_Email() {
+ val vCard = generate(version = VCardVersion.V3_0) {
+ emails += LabeledProperty(Email("test@example.com"))
+ // other values should be ignored because email is available
+ phoneNumbers += LabeledProperty(Telephone("+1 555 12345"))
+ }
+ assertEquals("test@example.com", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard3_NoFn_Phone() {
+ val vCard = generate(version = VCardVersion.V3_0) {
+ phoneNumbers += LabeledProperty(Telephone("+1 555 12345"))
+ // other values should be ignored because phone is available
+ uid = "uid"
+ }
+ assertEquals("+1 555 12345", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard3_NoFn_Uid() {
+ val vCard = generate(version = VCardVersion.V3_0) {
+ uid = "uid"
+ }
+ assertEquals("uid", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard3_NoFn_Nothing() {
+ val vCard = generate(version = VCardVersion.V3_0) { }
+ assertEquals("", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard3_Fn() {
+ val vCard = generate(version = VCardVersion.V3_0) {
+ displayName = "Display Name"
+ }
+ assertEquals("Display Name", vCard.formattedName.value)
+ }
+
+ @Test
+ fun testFn_vCard4_NoFn() {
+ val vCard = generate(version = VCardVersion.V4_0) { }
+ assertNull(vCard.formattedName)
+ }
+
+ @Test
+ fun testFn_vCard4_Fn() {
+ val vCard = generate(version = VCardVersion.V4_0) {
+ displayName = "Display Name"
+ }
+ assertEquals("Display Name", vCard.formattedName.value)
+ }
+
+
+ @Test
+ fun testImpp() {
+ val vCard = generate {
+ impps.add(LabeledProperty(Impp.xmpp("test@example.com")))
+ }
+ assertEquals(URI("xmpp:test@example.com"), vCard.impps.first().uri)
+ }
+
+
+ @Test
+ fun testKindAndMember_vCard3() {
+ val vCard = generate(GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) {
+ group = true
+ members += "member1"
+ }
+ assertEquals(Kind.GROUP, vCard.getProperty(XAddressBookServerKind::class.java).value)
+ assertEquals("urn:uuid:member1", vCard.getProperty(XAddressBookServerMember::class.java).value)
+ }
+
+ @Test
+ fun testKindAndMember_vCard4() {
+ val vCard = generate(GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) {
+ group = true
+ members += "member1"
+ }
+ assertEquals(Kind.GROUP, vCard.getProperty(Kind::class.java).value)
+ assertEquals("urn:uuid:member1", vCard.members.first().value)
+ }
+
+
+ @Test
+ fun testN_vCard3_NoN() {
+ val vCard = generate(version = VCardVersion.V3_0) { }
+ assertEquals(StructuredName(), vCard.structuredName)
+ }
+
+ @Test
+ fun testN_vCard4_NoN() {
+ val vCard = generate(version = VCardVersion.V4_0) { }
+ assertNull(vCard.structuredName)
+ }
+
+ @Test
+ fun testN() {
+ val vCard = generate(version = VCardVersion.V4_0) {
+ prefix = "P1. P2."
+ givenName = "Given"
+ middleName = "Middle1 Middle2"
+ familyName = "Family"
+ suffix = "S1 S2"
+ }
+ assertEquals(StructuredName().apply {
+ prefixes += "P1."
+ prefixes += "P2."
+ given = "Given"
+ additionalNames += "Middle1"
+ additionalNames += "Middle2"
+ family = "Family"
+ suffixes += "S1"
+ suffixes += "S2"
+ }, vCard.structuredName)
+ }
+
+
+ @Test
+ fun testNote() {
+ val vCard = generate { note = "Some Note" }
+ assertEquals("Some Note", vCard.notes.first().value)
+ }
+
+
+ @Test
+ fun testOrganization() {
+ val org = Organization().apply {
+ values.add("Org")
+ values.add("Dept")
+ }
+ val vCard = generate {
+ organization = org
+ jobTitle = "CEO"
+ jobDescription = "Executive"
+ }
+ assertEquals(org, vCard.organization)
+ assertEquals("CEO", vCard.titles.first().value)
+ assertEquals("Executive", vCard.roles.first().value)
+ }
+
+
+ @Test
+ fun testPhoto() {
+ val testPhoto = ByteArray(128)
+ val vCard = generate { photo = testPhoto }
+ assertEquals(Photo(testPhoto, ImageType.JPEG), vCard.photos.first())
+ }
+
+
+ @Test
+ fun testRelation() {
+ val rel = Related.email("bigbrother@example.com")
+ val vCard = generate { relations += rel }
+ assertEquals(rel, vCard.relations.first())
+ }
+
+
+ @Test
+ fun testTel() {
+ val vCard = generate {
+ phoneNumbers.add(LabeledProperty(Telephone("+1 555 12345")))
+ }
+ assertEquals("+1 555 12345", vCard.telephoneNumbers.first().text)
+ }
+
+
+ @Test
+ fun testUid() {
+ val vCard = generate { uid = "12345" }
+ assertEquals("12345", vCard.uid.value)
+ }
+
+
+ @Test
+ fun testUnknownProperty() {
+ val vCard = generate {
+ unknownProperties = "BEGIN:VCARD\r\n" +
+ "FUTURE-PROPERTY;X-TEST=test1;TYPE=uri:12345\r\n" +
+ "END:VCARD\r\n"
+ }
+ assertEquals("12345", vCard.getExtendedProperty("FUTURE-PROPERTY").value)
+ assertEquals("test1", vCard.getExtendedProperty("FUTURE-PROPERTY").getParameter("X-TEST"))
+ }
+
+
+ @Test
+ fun testUrl() {
+ val vCard = generate { urls += LabeledProperty(Url("https://example.com")) }
+ assertEquals("https://example.com", vCard.urls.first().value)
+ }
+
+
+ @Test
+ fun testXPhoneticName() {
+ val vCard = generate() {
+ phoneticGivenName = "Given"
+ phoneticMiddleName = "Middle"
+ phoneticFamilyName = "Family"
+ }
+ assertEquals("Given", vCard.getProperty(XPhoneticFirstName::class.java).value)
+ assertEquals("Middle", vCard.getProperty(XPhoneticMiddleName::class.java).value)
+ assertEquals("Family", vCard.getProperty(XPhoneticLastName::class.java).value)
+ }
+
+
+
+ // test generator helpers
+
+ @Test
+ fun testAddLabeledProperty_NoLabel() {
+ val vCard = generate {
+ nickName = LabeledProperty(Nickname().apply {
+ values.add("nick1")
+ })
+ }
+ assertEquals(2 /* NICK + REV */, vCard.properties.size)
+ assertEquals("nick1", vCard.nickname.values.first())
+ }
+
+ @Test
+ fun testAddLabeledProperty_Label() {
+ val vCard = generate {
+ nickName = LabeledProperty(Nickname().apply {
+ values.add("nick1")
+ }, "label1")
+ }
+ assertEquals(3 /* NICK + X-ABLABEL + REV */, vCard.properties.size)
+ vCard.nickname.apply {
+ assertEquals("nick1", values.first())
+ assertEquals("item1", group)
+ }
+ vCard.getProperty(XAbLabel::class.java).apply {
+ assertEquals("label1", value)
+ assertEquals("item1", group)
+ }
+ }
+
+ @Test
+ fun testAddLabeledProperty_Label_CollisionWithUnknownProperty() {
+ val vCard = generate {
+ unknownProperties = "BEGIN:VCARD\n" +
+ "item1.X-TEST:This property is blocking the first item ID\n" +
+ "END:VCARD"
+ nickName = LabeledProperty(Nickname().apply {
+ values.add("nick1")
+ }, "label1")
+ }
+ assertEquals(4 /* X-TEST + NICK + X-ABLABEL + REV */, vCard.properties.size)
+ vCard.nickname.apply {
+ assertEquals("nick1", values.first())
+ assertEquals("item2", group)
+ }
+ vCard.getProperty(XAbLabel::class.java).apply {
+ assertEquals("label1", value)
+ assertEquals("item2", group)
+ }
+ }
+
+
+ @Test
+ fun testRewritePartialDate_vCard3_Date() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS)
+ val date = Birthday(Date(121, 6, 30))
+ generator.rewritePartialDate(date)
+ assertEquals(Date(121, 6, 30), date.date)
+ assertNull(date.partialDate)
+ }
+
+ @Test
+ fun testRewritePartialDate_vCard4_Date() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS)
+ val date = Birthday(Date(121, 6, 30))
+ generator.rewritePartialDate(date)
+ assertEquals(Date(121, 6, 30), date.date)
+ assertNull(date.partialDate)
+ assertEquals(0, date.parameters.size())
+ }
+
+ @Test
+ fun testRewritePartialDate_vCard3_PartialDateWithYear() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS)
+ val date = Birthday(PartialDate.parse("20210730"))
+ generator.rewritePartialDate(date)
+ assertEquals(Date(121, 6, 30), date.date)
+ assertNull(date.partialDate)
+ assertEquals(0, date.parameters.size())
+ }
+
+ @Test
+ fun testRewritePartialDate_vCard4_PartialDateWithYear() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS)
+ val date = Birthday(PartialDate.parse("20210730"))
+ generator.rewritePartialDate(date)
+ assertNull(date.date)
+ assertEquals(PartialDate.parse("20210730"), date.partialDate)
+ assertEquals(0, date.parameters.size())
+ }
+
+ @Test
+ fun testRewritePartialDate_vCard3_PartialDateWithoutYear() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS)
+ val date = Birthday(PartialDate.parse("--0730"))
+ generator.rewritePartialDate(date)
+ assertEquals(Date(-300+4, 6, 30), date.date)
+ assertNull(date.partialDate)
+ assertEquals(1, date.parameters.size())
+ assertEquals("1604", date.getParameter(Contact.DATE_PARAMETER_OMIT_YEAR))
+ }
+
+ @Test
+ fun testRewritePartialDate_vCard4_PartialDateWithoutYear() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS)
+ val date = Birthday(PartialDate.parse("--0730"))
+ generator.rewritePartialDate(date)
+ assertNull(date.date)
+ assertEquals(PartialDate.parse("--0730"), date.partialDate)
+ assertEquals(0, date.parameters.size())
+ }
+
+
+ @Test
+ fun testWriteVCard() {
+ val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS)
+ generator.vCard.revision = Revision(Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC.id)).apply {
+ set(2021, 6, 30, 1, 2, 3)
+ })
+
+ val stream = ByteArrayOutputStream()
+ generator.writeVCard(stream)
+ assertEquals("BEGIN:VCARD\r\n" +
+ "VERSION:4.0\r\n" +
+ "PRODID:ez-vcard 0.11.2\r\n" +
+ "REV:20210730T010203Z\r\n" +
+ "END:VCARD\r\n", stream.toString())
+ }
+
+ @Test
+ fun testWriteVCard_CaretEncoding() {
+ val stream = ByteArrayOutputStream()
+ val contact = Contact().apply {
+ addresses += LabeledProperty(Address().apply {
+ label = "Li^ne 1,1 - \" -"
+ streetAddress = "Line1"
+ country = "Line2"
+ })
+ }
+ ContactWriter
+ .fromContact(contact, VCardVersion.V4_0, GroupMethod.GROUP_VCARDS)
+ .writeVCard(stream)
+ assertTrue(stream.toString().contains("ADR;LABEL=\"Li^^ne 1,1 - ^' -\":;;Line1;;;;Line2"))
+ }
+
+
+ // helpers
+
+ private fun generate(groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS, version: VCardVersion = VCardVersion.V4_0, prepare: Contact.() -> Unit): VCard {
+ val contact = Contact()
+ contact.run(prepare)
+ return ContactWriter.fromContact(contact, version, groupMethod).vCard
+ }
+
+} \ No newline at end of file
diff --git a/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt b/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt
index 09b7528..ae10388 100644
--- a/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt
+++ b/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt
@@ -9,6 +9,9 @@
package at.bitfire.vcard4android
import ezvcard.Ezvcard
+import ezvcard.VCard
+import ezvcard.VCardVersion
+import ezvcard.property.Address
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
@@ -62,4 +65,20 @@ class EzVCardTest {
assertNotNull(vCard.revision)
}
+ @Test
+ fun testGenerateCaretNewline() {
+ val vCard = VCard()
+ vCard.addAddress(Address().apply {
+ label = "Li^ne 1,1\n- \" -"
+ streetAddress = "Line 1"
+ country = "Line 2"
+ })
+ val str = Ezvcard .write(vCard)
+ .version(VCardVersion.V4_0)
+ .caretEncoding(true)
+ .go().lines().filter { it.startsWith("ADR") }.first()
+ //assertEquals("ADR;LABEL=\"Li^^ne 1,1^n- ^' -\":;;Line 1;;;;Line 2", str)
+ assertEquals("ADR;LABEL=\"Li^^ne 1,1\\n- ^' -\":;;Line 1;;;;Line 2", str)
+ }
+
}
diff --git a/src/test/resources/allfields-vcard3.vcf b/src/test/resources/allfields-vcard3.vcf
index af253a5..5a2a329 100644
--- a/src/test/resources/allfields-vcard3.vcf
+++ b/src/test/resources/allfields-vcard3.vcf
@@ -29,13 +29,15 @@ NOTE:This fax number is operational 0800 to 1715
EST\, Mon-Fri.
NOTE:Second note
CATEGORIES:A,B\'C
-URL:https://davdroid.bitfire.at/
+URL:https://www.davx5.com/
blog.URL;TYPE=x-blog:http://www.swbyps.restaurant.french/~chezchic.html
blog.X-ABLabel:blog
BDAY:1996-04-15
ANNIVERSARY:2014-08-12
RELATED;CHARSET=UTF-8;TYPE=co-worker,crush;VALUE=text:Ägidius
-RELATED;TYPE=parent;VALUE=text:muuuum
+RELATED;TYPE=parent;VALUE=uri:muuum@example.com
PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRgABAQIAHAAcAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAKACABAREA/8QAFgABAQEAAAAAAAAAAAAAAAAAAAgE/8QAIBAAAQQCAwEBAQAAAAAAAAAABAECAwUABgcREggUFf/aAAgBAQAAPwC/huVQw/pq74hvt0qhP16fQ2mu0hBEERRhUhV0h0g7V6ln6hDFV6J6axrEd0305XS/xR9SbWRxtLudn9HibSOLT8SW2wWBf8VsGvl3F7KPehyOFHiZAxozGorZ/UsKKrvbV6cl1VNtVX9UHe0VmJY1tiPGWGYJM2aAmCRqOjljkaqtexzVRyORVRUVFTNeMYxn/9k=
X-TEST;A=B:Value
+item2.X-ABDate:20210729
+item2.X-ABLabel:Custom Date
END:VCARD
diff --git a/src/test/resources/invalid-rev.vcf b/src/test/resources/invalid-rev.vcf
deleted file mode 100644
index c89bdd9..0000000
--- a/src/test/resources/invalid-rev.vcf
+++ /dev/null
@@ -1,21 +0,0 @@
-BEGIN:VCARD
-VERSION:4.0
-PRODID:+//IDN bitfire.at//DAVdroid/1.11.4.1-ose ez-vcard/0.10.4
-X-EVOLUTION-FILE-AS:Xxxxxx-xxxx\, Loïc
-X-EVOLUTION-MANAGER:
-X-EVOLUTION-ASSISTANT:
-X-EVOLUTION-SPOUSE:
-X-EVOLUTION-BLOG-URL:
-X-EVOLUTION-VIDEO-URL:
-X-MOZILLA-HTML:FALSE
-REV;VALUE=timestamp:2018-04-30T07:42:25Z(1)
-CALURI:
-FBURL:
-UID:pas-id-0c6bbc36b403f6e885c75e4835f796bba6fcdc12
-FN:Loïc Xxxxxx-xxxx
-N:Xxxxxx-xxxx;Loïc;;;
-ORG:xxxxxxxmail
-EMAIL;TYPE=work:loic.Xxxxxx-xxxx@xxxxxxxmail.com
-URL:
-REV:20180619T131622Z
-END:VCARD