Loading build.gradle +3 −3 Original line number Diff line number Diff line buildscript { ext.versions = [ kotlin: '1.3.60', kotlin: '1.3.61', dokka: '0.10.0' ] Loading @@ -11,7 +11,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } Loading Loading @@ -72,7 +72,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}" implementation 'org.apache.commons:commons-text:1.7' implementation 'org.apache.commons:commons-text:1.8' implementation 'commons-io:commons-io:2.6' // ez-vcard to parse/generate VCards Loading src/androidTest/java/foundation/e/vcard4android/AndroidContactTest.kt +45 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import foundation.e.vcard4android.impl.TestAddressBook import ezvcard.VCardVersion import ezvcard.parameter.EmailType import ezvcard.property.Address import ezvcard.property.Birthday import ezvcard.property.Email Loading Loading @@ -202,6 +203,50 @@ class AndroidContactTest { } } @Test @SmallTest fun testEmailTypes() { val vCard = "BEGIN:VCARD\r\n" + "VERSION:4.0\r\n" + "FN:Test\r\n" + "EMAIL;TYPE=internet;TYPE=work:work@example.com\r\n" + "EMAIL;TYPE=home:home@example.com\r\n" + "EMAIL;TYPE=internet,pref:other1@example.com\r\n" + "EMAIL;TYPE=x400,other:other2@example.com\r\n" + "EMAIL;TYPE=x-mobile:mobile@example.com\r\n" + "END:VCARD\r\n" val contacts = Contact.fromReader(StringReader(vCard), null) val dbContact = AndroidContact(addressBook, contacts.first(), null, null) dbContact.add() val dbContact2 = addressBook.findContactByID(dbContact.id!!) try { val contact2 = dbContact2.contact!! assertEquals("work@example.com", contact2.emails[0].property.value) assertArrayEquals(arrayOf(EmailType.WORK), contact2.emails[0].property.types.toTypedArray()) assertNull(contact2.emails[0].property.pref) assertEquals("home@example.com", contact2.emails[1].property.value) assertArrayEquals(arrayOf(EmailType.HOME), contact2.emails[1].property.types.toTypedArray()) assertNull(contact2.emails[1].property.pref) assertEquals("other1@example.com", contact2.emails[2].property.value) assertTrue(contact2.emails[2].property.types.isEmpty()) assertNotEquals(0, contact2.emails[2].property.pref) assertEquals("other2@example.com", contact2.emails[3].property.value) assertTrue(contact2.emails[3].property.types.isEmpty()) assertNull(contact2.emails[3].property.pref) assertEquals("mobile@example.com", contact2.emails[4].property.value) assertArrayEquals(arrayOf(Contact.EMAIL_TYPE_MOBILE), contact2.emails[4].property.types.toTypedArray()) assertNull(contact2.emails[4].property.pref) } finally { dbContact2.delete() } } @Test fun testLabelToXName() { Loading src/main/java/foundation/e/vcard4android/AndroidContact.kt +33 −12 Original line number Diff line number Diff line Loading @@ -572,11 +572,28 @@ open class AndroidContact( buildContact(builder, true) batch.enqueue(BatchOperation.Operation(builder)) // delete known data rows before adding the new ones; don't delete group memberships! // Delete known data rows before adding the new ones. // - We don't delete group memberships. // - We'll only delete rows we have inserted so that unknown rows like // vnd.android.cursor.item/important_people (= contact is in Samsung "edge panel") remain untouched. batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newDelete(dataSyncURI()) .withSelection(Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " NOT IN (?,?)", arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)) .withSelection(Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?,?,?,?,?,?,?,?,?,?,?,?)", arrayOf(id.toString(), StructuredName.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE)) )) insertDataRows(batch) batch.commit() Loading Loading @@ -732,6 +749,7 @@ open class AndroidContact( types.contains(Contact.PHONE_TYPE_MMS) -> typeCode = Phone.TYPE_MMS types.contains(Contact.PHONE_TYPE_OTHER) || types.contains(TelephoneType.VOICE) || types.contains(TelephoneType.TEXT) -> {} Loading Loading @@ -768,7 +786,10 @@ open class AndroidContact( protected open fun insertEmail(batch: BatchOperation, labeledEmail: LabeledProperty<ezvcard.property.Email>) { val email = labeledEmail.property // drop TYPE=internet and TYPE=x400 because Android only knows Internet email addresses // drop TYPE=other for compatibility, too (non-standard type which is only used by some clients and not useful as an explicit value) val types = email.types types.removeAll(arrayOf(EmailType.INTERNET, EmailType.X400, Contact.EMAIL_TYPE_OTHER)) // preferred email address? var pref: Int? = null Loading @@ -795,7 +816,7 @@ open class AndroidContact( EmailType.WORK -> typeCode = Email.TYPE_WORK Contact.EMAIL_TYPE_MOBILE -> typeCode = Email.TYPE_MOBILE } if (typeCode == 0) { if (typeCode == 0) { // we still didn't find a known type if (email.types.isEmpty()) typeCode = Email.TYPE_OTHER else { Loading Loading @@ -869,7 +890,6 @@ open class AndroidContact( var typeCode: Int = Im.TYPE_OTHER var typeLabel: String? = null if (labeledImpp.label != null) { typeCode = Im.TYPE_CUSTOM typeLabel = labeledImpp.label Loading Loading @@ -1041,22 +1061,23 @@ open class AndroidContact( formattedAddress = lines.joinToString("\n") } val types = address.types var typeCode = StructuredPostal.TYPE_OTHER var typeLabel: String? = null if (labeledAddress.label != null) { typeCode = StructuredPostal.TYPE_CUSTOM typeLabel = labeledAddress.label } else { for (type in address.types) when (type) { AddressType.HOME -> typeCode = StructuredPostal.TYPE_HOME AddressType.WORK -> typeCode = StructuredPostal.TYPE_WORK } if (typeCode == StructuredPostal.TYPE_OTHER && address.types.isNotEmpty()) { when { types.contains(AddressType.HOME) -> typeCode = StructuredPostal.TYPE_HOME types.contains(AddressType.WORK) -> typeCode = StructuredPostal.TYPE_WORK types.contains(Contact.ADDRESS_TYPE_OTHER) -> {} types.isNotEmpty() -> { typeCode = StructuredPostal.TYPE_CUSTOM typeLabel = xNameToLabel(address.types.first().value) } } } val op: BatchOperation.Operation val builder = ContentProviderOperation.newInsert(dataSyncURI()) Loading src/main/java/foundation/e/vcard4android/Contact.kt +9 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ package foundation.e.vcard4android import ezvcard.Ezvcard import ezvcard.VCard import ezvcard.VCardVersion import ezvcard.parameter.AddressType import ezvcard.parameter.EmailType import ezvcard.parameter.ImageType import ezvcard.parameter.TelephoneType Loading Loading @@ -90,8 +91,16 @@ class Contact { val PHONE_TYPE_RADIO = TelephoneType.get("x-radio")!! val PHONE_TYPE_ASSISTANT = TelephoneType.get("X-assistant")!! val PHONE_TYPE_MMS = TelephoneType.get("x-mms")!! /** Sometimes used to denote an "other" phone numbers. Only for compatibility – don't use it yourself! */ val PHONE_TYPE_OTHER = TelephoneType.get("other")!! /** Custom email type to denote "mobile" email addresses. */ val EMAIL_TYPE_MOBILE = EmailType.get("x-mobile")!! /** Sometimes used to denote an "other" email address. Only for compatibility – don't use it yourself! */ val EMAIL_TYPE_OTHER = EmailType.get("other")!! /** Sometimes used to denote an "other" postal address. Only for compatibility – don't use it yourself! */ val ADDRESS_TYPE_OTHER = AddressType.get("other")!! const val NICKNAME_TYPE_MAIDEN_NAME = "x-maiden-name" const val NICKNAME_TYPE_SHORT_NAME = "x-short-name" Loading Loading
build.gradle +3 −3 Original line number Diff line number Diff line buildscript { ext.versions = [ kotlin: '1.3.60', kotlin: '1.3.61', dokka: '0.10.0' ] Loading @@ -11,7 +11,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } Loading Loading @@ -72,7 +72,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}" implementation 'org.apache.commons:commons-text:1.7' implementation 'org.apache.commons:commons-text:1.8' implementation 'commons-io:commons-io:2.6' // ez-vcard to parse/generate VCards Loading
src/androidTest/java/foundation/e/vcard4android/AndroidContactTest.kt +45 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import foundation.e.vcard4android.impl.TestAddressBook import ezvcard.VCardVersion import ezvcard.parameter.EmailType import ezvcard.property.Address import ezvcard.property.Birthday import ezvcard.property.Email Loading Loading @@ -202,6 +203,50 @@ class AndroidContactTest { } } @Test @SmallTest fun testEmailTypes() { val vCard = "BEGIN:VCARD\r\n" + "VERSION:4.0\r\n" + "FN:Test\r\n" + "EMAIL;TYPE=internet;TYPE=work:work@example.com\r\n" + "EMAIL;TYPE=home:home@example.com\r\n" + "EMAIL;TYPE=internet,pref:other1@example.com\r\n" + "EMAIL;TYPE=x400,other:other2@example.com\r\n" + "EMAIL;TYPE=x-mobile:mobile@example.com\r\n" + "END:VCARD\r\n" val contacts = Contact.fromReader(StringReader(vCard), null) val dbContact = AndroidContact(addressBook, contacts.first(), null, null) dbContact.add() val dbContact2 = addressBook.findContactByID(dbContact.id!!) try { val contact2 = dbContact2.contact!! assertEquals("work@example.com", contact2.emails[0].property.value) assertArrayEquals(arrayOf(EmailType.WORK), contact2.emails[0].property.types.toTypedArray()) assertNull(contact2.emails[0].property.pref) assertEquals("home@example.com", contact2.emails[1].property.value) assertArrayEquals(arrayOf(EmailType.HOME), contact2.emails[1].property.types.toTypedArray()) assertNull(contact2.emails[1].property.pref) assertEquals("other1@example.com", contact2.emails[2].property.value) assertTrue(contact2.emails[2].property.types.isEmpty()) assertNotEquals(0, contact2.emails[2].property.pref) assertEquals("other2@example.com", contact2.emails[3].property.value) assertTrue(contact2.emails[3].property.types.isEmpty()) assertNull(contact2.emails[3].property.pref) assertEquals("mobile@example.com", contact2.emails[4].property.value) assertArrayEquals(arrayOf(Contact.EMAIL_TYPE_MOBILE), contact2.emails[4].property.types.toTypedArray()) assertNull(contact2.emails[4].property.pref) } finally { dbContact2.delete() } } @Test fun testLabelToXName() { Loading
src/main/java/foundation/e/vcard4android/AndroidContact.kt +33 −12 Original line number Diff line number Diff line Loading @@ -572,11 +572,28 @@ open class AndroidContact( buildContact(builder, true) batch.enqueue(BatchOperation.Operation(builder)) // delete known data rows before adding the new ones; don't delete group memberships! // Delete known data rows before adding the new ones. // - We don't delete group memberships. // - We'll only delete rows we have inserted so that unknown rows like // vnd.android.cursor.item/important_people (= contact is in Samsung "edge panel") remain untouched. batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newDelete(dataSyncURI()) .withSelection(Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " NOT IN (?,?)", arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)) .withSelection(Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?,?,?,?,?,?,?,?,?,?,?,?)", arrayOf(id.toString(), StructuredName.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE)) )) insertDataRows(batch) batch.commit() Loading Loading @@ -732,6 +749,7 @@ open class AndroidContact( types.contains(Contact.PHONE_TYPE_MMS) -> typeCode = Phone.TYPE_MMS types.contains(Contact.PHONE_TYPE_OTHER) || types.contains(TelephoneType.VOICE) || types.contains(TelephoneType.TEXT) -> {} Loading Loading @@ -768,7 +786,10 @@ open class AndroidContact( protected open fun insertEmail(batch: BatchOperation, labeledEmail: LabeledProperty<ezvcard.property.Email>) { val email = labeledEmail.property // drop TYPE=internet and TYPE=x400 because Android only knows Internet email addresses // drop TYPE=other for compatibility, too (non-standard type which is only used by some clients and not useful as an explicit value) val types = email.types types.removeAll(arrayOf(EmailType.INTERNET, EmailType.X400, Contact.EMAIL_TYPE_OTHER)) // preferred email address? var pref: Int? = null Loading @@ -795,7 +816,7 @@ open class AndroidContact( EmailType.WORK -> typeCode = Email.TYPE_WORK Contact.EMAIL_TYPE_MOBILE -> typeCode = Email.TYPE_MOBILE } if (typeCode == 0) { if (typeCode == 0) { // we still didn't find a known type if (email.types.isEmpty()) typeCode = Email.TYPE_OTHER else { Loading Loading @@ -869,7 +890,6 @@ open class AndroidContact( var typeCode: Int = Im.TYPE_OTHER var typeLabel: String? = null if (labeledImpp.label != null) { typeCode = Im.TYPE_CUSTOM typeLabel = labeledImpp.label Loading Loading @@ -1041,22 +1061,23 @@ open class AndroidContact( formattedAddress = lines.joinToString("\n") } val types = address.types var typeCode = StructuredPostal.TYPE_OTHER var typeLabel: String? = null if (labeledAddress.label != null) { typeCode = StructuredPostal.TYPE_CUSTOM typeLabel = labeledAddress.label } else { for (type in address.types) when (type) { AddressType.HOME -> typeCode = StructuredPostal.TYPE_HOME AddressType.WORK -> typeCode = StructuredPostal.TYPE_WORK } if (typeCode == StructuredPostal.TYPE_OTHER && address.types.isNotEmpty()) { when { types.contains(AddressType.HOME) -> typeCode = StructuredPostal.TYPE_HOME types.contains(AddressType.WORK) -> typeCode = StructuredPostal.TYPE_WORK types.contains(Contact.ADDRESS_TYPE_OTHER) -> {} types.isNotEmpty() -> { typeCode = StructuredPostal.TYPE_CUSTOM typeLabel = xNameToLabel(address.types.first().value) } } } val op: BatchOperation.Operation val builder = ContentProviderOperation.newInsert(dataSyncURI()) Loading
src/main/java/foundation/e/vcard4android/Contact.kt +9 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ package foundation.e.vcard4android import ezvcard.Ezvcard import ezvcard.VCard import ezvcard.VCardVersion import ezvcard.parameter.AddressType import ezvcard.parameter.EmailType import ezvcard.parameter.ImageType import ezvcard.parameter.TelephoneType Loading Loading @@ -90,8 +91,16 @@ class Contact { val PHONE_TYPE_RADIO = TelephoneType.get("x-radio")!! val PHONE_TYPE_ASSISTANT = TelephoneType.get("X-assistant")!! val PHONE_TYPE_MMS = TelephoneType.get("x-mms")!! /** Sometimes used to denote an "other" phone numbers. Only for compatibility – don't use it yourself! */ val PHONE_TYPE_OTHER = TelephoneType.get("other")!! /** Custom email type to denote "mobile" email addresses. */ val EMAIL_TYPE_MOBILE = EmailType.get("x-mobile")!! /** Sometimes used to denote an "other" email address. Only for compatibility – don't use it yourself! */ val EMAIL_TYPE_OTHER = EmailType.get("other")!! /** Sometimes used to denote an "other" postal address. Only for compatibility – don't use it yourself! */ val ADDRESS_TYPE_OTHER = AddressType.get("other")!! const val NICKNAME_TYPE_MAIDEN_NAME = "x-maiden-name" const val NICKNAME_TYPE_SHORT_NAME = "x-short-name" Loading