From 53bd0f06c57d0fb93c9879c220918e36606237d4 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 15:36:00 +0200 Subject: [PATCH 1/9] Move UnknownProperties and GroupMembership from synctools to davx5-ose --- .../storage/contacts/AndroidAddressBook.kt | 22 ++++++- .../storage/contacts/AndroidContact.kt | 62 ++++++++++++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt index 4bab217d..29da8dfe 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt @@ -1,18 +1,19 @@ /* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. */ package at.bitfire.synctools.storage.contacts import android.accounts.Account import android.content.ContentProviderClient +import android.content.ContentUris import android.content.ContentValues import android.net.Uri +import android.os.RemoteException import android.provider.ContactsContract import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts +import androidx.core.content.contentValuesOf import at.bitfire.synctools.storage.toContentValues import java.io.FileNotFoundException import java.util.LinkedList @@ -118,6 +119,21 @@ open class AndroidAddressBook( fun findGroupById(id: Long) = queryGroups("${Groups._ID}=?", arrayOf(id.toString())).firstOrNull() ?: throw FileNotFoundException() + fun findOrCreateGroup(title: String): Long { + provider!!.query( + syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID), + "${Groups.TITLE}=?", arrayOf(title), null + )?.use { cursor -> + if (cursor.moveToNext()) + return cursor.getLong(0) + } + + val values = contentValuesOf(Groups.TITLE to title) + val uri = provider!!.insert(syncAdapterURI(Groups.CONTENT_URI), values) + ?: throw RemoteException("Couldn't create contact group") + return ContentUris.parseId(uri) + } + // helpers diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index af6e2c70..eb41802c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -1,7 +1,5 @@ /* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. */ package at.bitfire.synctools.storage.contacts @@ -13,6 +11,7 @@ import android.database.DatabaseUtils import android.net.Uri import android.os.RemoteException import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership import android.provider.ContactsContract.RawContacts import android.provider.ContactsContract.RawContacts.Data import androidx.annotation.CallSuper @@ -46,6 +45,9 @@ open class AndroidContact( var eTag: String? = null + val cachedGroupMemberships = HashSet() + val groupMemberships = HashSet() + /** * Creates a new instance, initialized with some metadata. Usually used to insert a contact to an address book. @@ -212,6 +214,60 @@ open class AndroidContact( } + // group membership helpers + + fun addToGroup(batch: ContactsBatchOperation, groupID: Long) { + batch += BatchOperation.CpoBuilder + .newInsert(dataSyncURI()) + .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) + .withValue(GroupMembership.RAW_CONTACT_ID, id) + .withValue(GroupMembership.GROUP_ROW_ID, groupID) + groupMemberships += groupID + + batch += BatchOperation.CpoBuilder + .newInsert(dataSyncURI()) + .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + .withValue(CachedGroupMembership.RAW_CONTACT_ID, id) + .withValue(CachedGroupMembership.GROUP_ID, groupID) + cachedGroupMemberships += groupID + } + + fun removeGroupMemberships(batch: BatchOperation) { + batch += BatchOperation.CpoBuilder + .newDelete(dataSyncURI()) + .withSelection( + "${Data.RAW_CONTACT_ID}=? AND ${Data.MIMETYPE} IN (?,?)", + arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + ) + groupMemberships.clear() + cachedGroupMemberships.clear() + } + + /** + * Returns the IDs of all groups the contact was member of (cached memberships). + * Cached memberships are kept in sync with memberships by DAVx5 and are used to determine + * whether a membership has been deleted/added when a raw contact is dirty. + * @return set of [GroupMembership.GROUP_ROW_ID] (may be empty) + * @throws FileNotFoundException if the current contact can't be found + * @throws RemoteException on contacts provider errors + */ + fun getCachedGroupMemberships(): Set { + getContact() + return cachedGroupMemberships + } + + /** + * Returns the IDs of all groups the contact is member of. + * @return set of [GroupMembership.GROUP_ROW_ID]s (may be empty) + * @throws FileNotFoundException if the current contact can't be found + * @throws RemoteException on contacts provider errors + */ + fun getGroupMemberships(): Set { + getContact() + return groupMemberships + } + + // helpers fun rawContactSyncURI(): Uri { From 37de6b0dd59bfa6f10fbfe297a59480764d9efae Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 15:47:12 +0200 Subject: [PATCH 2/9] Don't initialize handlers/builders in davx5-ose anymore (everything is handled in synctools) --- .../builder/GroupMembershipBuilderTest.kt | 77 +++++++++++++++++ .../CachedGroupMembershipHandlerTest.kt | 65 +++++++++++++++ .../handler/GroupMembershipHandlerTest.kt | 83 +++++++++++++++++++ .../mapping/contacts/RawContactBuilder.kt | 6 +- .../mapping/contacts/RawContactHandler.kt | 6 +- .../builder/GroupMembershipBuilder.kt | 49 +++++++++++ .../builder/UnknownPropertiesBuilder.kt | 31 +++++++ .../handler/CachedGroupMembershipHandler.kt | 27 ++++++ .../handler/GroupMembershipHandler.kt | 38 +++++++++ .../handler/UnknownPropertiesHandler.kt | 21 +++++ .../storage/contacts/AndroidAddressBook.kt | 2 + .../storage/contacts/AndroidContact.kt | 24 +++++- .../storage/contacts/UnknownProperties.kt | 17 ++++ .../builder/UnknownPropertiesBuilderTest.kt | 33 ++++++++ .../handler/UnknownPropertiesHandlerTest.kt | 34 ++++++++ 15 files changed, 502 insertions(+), 11 deletions(-) create mode 100644 lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt create mode 100644 lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt create mode 100644 lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt new file mode 100644 index 00000000..aa5260b3 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.builder + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.net.Uri +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.TestAddressBook +import at.bitfire.synctools.vcard.GroupMethod +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test + +class GroupMembershipBuilderTest { + + @Test + fun testCategories_GroupsAsCategories() { + val addressBook = TestAddressBook(account, provider) + val contact = Contact().apply { + categories += "TEST GROUP" + } + GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBook, GroupMethod.CATEGORIES, false).build().also { result -> + assertEquals(1, result.size) + assertEquals(GroupMembership.CONTENT_ITEM_TYPE, result[0].values[GroupMembership.MIMETYPE]) + assertEquals(addressBook.findOrCreateGroup("TEST GROUP"), result[0].values[GroupMembership.GROUP_ROW_ID]) + } + } + + @Test + fun testCategories_GroupsAsVCards() { + val addressBook = TestAddressBook(account, provider) + val contact = Contact().apply { + categories += "TEST GROUP" + } + GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBook, GroupMethod.GROUP_VCARDS, false).build().also { result -> + // group membership is constructed during post-processing + assertEquals(0, result.size) + } + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + val account = Account("GroupMembershipBuilderTest", "at.bitfire.vcard4android") + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + + } + +} diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt new file mode 100644 index 00000000..04f5b6bd --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.handler + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentValues +import android.provider.ContactsContract +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.AndroidContact +import at.bitfire.synctools.storage.contacts.CachedGroupMembership +import at.bitfire.synctools.storage.contacts.TestAddressBook +import at.bitfire.synctools.vcard.GroupMethod +import org.junit.AfterClass +import org.junit.Assert.assertArrayEquals +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test + +class CachedGroupMembershipHandlerTest { + + @Test + fun testMembership() { + val addressBook = TestAddressBook(account, provider) + val contact = Contact() + val androidContact = AndroidContact(addressBook, contact, null, null) + CachedGroupMembershipHandler(androidContact, GroupMethod.GROUP_VCARDS).handle(ContentValues().apply { + put(CachedGroupMembership.GROUP_ID, 123456) + put(CachedGroupMembership.RAW_CONTACT_ID, 789) + }, contact) + assertArrayEquals(arrayOf(123456L), androidContact.cachedGroupMemberships.toArray()) + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + val account = Account("CachedGroupMembershipHandlerTest", "at.bitfire.vcard4android") + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + + } + +} diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt new file mode 100644 index 00000000..aceebf55 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.handler + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentValues +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.AndroidContact +import at.bitfire.synctools.storage.contacts.CachedGroupMembership +import at.bitfire.synctools.storage.contacts.TestAddressBook +import at.bitfire.synctools.vcard.GroupMethod +import org.junit.AfterClass +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test + +class GroupMembershipHandlerTest { + + @Test + fun testMembership_GroupsAsCategories() { + val addressBook = TestAddressBook(account, provider) + val groupId = addressBook.findOrCreateGroup("TEST GROUP") + + val contact = Contact() + val androidContact = AndroidContact(addressBook, contact, null, null) + GroupMembershipHandler(androidContact, GroupMethod.CATEGORIES).handle(ContentValues().apply { + put(GroupMembership.GROUP_ROW_ID, groupId) + put(CachedGroupMembership.RAW_CONTACT_ID, -1) + }, contact) + assertArrayEquals(arrayOf(groupId), androidContact.groupMemberships.toArray()) + assertArrayEquals(arrayOf("TEST GROUP"), contact.categories.toArray()) + } + + @Test + fun testMembership_GroupsAsVCards() { + val addressBook = TestAddressBook(account, provider) + val contact = Contact() + val androidContact = AndroidContact(addressBook, contact, null, null) + GroupMembershipHandler(androidContact, GroupMethod.GROUP_VCARDS).handle(ContentValues().apply { + put(GroupMembership.GROUP_ROW_ID, 12345L) // group doesn't have to really exist for GROUP_VCARDS + put(CachedGroupMembership.RAW_CONTACT_ID, -1) + }, contact) + assertArrayEquals(arrayOf(12345L), androidContact.groupMemberships.toArray()) + assertTrue(contact.categories.isEmpty()) + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + val account = Account("GroupMembershipHandlerTest", "at.bitfire.vcard4android") + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt index e14047ec..538568f2 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt @@ -1,7 +1,5 @@ /* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. */ package at.bitfire.synctools.mapping.contacts @@ -41,7 +39,7 @@ class RawContactBuilder { WebsiteBuilder.Factory ) - fun registerBuilderFactory(factory: DataRowBuilder.Factory<*>) { + internal fun registerBuilderFactory(factory: DataRowBuilder.Factory<*>) { dataRowBuilderFactories += factory } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt index 6dfe6df8..177e7aed 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt @@ -1,7 +1,5 @@ /* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. */ package at.bitfire.synctools.mapping.contacts @@ -53,7 +51,7 @@ class RawContactHandler( registerHandler(handler) } - fun registerHandler(handler: DataRowHandler) { + internal fun registerHandler(handler: DataRowHandler) { val mimeType = handler.forMimeType() val handlers = dataRowHandlers[mimeType] ?: run { val newList = mutableListOf() diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt new file mode 100644 index 00000000..6294d8b9 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt @@ -0,0 +1,49 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.builder + +import android.net.Uri +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.contacts.AndroidAddressBook +import at.bitfire.synctools.vcard.GroupMethod +import java.util.LinkedList + +class GroupMembershipBuilder( + dataRowUri: Uri, + rawContactId: Long?, + contact: Contact, + val addressBook: AndroidAddressBook<*, *>, + val groupMethod: GroupMethod, + readOnly: Boolean +) : DataRowBuilder(Factory.MIME_TYPE, dataRowUri, rawContactId, contact, readOnly) { + + override fun build(): List { + val result = LinkedList() + + if (groupMethod == GroupMethod.CATEGORIES) + for (category in contact.categories) + result += newDataRow().withValue(GroupMembership.GROUP_ROW_ID, addressBook.findOrCreateGroup(category)) + else { + // GroupMethod.GROUP_VCARDS -> memberships are handled by AndroidGroups (and not by the members = AndroidContacts, which we are processing here) + // TODO: CATEGORIES <-> unknown properties + } + + return result + } + + + class Factory(val addressBook: AndroidAddressBook<*, *>, val groupMethod: GroupMethod) : DataRowBuilder.Factory { + companion object { + const val MIME_TYPE = GroupMembership.CONTENT_ITEM_TYPE + } + + override fun mimeType() = MIME_TYPE + override fun newInstance(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) = + GroupMembershipBuilder(dataRowUri, rawContactId, contact, addressBook, groupMethod, readOnly) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt new file mode 100644 index 00000000..7a73e165 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt @@ -0,0 +1,31 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.builder + +import android.net.Uri +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.contacts.UnknownProperties +import java.util.LinkedList + +class UnknownPropertiesBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) : + DataRowBuilder(Factory.mimeType(), dataRowUri, rawContactId, contact, readOnly) { + + override fun build(): List { + val result = LinkedList() + contact.unknownProperties?.let { unknownProperties -> + result += newDataRow().withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties) + } + return result + } + + + object Factory : DataRowBuilder.Factory { + override fun mimeType() = UnknownProperties.CONTENT_ITEM_TYPE + override fun newInstance(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) = + UnknownPropertiesBuilder(dataRowUri, rawContactId, contact, readOnly) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt new file mode 100644 index 00000000..bc36641a --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt @@ -0,0 +1,27 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.handler + +import android.content.ContentValues +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.AndroidContact +import at.bitfire.synctools.storage.contacts.CachedGroupMembership +import at.bitfire.synctools.vcard.GroupMethod +import java.util.logging.Logger + +class CachedGroupMembershipHandler(val androidContact: AndroidContact, val groupMethod: GroupMethod) : DataRowHandler() { + + override fun forMimeType() = CachedGroupMembership.CONTENT_ITEM_TYPE + + override fun handle(values: ContentValues, contact: Contact) { + super.handle(values, contact) + + if (groupMethod == GroupMethod.GROUP_VCARDS) + androidContact.cachedGroupMemberships += values.getAsLong(CachedGroupMembership.GROUP_ID) + else + Logger.getGlobal().warning("Ignoring cached group membership for group method CATEGORIES") + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt new file mode 100644 index 00000000..7e2dcce0 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt @@ -0,0 +1,38 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.handler + +import android.content.ContentValues +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.AndroidContact +import at.bitfire.synctools.util.Utils.trimToNull +import at.bitfire.synctools.vcard.GroupMethod +import java.io.FileNotFoundException + +class GroupMembershipHandler(val androidContact: AndroidContact, val groupMethod: GroupMethod) : DataRowHandler() { + + override fun forMimeType() = GroupMembership.CONTENT_ITEM_TYPE + + override fun handle(values: ContentValues, contact: Contact) { + super.handle(values, contact) + + val groupId = values.getAsLong(GroupMembership.GROUP_ROW_ID) + androidContact.groupMemberships += groupId + + if (groupMethod == GroupMethod.CATEGORIES) { + try { + val group = androidContact.addressBook.findGroupById(groupId) + group.getContact().displayName.trimToNull()?.let { groupName -> + logger.fine("Adding membership in group $groupName as category") + contact.categories.add(groupName) + } + } catch (ignored: FileNotFoundException) { + logger.warning("Contact is member in group $groupId which doesn't exist anymore") + } + } + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt new file mode 100644 index 00000000..94d9a444 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt @@ -0,0 +1,21 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.handler + +import android.content.ContentValues +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.UnknownProperties + +object UnknownPropertiesHandler : DataRowHandler() { + + override fun forMimeType() = UnknownProperties.CONTENT_ITEM_TYPE + + override fun handle(values: ContentValues, contact: Contact) { + super.handle(values, contact) + + contact.unknownProperties = values.getAsString(UnknownProperties.UNKNOWN_PROPERTIES) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt index 29da8dfe..da784e0c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt @@ -15,6 +15,7 @@ import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import androidx.core.content.contentValuesOf import at.bitfire.synctools.storage.toContentValues +import at.bitfire.synctools.vcard.GroupMethod import java.io.FileNotFoundException import java.util.LinkedList @@ -26,6 +27,7 @@ open class AndroidAddressBook( ) { open var readOnly: Boolean = false + open val groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS var settings: ContentValues /** diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index eb41802c..82e3068b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -18,15 +18,18 @@ import androidx.annotation.CallSuper import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.mapping.contacts.RawContactBuilder import at.bitfire.synctools.mapping.contacts.RawContactHandler +import at.bitfire.synctools.mapping.contacts.builder.GroupMembershipBuilder import at.bitfire.synctools.mapping.contacts.builder.PhotoBuilder +import at.bitfire.synctools.mapping.contacts.builder.UnknownPropertiesBuilder +import at.bitfire.synctools.mapping.contacts.handler.CachedGroupMembershipHandler +import at.bitfire.synctools.mapping.contacts.handler.GroupMembershipHandler +import at.bitfire.synctools.mapping.contacts.handler.UnknownPropertiesHandler import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.LocalStorageException import java.io.FileNotFoundException open class AndroidContact( - open val addressBook: AndroidAddressBook, - protected open val rawContactBuilder: RawContactBuilder = RawContactBuilder(), - protected open val rawContactHandler: RawContactHandler = RawContactHandler(addressBook.provider!!) + open val addressBook: AndroidAddressBook ) { companion object { @@ -45,6 +48,21 @@ open class AndroidContact( var eTag: String? = null + protected val rawContactHandler: RawContactHandler by lazy { + RawContactHandler(addressBook.provider!!).apply { + registerHandler(CachedGroupMembershipHandler(this@AndroidContact, addressBook.groupMethod)) + registerHandler(GroupMembershipHandler(this@AndroidContact, addressBook.groupMethod)) + registerHandler(UnknownPropertiesHandler) + } + } + + protected val rawContactBuilder: RawContactBuilder by lazy { + RawContactBuilder().apply { + registerBuilderFactory(GroupMembershipBuilder.Factory(addressBook, addressBook.groupMethod)) + registerBuilderFactory(UnknownPropertiesBuilder.Factory) + } + } + val cachedGroupMemberships = HashSet() val groupMemberships = HashSet() diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt new file mode 100644 index 00000000..39cb8489 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt @@ -0,0 +1,17 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.storage.contacts + +import android.provider.ContactsContract.RawContacts + +object UnknownProperties { + + const val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties" + + const val MIMETYPE = RawContacts.Data.MIMETYPE + const val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID + const val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1 + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt new file mode 100644 index 00000000..115046e5 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.builder + +import android.net.Uri +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.UnknownProperties +import org.junit.Assert.assertEquals +import org.junit.Test + +class UnknownPropertiesBuilderTest { + + @Test + fun testUnknownProperties_None() { + UnknownPropertiesBuilder(Uri.EMPTY, null, Contact(), false).build().also { result -> + assertEquals(0, result.size) + } + } + + @Test + fun testUnknownProperties_Properties() { + UnknownPropertiesBuilder(Uri.EMPTY, null, Contact().apply { + unknownProperties = "X-TEST:12345" + }, false).build().also { result -> + assertEquals(1, result.size) + assertEquals(UnknownProperties.CONTENT_ITEM_TYPE, result[0].values[UnknownProperties.MIMETYPE]) + assertEquals("X-TEST:12345", result[0].values[UnknownProperties.UNKNOWN_PROPERTIES]) + } + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt new file mode 100644 index 00000000..3a127cda --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.synctools.mapping.contacts.handler + +import android.content.ContentValues +import at.bitfire.synctools.mapping.contacts.Contact +import at.bitfire.synctools.storage.contacts.UnknownProperties +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class UnknownPropertiesHandlerTest { + + @Test + fun testUnknownProperties_Empty() { + val contact = Contact() + UnknownPropertiesHandler.handle(ContentValues().apply { + putNull(UnknownProperties.UNKNOWN_PROPERTIES) + }, contact) + assertNull(contact.unknownProperties) + } + + @Test + fun testUnknownProperties_Values() { + val contact = Contact() + UnknownPropertiesHandler.handle(ContentValues().apply { + put(UnknownProperties.UNKNOWN_PROPERTIES, "X-TEST:12345") + }, contact) + assertEquals("X-TEST:12345", contact.unknownProperties) + } + +} From 3da24a5301f54e10bcbc4a4019132c88dfcdb8a2 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:03:15 +0200 Subject: [PATCH 3/9] Add ...Contract to UnknownProperty and CachedGroupMembership definitions --- .../handler/CachedGroupMembershipHandlerTest.kt | 6 +++--- .../handler/GroupMembershipHandlerTest.kt | 6 +++--- .../contacts/builder/UnknownPropertiesBuilder.kt | 6 +++--- .../handler/CachedGroupMembershipHandler.kt | 6 +++--- .../contacts/handler/UnknownPropertiesHandler.kt | 6 +++--- .../synctools/storage/contacts/AndroidContact.kt | 8 ++++---- ...ership.kt => CachedGroupMembershipContract.kt} | 15 ++++++++++----- ...wnProperties.kt => UnknownPropertyContract.kt} | 11 +++++++++-- .../builder/UnknownPropertiesBuilderTest.kt | 6 +++--- .../handler/UnknownPropertiesHandlerTest.kt | 6 +++--- 10 files changed, 44 insertions(+), 32 deletions(-) rename lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/{CachedGroupMembership.kt => CachedGroupMembershipContract.kt} (65%) rename lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/{UnknownProperties.kt => UnknownPropertyContract.kt} (50%) diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt index 04f5b6bd..5bdb84e5 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt @@ -13,7 +13,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.AndroidContact -import at.bitfire.synctools.storage.contacts.CachedGroupMembership +import at.bitfire.synctools.storage.contacts.CachedGroupMembershipContract import at.bitfire.synctools.storage.contacts.TestAddressBook import at.bitfire.synctools.vcard.GroupMethod import org.junit.AfterClass @@ -30,8 +30,8 @@ class CachedGroupMembershipHandlerTest { val contact = Contact() val androidContact = AndroidContact(addressBook, contact, null, null) CachedGroupMembershipHandler(androidContact, GroupMethod.GROUP_VCARDS).handle(ContentValues().apply { - put(CachedGroupMembership.GROUP_ID, 123456) - put(CachedGroupMembership.RAW_CONTACT_ID, 789) + put(CachedGroupMembershipContract.GROUP_ID, 123456) + put(CachedGroupMembershipContract.RAW_CONTACT_ID, 789) }, contact) assertArrayEquals(arrayOf(123456L), androidContact.cachedGroupMemberships.toArray()) } diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt index aceebf55..0bd7fdd1 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt @@ -14,7 +14,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.AndroidContact -import at.bitfire.synctools.storage.contacts.CachedGroupMembership +import at.bitfire.synctools.storage.contacts.CachedGroupMembershipContract import at.bitfire.synctools.storage.contacts.TestAddressBook import at.bitfire.synctools.vcard.GroupMethod import org.junit.AfterClass @@ -35,7 +35,7 @@ class GroupMembershipHandlerTest { val androidContact = AndroidContact(addressBook, contact, null, null) GroupMembershipHandler(androidContact, GroupMethod.CATEGORIES).handle(ContentValues().apply { put(GroupMembership.GROUP_ROW_ID, groupId) - put(CachedGroupMembership.RAW_CONTACT_ID, -1) + put(CachedGroupMembershipContract.RAW_CONTACT_ID, -1) }, contact) assertArrayEquals(arrayOf(groupId), androidContact.groupMemberships.toArray()) assertArrayEquals(arrayOf("TEST GROUP"), contact.categories.toArray()) @@ -48,7 +48,7 @@ class GroupMembershipHandlerTest { val androidContact = AndroidContact(addressBook, contact, null, null) GroupMembershipHandler(androidContact, GroupMethod.GROUP_VCARDS).handle(ContentValues().apply { put(GroupMembership.GROUP_ROW_ID, 12345L) // group doesn't have to really exist for GROUP_VCARDS - put(CachedGroupMembership.RAW_CONTACT_ID, -1) + put(CachedGroupMembershipContract.RAW_CONTACT_ID, -1) }, contact) assertArrayEquals(arrayOf(12345L), androidContact.groupMemberships.toArray()) assertTrue(contact.categories.isEmpty()) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt index 7a73e165..4186d91a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt @@ -7,7 +7,7 @@ package at.bitfire.synctools.mapping.contacts.builder import android.net.Uri import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.BatchOperation -import at.bitfire.synctools.storage.contacts.UnknownProperties +import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import java.util.LinkedList class UnknownPropertiesBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) : @@ -16,14 +16,14 @@ class UnknownPropertiesBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Co override fun build(): List { val result = LinkedList() contact.unknownProperties?.let { unknownProperties -> - result += newDataRow().withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties) + result += newDataRow().withValue(UnknownPropertyContract.UNKNOWN_PROPERTIES, unknownProperties) } return result } object Factory : DataRowBuilder.Factory { - override fun mimeType() = UnknownProperties.CONTENT_ITEM_TYPE + override fun mimeType() = UnknownPropertyContract.CONTENT_ITEM_TYPE override fun newInstance(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) = UnknownPropertiesBuilder(dataRowUri, rawContactId, contact, readOnly) } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt index bc36641a..69e7f962 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt @@ -7,19 +7,19 @@ package at.bitfire.synctools.mapping.contacts.handler import android.content.ContentValues import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.AndroidContact -import at.bitfire.synctools.storage.contacts.CachedGroupMembership +import at.bitfire.synctools.storage.contacts.CachedGroupMembershipContract import at.bitfire.synctools.vcard.GroupMethod import java.util.logging.Logger class CachedGroupMembershipHandler(val androidContact: AndroidContact, val groupMethod: GroupMethod) : DataRowHandler() { - override fun forMimeType() = CachedGroupMembership.CONTENT_ITEM_TYPE + override fun forMimeType() = CachedGroupMembershipContract.CONTENT_ITEM_TYPE override fun handle(values: ContentValues, contact: Contact) { super.handle(values, contact) if (groupMethod == GroupMethod.GROUP_VCARDS) - androidContact.cachedGroupMemberships += values.getAsLong(CachedGroupMembership.GROUP_ID) + androidContact.cachedGroupMemberships += values.getAsLong(CachedGroupMembershipContract.GROUP_ID) else Logger.getGlobal().warning("Ignoring cached group membership for group method CATEGORIES") } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt index 94d9a444..80beabc7 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt @@ -6,16 +6,16 @@ package at.bitfire.synctools.mapping.contacts.handler import android.content.ContentValues import at.bitfire.synctools.mapping.contacts.Contact -import at.bitfire.synctools.storage.contacts.UnknownProperties +import at.bitfire.synctools.storage.contacts.UnknownPropertyContract object UnknownPropertiesHandler : DataRowHandler() { - override fun forMimeType() = UnknownProperties.CONTENT_ITEM_TYPE + override fun forMimeType() = UnknownPropertyContract.CONTENT_ITEM_TYPE override fun handle(values: ContentValues, contact: Contact) { super.handle(values, contact) - contact.unknownProperties = values.getAsString(UnknownProperties.UNKNOWN_PROPERTIES) + contact.unknownProperties = values.getAsString(UnknownPropertyContract.UNKNOWN_PROPERTIES) } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index 82e3068b..6b32f6b4 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -244,9 +244,9 @@ open class AndroidContact( batch += BatchOperation.CpoBuilder .newInsert(dataSyncURI()) - .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) - .withValue(CachedGroupMembership.RAW_CONTACT_ID, id) - .withValue(CachedGroupMembership.GROUP_ID, groupID) + .withValue(CachedGroupMembershipContract.MIMETYPE, CachedGroupMembershipContract.CONTENT_ITEM_TYPE) + .withValue(CachedGroupMembershipContract.RAW_CONTACT_ID, id) + .withValue(CachedGroupMembershipContract.GROUP_ID, groupID) cachedGroupMemberships += groupID } @@ -255,7 +255,7 @@ open class AndroidContact( .newDelete(dataSyncURI()) .withSelection( "${Data.RAW_CONTACT_ID}=? AND ${Data.MIMETYPE} IN (?,?)", - arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembershipContract.CONTENT_ITEM_TYPE) ) groupMemberships.clear() cachedGroupMemberships.clear() diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembership.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt similarity index 65% rename from lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembership.kt rename to lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt index 4ba95bfe..9bf75779 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembership.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt @@ -1,12 +1,11 @@ /* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. */ package at.bitfire.synctools.storage.contacts import android.provider.ContactsContract.RawContacts.Data +import at.bitfire.synctools.storage.contacts.CachedGroupMembershipContract.MIMETYPE /** * Represents a "cached group membership" row. Cached group memberships exist only @@ -22,12 +21,18 @@ import android.provider.ContactsContract.RawContacts.Data * * Cached group memberships must not be used for anything else that detecting dirty groups. */ -object CachedGroupMembership { +object CachedGroupMembershipContract { + /** Column name for the MIME type of the data row. Type: [String] */ + const val MIMETYPE = Data.MIMETYPE + + /** MIME type of cached group membership data rows. Stored in [MIMETYPE]. */ const val CONTENT_ITEM_TYPE = "x.davdroid/cached-group-membership" - const val MIMETYPE = Data.MIMETYPE + /** Column name for the ID of the raw contact this cached membership belongs to. Type: [Long] */ const val RAW_CONTACT_ID = Data.RAW_CONTACT_ID + + /** Column name for the ID of the group this cached membership refers to. Type: [Long] */ const val GROUP_ID = Data.DATA1 } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt similarity index 50% rename from lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt rename to lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt index 39cb8489..1cdd78f6 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownProperties.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt @@ -5,13 +5,20 @@ package at.bitfire.synctools.storage.contacts import android.provider.ContactsContract.RawContacts +import at.bitfire.synctools.storage.contacts.UnknownPropertyContract.MIMETYPE -object UnknownProperties { +object UnknownPropertyContract { + /** Column name for the MIME type of the data row. Type: [String] */ + const val MIMETYPE = RawContacts.Data.MIMETYPE + + /** MIME type of unknown-property data rows. Stored in [MIMETYPE]. */ const val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties" - const val MIMETYPE = RawContacts.Data.MIMETYPE + /** Column name for the ID of the raw contact this row belongs to. Type: [Long] */ const val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID + + /** Column name for the serialized unknown vCard properties. Type: [String] */ const val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1 } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt index 115046e5..cc532804 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt @@ -6,7 +6,7 @@ package at.bitfire.synctools.mapping.contacts.builder import android.net.Uri import at.bitfire.synctools.mapping.contacts.Contact -import at.bitfire.synctools.storage.contacts.UnknownProperties +import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import org.junit.Assert.assertEquals import org.junit.Test @@ -25,8 +25,8 @@ class UnknownPropertiesBuilderTest { unknownProperties = "X-TEST:12345" }, false).build().also { result -> assertEquals(1, result.size) - assertEquals(UnknownProperties.CONTENT_ITEM_TYPE, result[0].values[UnknownProperties.MIMETYPE]) - assertEquals("X-TEST:12345", result[0].values[UnknownProperties.UNKNOWN_PROPERTIES]) + assertEquals(UnknownPropertyContract.CONTENT_ITEM_TYPE, result[0].values[UnknownPropertyContract.MIMETYPE]) + assertEquals("X-TEST:12345", result[0].values[UnknownPropertyContract.UNKNOWN_PROPERTIES]) } } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt index 3a127cda..87ffee8c 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt @@ -6,7 +6,7 @@ package at.bitfire.synctools.mapping.contacts.handler import android.content.ContentValues import at.bitfire.synctools.mapping.contacts.Contact -import at.bitfire.synctools.storage.contacts.UnknownProperties +import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test @@ -17,7 +17,7 @@ class UnknownPropertiesHandlerTest { fun testUnknownProperties_Empty() { val contact = Contact() UnknownPropertiesHandler.handle(ContentValues().apply { - putNull(UnknownProperties.UNKNOWN_PROPERTIES) + putNull(UnknownPropertyContract.UNKNOWN_PROPERTIES) }, contact) assertNull(contact.unknownProperties) } @@ -26,7 +26,7 @@ class UnknownPropertiesHandlerTest { fun testUnknownProperties_Values() { val contact = Contact() UnknownPropertiesHandler.handle(ContentValues().apply { - put(UnknownProperties.UNKNOWN_PROPERTIES, "X-TEST:12345") + put(UnknownPropertyContract.UNKNOWN_PROPERTIES, "X-TEST:12345") }, contact) assertEquals("X-TEST:12345", contact.unknownProperties) } From 63618225144889a9459f32c4d803b1b11093bd1b Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:15:21 +0200 Subject: [PATCH 4/9] Minor changes, revert mistaken copyright changes --- .../synctools/storage/contacts/AndroidContact.kt | 16 +++++++++------- .../builder/UnknownPropertiesBuilderTest.kt | 4 +++- .../handler/UnknownPropertiesHandlerTest.kt | 5 ++++- .../synctools/util/SensitiveStringTest.kt | 4 +++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index 6b32f6b4..262b565c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.storage.contacts @@ -48,7 +50,10 @@ open class AndroidContact( var eTag: String? = null - protected val rawContactHandler: RawContactHandler by lazy { + val cachedGroupMemberships = HashSet() + val groupMemberships = HashSet() + + private val rawContactHandler: RawContactHandler by lazy { RawContactHandler(addressBook.provider!!).apply { registerHandler(CachedGroupMembershipHandler(this@AndroidContact, addressBook.groupMethod)) registerHandler(GroupMembershipHandler(this@AndroidContact, addressBook.groupMethod)) @@ -56,16 +61,13 @@ open class AndroidContact( } } - protected val rawContactBuilder: RawContactBuilder by lazy { + private val rawContactBuilder: RawContactBuilder by lazy { RawContactBuilder().apply { registerBuilderFactory(GroupMembershipBuilder.Factory(addressBook, addressBook.groupMethod)) registerBuilderFactory(UnknownPropertiesBuilder.Factory) } } - val cachedGroupMemberships = HashSet() - val groupMemberships = HashSet() - /** * Creates a new instance, initialized with some metadata. Usually used to insert a contact to an address book. @@ -232,7 +234,7 @@ open class AndroidContact( } - // group membership helpers + // group membership management fun addToGroup(batch: ContactsBatchOperation, groupID: Long) { batch += BatchOperation.CpoBuilder diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt index cc532804..81112ba5 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.builder diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt index 87ffee8c..5c7d7434 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt @@ -1,10 +1,13 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.handler import android.content.ContentValues +import at.bitfire.synctools.mapping.calendar.handler.UnknownPropertiesHandler import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import org.junit.Assert.assertEquals diff --git a/lib/src/test/kotlin/at/bitfire/synctools/util/SensitiveStringTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/util/SensitiveStringTest.kt index d66e2249..18b93a8d 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/util/SensitiveStringTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/util/SensitiveStringTest.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.util From 9897a593fb08e02ab6f5ade110c17595968c3bbd Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:16:20 +0200 Subject: [PATCH 5/9] Update copyright --- .../mapping/contacts/builder/GroupMembershipBuilderTest.kt | 4 +++- .../contacts/handler/CachedGroupMembershipHandlerTest.kt | 4 +++- .../mapping/contacts/handler/GroupMembershipHandlerTest.kt | 4 +++- .../bitfire/synctools/mapping/contacts/RawContactBuilder.kt | 4 +++- .../bitfire/synctools/mapping/contacts/RawContactHandler.kt | 4 +++- .../mapping/contacts/builder/GroupMembershipBuilder.kt | 4 +++- .../mapping/contacts/builder/UnknownPropertiesBuilder.kt | 4 +++- .../mapping/contacts/handler/CachedGroupMembershipHandler.kt | 4 +++- .../mapping/contacts/handler/GroupMembershipHandler.kt | 4 +++- .../mapping/contacts/handler/UnknownPropertiesHandler.kt | 4 +++- .../bitfire/synctools/storage/contacts/AndroidAddressBook.kt | 4 +++- .../storage/contacts/CachedGroupMembershipContract.kt | 4 +++- .../synctools/storage/contacts/UnknownPropertyContract.kt | 4 +++- .../main/kotlin/at/bitfire/synctools/util/SensitiveString.kt | 4 +++- 14 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt index aa5260b3..d1dd5e28 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilderTest.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.builder diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt index 5bdb84e5..57f9c881 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandlerTest.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.handler diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt index 0bd7fdd1..4f04195a 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandlerTest.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.handler diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt index 538568f2..e141894d 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt index 177e7aed..2313faf6 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt index 6294d8b9..17118da7 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/GroupMembershipBuilder.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.builder diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt index 4186d91a..4e175242 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilder.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.builder diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt index 69e7f962..75017cb8 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.handler diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt index 7e2dcce0..a4be080e 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.handler diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt index 80beabc7..0138a5ac 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandler.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.mapping.contacts.handler diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt index da784e0c..f21b904e 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidAddressBook.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.storage.contacts diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt index 9bf75779..06940fee 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/CachedGroupMembershipContract.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.storage.contacts diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt index 1cdd78f6..e33eadaf 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.storage.contacts diff --git a/lib/src/main/kotlin/at/bitfire/synctools/util/SensitiveString.kt b/lib/src/main/kotlin/at/bitfire/synctools/util/SensitiveString.kt index 7e34fcfd..9917d3cb 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/util/SensitiveString.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/util/SensitiveString.kt @@ -1,5 +1,7 @@ /* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later */ package at.bitfire.synctools.util From aa19446bbd490e1376afbbcc223b3d7a2b4f21a3 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:26:44 +0200 Subject: [PATCH 6/9] Fix test --- .../mapping/contacts/handler/UnknownPropertiesHandlerTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt index 5c7d7434..74000701 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt @@ -7,7 +7,6 @@ package at.bitfire.synctools.mapping.contacts.handler import android.content.ContentValues -import at.bitfire.synctools.mapping.calendar.handler.UnknownPropertiesHandler import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import org.junit.Assert.assertEquals From 1e8b0cc62de8609adf10272db7e85ccc9e9ff15c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:35:34 +0200 Subject: [PATCH 7/9] Minor changes --- .../handler/CachedGroupMembershipHandler.kt | 15 +++++++++------ .../contacts/handler/GroupMembershipHandler.kt | 7 +++++-- .../synctools/storage/contacts/AndroidContact.kt | 4 ++-- .../storage/contacts/UnknownPropertyContract.kt | 3 +-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt index 75017cb8..1bd2f423 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/CachedGroupMembershipHandler.kt @@ -11,19 +11,22 @@ import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.AndroidContact import at.bitfire.synctools.storage.contacts.CachedGroupMembershipContract import at.bitfire.synctools.vcard.GroupMethod -import java.util.logging.Logger -class CachedGroupMembershipHandler(val androidContact: AndroidContact, val groupMethod: GroupMethod) : DataRowHandler() { +class CachedGroupMembershipHandler( + val androidContact: AndroidContact, + val groupMethod: GroupMethod +) : DataRowHandler() { override fun forMimeType() = CachedGroupMembershipContract.CONTENT_ITEM_TYPE override fun handle(values: ContentValues, contact: Contact) { super.handle(values, contact) - if (groupMethod == GroupMethod.GROUP_VCARDS) - androidContact.cachedGroupMemberships += values.getAsLong(CachedGroupMembershipContract.GROUP_ID) - else - Logger.getGlobal().warning("Ignoring cached group membership for group method CATEGORIES") + if (groupMethod == GroupMethod.GROUP_VCARDS) { + val groupId = values.getAsLong(CachedGroupMembershipContract.GROUP_ID) ?: return + androidContact.cachedGroupMemberships += groupId + } else + logger.warning("Ignoring cached group membership for group method CATEGORIES") } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt index a4be080e..f2396ddf 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/handler/GroupMembershipHandler.kt @@ -14,14 +14,17 @@ import at.bitfire.synctools.util.Utils.trimToNull import at.bitfire.synctools.vcard.GroupMethod import java.io.FileNotFoundException -class GroupMembershipHandler(val androidContact: AndroidContact, val groupMethod: GroupMethod) : DataRowHandler() { +class GroupMembershipHandler( + val androidContact: AndroidContact, + val groupMethod: GroupMethod +) : DataRowHandler() { override fun forMimeType() = GroupMembership.CONTENT_ITEM_TYPE override fun handle(values: ContentValues, contact: Contact) { super.handle(values, contact) - val groupId = values.getAsLong(GroupMembership.GROUP_ROW_ID) + val groupId = values.getAsLong(GroupMembership.GROUP_ROW_ID) ?: return androidContact.groupMemberships += groupId if (groupMethod == GroupMethod.CATEGORIES) { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index 262b565c..3c0dc7f9 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -240,7 +240,7 @@ open class AndroidContact( batch += BatchOperation.CpoBuilder .newInsert(dataSyncURI()) .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) - .withValue(GroupMembership.RAW_CONTACT_ID, id) + .withValue(GroupMembership.RAW_CONTACT_ID, id!!) .withValue(GroupMembership.GROUP_ROW_ID, groupID) groupMemberships += groupID @@ -257,7 +257,7 @@ open class AndroidContact( .newDelete(dataSyncURI()) .withSelection( "${Data.RAW_CONTACT_ID}=? AND ${Data.MIMETYPE} IN (?,?)", - arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembershipContract.CONTENT_ITEM_TYPE) + arrayOf(id!!.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembershipContract.CONTENT_ITEM_TYPE) ) groupMemberships.clear() cachedGroupMemberships.clear() diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt index e33eadaf..50b00da3 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/UnknownPropertyContract.kt @@ -7,14 +7,13 @@ package at.bitfire.synctools.storage.contacts import android.provider.ContactsContract.RawContacts -import at.bitfire.synctools.storage.contacts.UnknownPropertyContract.MIMETYPE object UnknownPropertyContract { /** Column name for the MIME type of the data row. Type: [String] */ const val MIMETYPE = RawContacts.Data.MIMETYPE - /** MIME type of unknown-property data rows. Stored in [MIMETYPE]. */ + /** MIME type of unknown-property data rows. Stored in [at.bitfire.synctools.storage.contacts.UnknownPropertyContract.MIMETYPE]. */ const val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties" /** Column name for the ID of the raw contact this row belongs to. Type: [Long] */ From 37705746ee4a483a347ead1c4c3f0932bdbe6b1b Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:42:51 +0200 Subject: [PATCH 8/9] Simplify handler/builder creation --- .../mapping/contacts/RawContactBuilder.kt | 14 +++++++------- .../mapping/contacts/RawContactHandler.kt | 13 +++++++++---- .../synctools/storage/contacts/AndroidContact.kt | 16 ++-------------- .../builder/UnknownPropertiesBuilderTest.kt | 3 +++ .../handler/UnknownPropertiesHandlerTest.kt | 3 +++ 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt index e141894d..d45caa90 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactBuilder.kt @@ -7,9 +7,9 @@ package at.bitfire.synctools.mapping.contacts import android.net.Uri -import at.bitfire.synctools.mapping.contacts.builder.DataRowBuilder import at.bitfire.synctools.mapping.contacts.builder.EmailBuilder import at.bitfire.synctools.mapping.contacts.builder.EventBuilder +import at.bitfire.synctools.mapping.contacts.builder.GroupMembershipBuilder import at.bitfire.synctools.mapping.contacts.builder.ImBuilder import at.bitfire.synctools.mapping.contacts.builder.NicknameBuilder import at.bitfire.synctools.mapping.contacts.builder.NoteBuilder @@ -20,14 +20,17 @@ import at.bitfire.synctools.mapping.contacts.builder.RelationBuilder import at.bitfire.synctools.mapping.contacts.builder.SipAddressBuilder import at.bitfire.synctools.mapping.contacts.builder.StructuredNameBuilder import at.bitfire.synctools.mapping.contacts.builder.StructuredPostalBuilder +import at.bitfire.synctools.mapping.contacts.builder.UnknownPropertiesBuilder import at.bitfire.synctools.mapping.contacts.builder.WebsiteBuilder +import at.bitfire.synctools.storage.contacts.AndroidAddressBook import at.bitfire.synctools.storage.contacts.ContactsBatchOperation -class RawContactBuilder { +class RawContactBuilder(addressBook: AndroidAddressBook<*, *>) { - private val dataRowBuilderFactories = mutableListOf>( + private val dataRowBuilderFactories = mutableListOf( EmailBuilder.Factory, EventBuilder.Factory, + GroupMembershipBuilder.Factory(addressBook, addressBook.groupMethod), ImBuilder.Factory, NicknameBuilder.Factory, NoteBuilder.Factory, @@ -38,13 +41,10 @@ class RawContactBuilder { SipAddressBuilder.Factory, StructuredNameBuilder.Factory, StructuredPostalBuilder.Factory, + UnknownPropertiesBuilder.Factory, WebsiteBuilder.Factory ) - internal fun registerBuilderFactory(factory: DataRowBuilder.Factory<*>) { - dataRowBuilderFactories += factory - } - fun insertDataRows(dataRowUri: Uri, rawContactId: Long?, contact: Contact, batch: ContactsBatchOperation, readOnly: Boolean) { for (factory in dataRowBuilderFactories) { val builder = factory.newInstance(dataRowUri, rawContactId, contact, readOnly) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt index 2313faf6..2bcb1178 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/contacts/RawContactHandler.kt @@ -6,12 +6,13 @@ package at.bitfire.synctools.mapping.contacts -import android.content.ContentProviderClient import android.content.ContentValues import android.provider.ContactsContract.RawContacts +import at.bitfire.synctools.mapping.contacts.handler.CachedGroupMembershipHandler import at.bitfire.synctools.mapping.contacts.handler.DataRowHandler import at.bitfire.synctools.mapping.contacts.handler.EmailHandler import at.bitfire.synctools.mapping.contacts.handler.EventHandler +import at.bitfire.synctools.mapping.contacts.handler.GroupMembershipHandler import at.bitfire.synctools.mapping.contacts.handler.ImHandler import at.bitfire.synctools.mapping.contacts.handler.NicknameHandler import at.bitfire.synctools.mapping.contacts.handler.NoteHandler @@ -22,29 +23,33 @@ import at.bitfire.synctools.mapping.contacts.handler.RelationHandler import at.bitfire.synctools.mapping.contacts.handler.SipAddressHandler import at.bitfire.synctools.mapping.contacts.handler.StructuredNameHandler import at.bitfire.synctools.mapping.contacts.handler.StructuredPostalHandler +import at.bitfire.synctools.mapping.contacts.handler.UnknownPropertiesHandler import at.bitfire.synctools.mapping.contacts.handler.WebsiteHandler import at.bitfire.synctools.storage.contacts.AndroidContact import java.util.logging.Level import java.util.logging.Logger class RawContactHandler( - provider: ContentProviderClient + androidContact: AndroidContact ) { private val dataRowHandlers = mutableMapOf>() private val defaultDataRowHandlers = arrayOf( + CachedGroupMembershipHandler(androidContact, androidContact.addressBook.groupMethod), EmailHandler, EventHandler, + GroupMembershipHandler(androidContact, androidContact.addressBook.groupMethod), ImHandler, NicknameHandler, NoteHandler, OrganizationHandler, PhoneHandler, - PhotoHandler(provider), + PhotoHandler(androidContact.addressBook.provider!!), RelationHandler, SipAddressHandler, StructuredNameHandler, StructuredPostalHandler, + UnknownPropertiesHandler, WebsiteHandler ) @@ -53,7 +58,7 @@ class RawContactHandler( registerHandler(handler) } - internal fun registerHandler(handler: DataRowHandler) { + private fun registerHandler(handler: DataRowHandler) { val mimeType = handler.forMimeType() val handlers = dataRowHandlers[mimeType] ?: run { val newList = mutableListOf() diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index 3c0dc7f9..e6ea106e 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -20,12 +20,7 @@ import androidx.annotation.CallSuper import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.mapping.contacts.RawContactBuilder import at.bitfire.synctools.mapping.contacts.RawContactHandler -import at.bitfire.synctools.mapping.contacts.builder.GroupMembershipBuilder import at.bitfire.synctools.mapping.contacts.builder.PhotoBuilder -import at.bitfire.synctools.mapping.contacts.builder.UnknownPropertiesBuilder -import at.bitfire.synctools.mapping.contacts.handler.CachedGroupMembershipHandler -import at.bitfire.synctools.mapping.contacts.handler.GroupMembershipHandler -import at.bitfire.synctools.mapping.contacts.handler.UnknownPropertiesHandler import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.LocalStorageException import java.io.FileNotFoundException @@ -54,18 +49,11 @@ open class AndroidContact( val groupMemberships = HashSet() private val rawContactHandler: RawContactHandler by lazy { - RawContactHandler(addressBook.provider!!).apply { - registerHandler(CachedGroupMembershipHandler(this@AndroidContact, addressBook.groupMethod)) - registerHandler(GroupMembershipHandler(this@AndroidContact, addressBook.groupMethod)) - registerHandler(UnknownPropertiesHandler) - } + RawContactHandler(this) } private val rawContactBuilder: RawContactBuilder by lazy { - RawContactBuilder().apply { - registerBuilderFactory(GroupMembershipBuilder.Factory(addressBook, addressBook.groupMethod)) - registerBuilderFactory(UnknownPropertiesBuilder.Factory) - } + RawContactBuilder(addressBook) } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt index 81112ba5..da0f1f88 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/builder/UnknownPropertiesBuilderTest.kt @@ -11,7 +11,10 @@ import at.bitfire.synctools.mapping.contacts.Contact import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class UnknownPropertiesBuilderTest { @Test diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt index 74000701..8405d298 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/contacts/handler/UnknownPropertiesHandlerTest.kt @@ -12,7 +12,10 @@ import at.bitfire.synctools.storage.contacts.UnknownPropertyContract import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class UnknownPropertiesHandlerTest { @Test From b3192a5c44057a6075c43f985489f350ad133311 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 May 2026 16:54:18 +0200 Subject: [PATCH 9/9] Add KDoc --- .../synctools/storage/contacts/AndroidContact.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt index e6ea106e..67201778 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/contacts/AndroidContact.kt @@ -45,7 +45,19 @@ open class AndroidContact( var eTag: String? = null + /** + * IDs of groups this contact's cached group membership rows belong to. + * Only filled after [getContact] has been called. + * + * Used to detect which groups have become dirty when a contact's memberships change. + * See [CachedGroupMembershipContract] for details. + */ val cachedGroupMemberships = HashSet() + + /** + * IDs of groups this contact is currently a member of. + * Only filled after [getContact] has been called. + */ val groupMemberships = HashSet() private val rawContactHandler: RawContactHandler by lazy {