Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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

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()
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.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.CachedGroupMembershipContract
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(CachedGroupMembershipContract.GROUP_ID, 123456)
put(CachedGroupMembershipContract.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()
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.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.CachedGroupMembershipContract
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(CachedGroupMembershipContract.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(CachedGroupMembershipContract.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()
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DataRowBuilder.Factory<*>>(
private val dataRowBuilderFactories = mutableListOf(
EmailBuilder.Factory,
EventBuilder.Factory,
GroupMembershipBuilder.Factory(addressBook, addressBook.groupMethod),
ImBuilder.Factory,
NicknameBuilder.Factory,
Comment thread
rfc2822 marked this conversation as resolved.
NoteBuilder.Factory,
Expand All @@ -38,13 +41,10 @@ class RawContactBuilder {
SipAddressBuilder.Factory,
StructuredNameBuilder.Factory,
StructuredPostalBuilder.Factory,
UnknownPropertiesBuilder.Factory,
WebsiteBuilder.Factory
)

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, MutableList<DataRowHandler>>()
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
)

Expand All @@ -53,7 +58,7 @@ class RawContactHandler(
registerHandler(handler)
}

fun registerHandler(handler: DataRowHandler) {
private fun registerHandler(handler: DataRowHandler) {
val mimeType = handler.forMimeType()
val handlers = dataRowHandlers[mimeType] ?: run {
val newList = mutableListOf<DataRowHandler>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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

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<BatchOperation.CpoBuilder> {
val result = LinkedList<BatchOperation.CpoBuilder>()

if (groupMethod == GroupMethod.CATEGORIES)
for (category in contact.categories)
result += newDataRow().withValue(GroupMembership.GROUP_ROW_ID, addressBook.findOrCreateGroup(category))
else {
Comment thread
rfc2822 marked this conversation as resolved.
// 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<GroupMembershipBuilder> {
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)
}

}
Loading
Loading