From 3bc02c9dde4a22d6697e0adef1e82fcf714e0202 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 2 Aug 2021 13:34:05 +0200 Subject: Support DAVx5 strategies for groups --- .../at/bitfire/vcard4android/AndroidContactTest.kt | 10 +- .../at/bitfire/vcard4android/AndroidGroupTest.kt | 6 +- .../at/bitfire/vcard4android/AndroidAddressBook.kt | 10 +- .../at/bitfire/vcard4android/AndroidContact.kt | 5 +- .../java/at/bitfire/vcard4android/AndroidGroup.kt | 114 ++++++++++++--------- src/main/java/at/bitfire/vcard4android/Contact.kt | 4 +- .../java/at/bitfire/vcard4android/ContactWriter.kt | 8 +- .../vcard4android/datarow/ContactProcessor.kt | 1 + .../java/at/bitfire/vcard4android/ContactTest.kt | 4 +- .../at/bitfire/vcard4android/ContactWriterTest.kt | 24 ++--- 10 files changed, 101 insertions(+), 85 deletions(-) diff --git a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt index cb5a948..b366b83 100644 --- a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt +++ b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt @@ -81,7 +81,7 @@ class AndroidContactTest { val contact = AndroidContact(addressBook, vcard, null, null) contact.add() - val contact2 = addressBook.findContactByID(contact.id!!) + val contact2 = addressBook.findContactById(contact.id!!) try { val vcard2 = contact2.getContact() assertEquals(vcard.displayName, vcard2.displayName) @@ -114,7 +114,7 @@ class AndroidContactTest { val dbContact = AndroidContact(addressBook, contacts.first(), null, null) dbContact.add() - val dbContact2 = addressBook.findContactByID(dbContact.id!!) + val dbContact2 = addressBook.findContactById(dbContact.id!!) try { val contact2 = dbContact2.getContact() assertEquals("Test", contact2.displayName) @@ -136,7 +136,7 @@ class AndroidContactTest { val contact = AndroidContact(addressBook, vcard, null, null) contact.add() - val contact2 = addressBook.findContactByID(contact.id!!) + val contact2 = addressBook.findContactById(contact.id!!) try { val vcard2 = contact2.getContact() assertEquals(4000, vcard2.emails.size) @@ -181,7 +181,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.writeVCard(VCardVersion.V4_0, GroupMethod.GROUP_VCARDS, os) + contact.writeVCard(VCardVersion.V4_0, os) assertTrue(os.toString().contains("ADR;LABEL=My ^'Label^'\\nLine 2:;;Street \"Address\";;;;")) } @@ -194,7 +194,7 @@ class AndroidContactTest { val contact = AndroidContact(addressBook, vcard, null, null) contact.add() - val contact2 = addressBook.findContactByID(contact.id!!) + val contact2 = addressBook.findContactById(contact.id!!) try { val vcard2 = contact2.getContact() assertEquals(vcard.displayName, vcard2.displayName) diff --git a/src/androidTest/java/at/bitfire/vcard4android/AndroidGroupTest.kt b/src/androidTest/java/at/bitfire/vcard4android/AndroidGroupTest.kt index cff3c82..ca6cb1e 100644 --- a/src/androidTest/java/at/bitfire/vcard4android/AndroidGroupTest.kt +++ b/src/androidTest/java/at/bitfire/vcard4android/AndroidGroupTest.kt @@ -63,9 +63,9 @@ class AndroidGroupTest { group.add() val groups = addressBook.queryGroups("${ContactsContract.Groups.TITLE}=?", arrayOf(contact.displayName!!)) assertEquals(1, groups.size) - val contact2 = groups.first().contact - assertEquals(contact.displayName, contact2?.displayName) - assertEquals(contact.note, contact2?.note) + val contact2 = groups.first().getContact() + assertEquals(contact.displayName, contact2.displayName) + assertEquals(contact.note, contact2.note) // delete group group.delete() diff --git a/src/main/java/at/bitfire/vcard4android/AndroidAddressBook.kt b/src/main/java/at/bitfire/vcard4android/AndroidAddressBook.kt index fcc325c..af0c1d6 100644 --- a/src/main/java/at/bitfire/vcard4android/AndroidAddressBook.kt +++ b/src/main/java/at/bitfire/vcard4android/AndroidAddressBook.kt @@ -80,13 +80,15 @@ open class AndroidAddressBook( return groups } - fun findContactByID(id: Long) = - queryContacts("${RawContacts._ID}=?", arrayOf(id.toString())).firstOrNull() - ?: throw FileNotFoundException() + fun findContactById(id: Long) = + queryContacts("${RawContacts._ID}=?", arrayOf(id.toString())).firstOrNull() ?: throw FileNotFoundException() - fun findContactByUID(uid: String) = + fun findContactByUid(uid: String) = queryContacts("${AndroidContact.COLUMN_UID}=?", arrayOf(uid)).firstOrNull() + fun findGroupById(id: Long) = + queryGroups("${Groups._ID}=?", arrayOf(id.toString())).firstOrNull() ?: throw FileNotFoundException() + // helpers diff --git a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt index d1f18fd..45ca96b 100644 --- a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt +++ b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt @@ -55,7 +55,6 @@ open class AndroidContact( setContact(_contact) } - /** * Creates a new instance, initialized with metadata from the content provider. Usually used when reading a contact from an address book. */ @@ -137,8 +136,8 @@ open class AndroidContact( return resultUri } - fun update(contact: Contact): Uri { - setContact(contact) + fun update(data: Contact): Uri { + setContact(data) val batch = BatchOperation(addressBook.provider!!) val uri = rawContactSyncURI() diff --git a/src/main/java/at/bitfire/vcard4android/AndroidGroup.kt b/src/main/java/at/bitfire/vcard4android/AndroidGroup.kt index 3670c29..8447184 100644 --- a/src/main/java/at/bitfire/vcard4android/AndroidGroup.kt +++ b/src/main/java/at/bitfire/vcard4android/AndroidGroup.kt @@ -37,77 +37,91 @@ open class AndroidGroup( var eTag: String? = null constructor(addressBook: AndroidAddressBook, values: ContentValues): this(addressBook) { - this.id = values.getAsLong(Groups._ID) - this.fileName = values.getAsString(COLUMN_FILENAME) - this.eTag = values.getAsString(COLUMN_ETAG) + initializeFromContentValues(values) } constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String? = null, eTag: String? = null): this(addressBook) { - this.contact = contact + _contact = contact this.fileName = fileName this.eTag = eTag } - var contact: Contact? = null + protected open fun initializeFromContentValues(values: ContentValues) { + id = values.getAsLong(Groups._ID) + fileName = values.getAsString(COLUMN_FILENAME) + eTag = values.getAsString(COLUMN_ETAG) + } + + + /** + * Cached copy of the [Contact]. If this is null, [getContact] must generate the [Contact] + * from the database and then set this property. + */ + protected var _contact: Contact? = null + /** - * Creates a [Contact] (representation of a vCard) from the group. + * Fetches group data from the content provider. + * * @throws IllegalArgumentException if group has not been saved yet * @throws FileNotFoundException when the group is not available (anymore) * @throws RemoteException on contact provider errors */ - get() { - field?.let { return field } - - val id = requireNotNull(id) - val c = Contact() - addressBook.provider!!.query(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)), - arrayOf(COLUMN_UID, Groups.TITLE, Groups.NOTES), null, null, null)?.use { cursor -> - if (!cursor.moveToNext()) - throw FileNotFoundException("Contact group not found") - - c.uid = cursor.getString(0) - c.group = true - c.displayName = cursor.getString(1) - c.note = cursor.getString(2) - } + fun getContact(): Contact { + _contact?.let { return it } - // query UIDs of all contacts which are member of the group - addressBook.provider.query(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), - arrayOf(Data.RAW_CONTACT_ID), - GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", - arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), null)?.use { cursor -> - while (cursor.moveToNext()) { - val contactID = cursor.getLong(0) - Constants.log.fine("Member ID: $contactID") - - addressBook.provider.query(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, contactID)), - arrayOf(AndroidContact.COLUMN_UID), null, null, null)?.use { cursor -> - if (cursor.moveToNext()) { - val uid = cursor.getString(0) - if (!uid.isNullOrEmpty()) { - Constants.log.fine("Found member of group: $uid") - c.members += uid - } - } + val id = requireNotNull(id) + val contact = Contact() + addressBook.provider!!.query(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)), + arrayOf(COLUMN_UID, Groups.TITLE, Groups.NOTES), null, null, null)?.use { cursor -> + if (!cursor.moveToNext()) + throw FileNotFoundException("Contact group not found") + + contact.group = true + contact.uid = cursor.getString(0) + contact.displayName = cursor.getString(1) + contact.note = cursor.getString(2) + } + + // get all contacts which are member of the group + addressBook.provider.query(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), + arrayOf(Data.RAW_CONTACT_ID), + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", + arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), null)?.use { membershipCursor -> + while (membershipCursor.moveToNext()) { + val contactId = membershipCursor.getLong(0) + Constants.log.fine("Member ID: $contactId") + + // get UID from the member + addressBook.provider.query(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, contactId)), + arrayOf(AndroidContact.COLUMN_UID), null, null, null)?.use { rawContactCursor -> + if (rawContactCursor.moveToNext()) { + val uid = rawContactCursor.getString(0) + if (!uid.isNullOrBlank()) { + Constants.log.fine("Found member of group: $uid") + // add UID to contact members (vCard MEMBERS field) + contact.members += uid + } else + Constants.log.severe("Couldn't add member $contactId to group contact because it doesn't have an UID (yet)") } } } - - field = c - return c } + _contact = contact + return contact + } + @CallSuper protected open fun contentValues(): ContentValues { val values = ContentValues() values.put(COLUMN_FILENAME, fileName) values.put(COLUMN_ETAG, eTag) - contact?.let { - values.put(COLUMN_UID, it.uid) - values.put(Groups.TITLE, it.displayName) - values.put(Groups.NOTES, it.note) - } + + val contact = getContact() + values.put(COLUMN_UID, contact.uid) + values.put(Groups.TITLE, contact.displayName) + values.put(Groups.NOTES, contact.note) return values } @@ -132,12 +146,12 @@ open class AndroidGroup( /** * Updates a group from a [Contact], which represents a vCard received from the * CardDAV server. - * @param contact data object to take group title, members etc. from + * @param data data object to take group title, members etc. from * @return number of affected rows * @throws RemoteException on contact provider errors */ - fun update(contact: Contact): Uri { - this.contact = contact + fun update(data: Contact): Uri { + _contact = data return update(contentValues()) } diff --git a/src/main/java/at/bitfire/vcard4android/Contact.kt b/src/main/java/at/bitfire/vcard4android/Contact.kt index 2d5ad6d..6935570 100644 --- a/src/main/java/at/bitfire/vcard4android/Contact.kt +++ b/src/main/java/at/bitfire/vcard4android/Contact.kt @@ -127,8 +127,8 @@ class Contact { @Throws(IOException::class) - fun writeVCard(vCardVersion: VCardVersion, groupMethod: GroupMethod, os: OutputStream) { - val generator = ContactWriter.fromContact(this, vCardVersion, groupMethod) + fun writeVCard(vCardVersion: VCardVersion, os: OutputStream) { + val generator = ContactWriter.fromContact(this, vCardVersion) generator.writeVCard(os) } diff --git a/src/main/java/at/bitfire/vcard4android/ContactWriter.kt b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt index a26bf94..a737960 100644 --- a/src/main/java/at/bitfire/vcard4android/ContactWriter.kt +++ b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt @@ -21,7 +21,7 @@ import java.util.logging.Level * * 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) { +class ContactWriter private constructor(val contact: Contact, val version: VCardVersion) { private val unknownProperties = LinkedList() val vCard = VCard() @@ -31,8 +31,8 @@ class ContactWriter private constructor(val contact: Contact, val version: VCard companion object { - fun fromContact(contact: Contact, version: VCardVersion, groupMethod: GroupMethod) = - ContactWriter(contact, version, groupMethod) + fun fromContact(contact: Contact, version: VCardVersion) = + ContactWriter(contact, version) } @@ -140,7 +140,7 @@ class ContactWriter private constructor(val contact: Contact, val version: VCard } private fun addKindAndMembers() { - if (contact.group && groupMethod == GroupMethod.GROUP_VCARDS) { + if (contact.group) { // TODO Use urn:uuid only when applicable if (version == VCardVersion.V4_0) { // vCard4 vCard.kind = Kind.group() diff --git a/src/main/java/at/bitfire/vcard4android/datarow/ContactProcessor.kt b/src/main/java/at/bitfire/vcard4android/datarow/ContactProcessor.kt index 178549a..c34470a 100644 --- a/src/main/java/at/bitfire/vcard4android/datarow/ContactProcessor.kt +++ b/src/main/java/at/bitfire/vcard4android/datarow/ContactProcessor.kt @@ -3,6 +3,7 @@ package at.bitfire.vcard4android.datarow import android.content.ContentProviderClient import android.content.ContentValues import android.net.Uri +import android.provider.ContactsContract.CommonDataKinds.GroupMembership import android.provider.ContactsContract.RawContacts import at.bitfire.vcard4android.AndroidContact import at.bitfire.vcard4android.BatchOperation diff --git a/src/test/java/at/bitfire/vcard4android/ContactTest.kt b/src/test/java/at/bitfire/vcard4android/ContactTest.kt index d003f26..786ad30 100644 --- a/src/test/java/at/bitfire/vcard4android/ContactTest.kt +++ b/src/test/java/at/bitfire/vcard4android/ContactTest.kt @@ -33,13 +33,13 @@ class ContactTest { private fun regenerate(c: Contact, vCardVersion: VCardVersion): Contact { val os = ByteArrayOutputStream() - c.writeVCard(vCardVersion, GroupMethod.CATEGORIES, os) + c.writeVCard(vCardVersion, 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.writeVCard(vCardVersion, groupMethod, os) + c.writeVCard(vCardVersion, os) return os.toString() } diff --git a/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt index 8eae372..d8a5dde 100644 --- a/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt +++ b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt @@ -192,7 +192,7 @@ class ContactWriterTest { @Test fun testKindAndMember_vCard3() { - val vCard = generate(GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) { + val vCard = generate(VCardVersion.V3_0) { group = true members += "member1" } @@ -202,7 +202,7 @@ class ContactWriterTest { @Test fun testKindAndMember_vCard4() { - val vCard = generate(GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) { + val vCard = generate(VCardVersion.V4_0) { group = true members += "member1" } @@ -478,7 +478,7 @@ class ContactWriterTest { @Test fun testRewritePartialDate_vCard3_Date() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0) val date = Birthday(Date(121, 6, 30)) generator.rewritePartialDate(date) assertEquals(Date(121, 6, 30), date.date) @@ -487,7 +487,7 @@ class ContactWriterTest { @Test fun testRewritePartialDate_vCard4_Date() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0) val date = Birthday(Date(121, 6, 30)) generator.rewritePartialDate(date) assertEquals(Date(121, 6, 30), date.date) @@ -497,7 +497,7 @@ class ContactWriterTest { @Test fun testRewritePartialDate_vCard3_PartialDateWithYear() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0) val date = Birthday(PartialDate.parse("20210730")) generator.rewritePartialDate(date) assertEquals(Date(121, 6, 30), date.date) @@ -507,7 +507,7 @@ class ContactWriterTest { @Test fun testRewritePartialDate_vCard4_PartialDateWithYear() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0) val date = Birthday(PartialDate.parse("20210730")) generator.rewritePartialDate(date) assertNull(date.date) @@ -517,7 +517,7 @@ class ContactWriterTest { @Test fun testRewritePartialDate_vCard3_PartialDateWithoutYear() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0) val date = Birthday(PartialDate.parse("--0730")) generator.rewritePartialDate(date) assertEquals(Date(-300+4, 6, 30), date.date) @@ -528,7 +528,7 @@ class ContactWriterTest { @Test fun testRewritePartialDate_vCard4_PartialDateWithoutYear() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0) val date = Birthday(PartialDate.parse("--0730")) generator.rewritePartialDate(date) assertNull(date.date) @@ -539,7 +539,7 @@ class ContactWriterTest { @Test fun testWriteVCard() { - val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0) generator.vCard.revision = Revision(Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC.id)).apply { set(2021, 6, 30, 1, 2, 3) }) @@ -564,7 +564,7 @@ class ContactWriterTest { }) } ContactWriter - .fromContact(contact, VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + .fromContact(contact, VCardVersion.V4_0) .writeVCard(stream) assertTrue(stream.toString().contains("ADR;LABEL=\"Li^^ne 1,1 - ^' -\":;;Line1;;;;Line2")) } @@ -572,10 +572,10 @@ class ContactWriterTest { // helpers - private fun generate(groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS, version: VCardVersion = VCardVersion.V4_0, prepare: Contact.() -> Unit): VCard { + private fun generate(version: VCardVersion = VCardVersion.V4_0, prepare: Contact.() -> Unit): VCard { val contact = Contact() contact.run(prepare) - return ContactWriter.fromContact(contact, version, groupMethod).vCard + return ContactWriter.fromContact(contact, version).vCard } } \ No newline at end of file -- cgit v1.2.3