Skip to content
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
Expand Up @@ -9,20 +9,18 @@ package at.bitfire.synctools.storage.contacts
import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentValues
import android.provider.ContactsContract
import android.util.Base64
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.synctools.mapping.contacts.Contact
import at.bitfire.synctools.mapping.contacts.ContactReader
import at.bitfire.synctools.mapping.contacts.ContactWriter
import at.bitfire.synctools.mapping.contacts.LabeledProperty
import at.bitfire.synctools.storage.LocalStorageException
import at.bitfire.synctools.vcard.VCardParser
import at.bitfire.synctools.vcard.property.XAbDate
import ezvcard.VCardVersion
import ezvcard.property.Address
import ezvcard.property.Birthday
import ezvcard.property.Email
import ezvcard.util.PartialDate
Expand All @@ -31,15 +29,12 @@ import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import java.io.FileNotFoundException
import java.io.StringReader
import java.io.StringWriter
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset

class AndroidContactTest {

Expand Down Expand Up @@ -135,29 +130,6 @@ class AndroidContactTest {
}
}

@Test
fun testBirthdayWithOffset() = runTest {
val vCard = "BEGIN:VCARD\r\n" +
"VERSION:3.0\n\n" +
"N:Doe;John;;;\n\n" +
"FN:John Doe\n\n" +
"BDAY:20010415T000000+0200\n\n" +
"END:VCARD\n\n"
val contacts = parseVCards(vCard)

assertEquals(1, contacts.size)
contacts.first().birthDay.let { birthday ->
assertNotNull(birthday)

val date = birthday?.date
assertNotNull(date)

assertEquals(
OffsetDateTime.of(2001, 4, 15, 0, 0, 0, 0, ZoneOffset.ofHours(2)), date
)
}
}

@Test
@MediumTest
fun testLargeTransactionManyRows() {
Expand Down Expand Up @@ -191,31 +163,13 @@ class AndroidContactTest {
contact.add()
}

@Test
fun testAddressCaretEncoding() {
val address = Address()
address.label = "My \"Label\"\nLine 2"
address.streetAddress = "Street \"Address\""
val contact = Contact()
contact.addresses += LabeledProperty(address)

/* label-param = "LABEL=" param-value
* param-values must not contain DQUOTE and should be encoded as defined in RFC 6868
*
* ADR-value = ADR-component-pobox ";" ADR-component-ext ";"
* ADR-component-street ";" ADR-component-locality ";"
* ADR-component-region ";" ADR-component-code ";"
* ADR-component-country
* ADR-component-pobox = list-component
*
* list-component = component *("," component)
* component = "\\" / "\," / "\;" / "\n" / WSP / NON-ASCII / %x21-2B / %x2D-3A / %x3C-5B / %x5D-7E
*
* So, ADR value components may contain DQUOTE (0x22) and don't have to be encoded as defined in RFC 6868 */

val writer = StringWriter()
ContactWriter(contact, VCardVersion.V4_0, testProductId).writeVCard(writer)
assertTrue(writer.toString().contains("ADR;LABEL=My ^'Label^'\\nLine 2:;;Street \"Address\";;;;"))
@Test(expected = FileNotFoundException::class)
fun testGetContactNotFound() {
val values = ContentValues()
values.put(ContactsContract.RawContacts._ID, Long.MAX_VALUE)
values.put(AndroidContact.COLUMN_FILENAME, "nonexistent.vcf")
values.put(AndroidContact.COLUMN_ETAG, "etag")
AndroidContact(addressBook, values).getContact()
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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

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.ImBuilder
import at.bitfire.synctools.mapping.contacts.builder.NicknameBuilder
import at.bitfire.synctools.mapping.contacts.builder.NoteBuilder
import at.bitfire.synctools.mapping.contacts.builder.OrganizationBuilder
import at.bitfire.synctools.mapping.contacts.builder.PhoneBuilder
import at.bitfire.synctools.mapping.contacts.builder.PhotoBuilder
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.WebsiteBuilder
import at.bitfire.synctools.storage.contacts.ContactsBatchOperation

class RawContactBuilder {

private val dataRowBuilderFactories = mutableListOf<DataRowBuilder.Factory<*>>(
EmailBuilder.Factory,
EventBuilder.Factory,
ImBuilder.Factory,
NicknameBuilder.Factory,
NoteBuilder.Factory,
OrganizationBuilder.Factory,
PhoneBuilder.Factory,
PhotoBuilder.Factory,
RelationBuilder.Factory,
SipAddressBuilder.Factory,
StructuredNameBuilder.Factory,
StructuredPostalBuilder.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)
batch += builder.build()
}
}

fun builderMimeTypes(): Set<String> {
val mimeTypes = mutableSetOf<String>()
for (factory in dataRowBuilderFactories)
mimeTypes += factory.mimeType()
return mimeTypes
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,7 @@ package at.bitfire.synctools.mapping.contacts

import android.content.ContentProviderClient
import android.content.ContentValues
import android.net.Uri
import android.provider.ContactsContract.RawContacts
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.ImBuilder
import at.bitfire.synctools.mapping.contacts.builder.NicknameBuilder
import at.bitfire.synctools.mapping.contacts.builder.NoteBuilder
import at.bitfire.synctools.mapping.contacts.builder.OrganizationBuilder
import at.bitfire.synctools.mapping.contacts.builder.PhoneBuilder
import at.bitfire.synctools.mapping.contacts.builder.PhotoBuilder
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.WebsiteBuilder
import at.bitfire.synctools.mapping.contacts.handler.DataRowHandler
import at.bitfire.synctools.mapping.contacts.handler.EmailHandler
import at.bitfire.synctools.mapping.contacts.handler.EventHandler
Expand All @@ -38,12 +23,12 @@ 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.WebsiteHandler
import at.bitfire.synctools.storage.contacts.ContactsBatchOperation
import at.bitfire.synctools.storage.contacts.AndroidContact
import java.util.logging.Level
import java.util.logging.Logger

class ContactProcessor(
val provider: ContentProviderClient?
class RawContactHandler(
provider: ContentProviderClient
) {

private val dataRowHandlers = mutableMapOf<String, MutableList<DataRowHandler>>()
Expand All @@ -63,29 +48,11 @@ class ContactProcessor(
WebsiteHandler
)

private val dataRowBuilderFactories = mutableListOf<DataRowBuilder.Factory<*>>(
EmailBuilder.Factory,
EventBuilder.Factory,
ImBuilder.Factory,
NicknameBuilder.Factory,
NoteBuilder.Factory,
OrganizationBuilder.Factory,
PhoneBuilder.Factory,
PhotoBuilder.Factory,
RelationBuilder.Factory,
SipAddressBuilder.Factory,
StructuredNameBuilder.Factory,
StructuredPostalBuilder.Factory,
WebsiteBuilder.Factory
)


init {
for (handler in defaultDataRowHandlers)
registerHandler(handler)
}


fun registerHandler(handler: DataRowHandler) {
val mimeType = handler.forMimeType()
val handlers = dataRowHandlers[mimeType] ?: run {
Expand All @@ -97,13 +64,8 @@ class ContactProcessor(
handlers += handler
}

fun registerBuilderFactory(factory: DataRowBuilder.Factory<*>) {
dataRowBuilderFactories += factory
}


fun handleRawContact(values: ContentValues, contact: Contact) {
contact.uid = values.getAsString(at.bitfire.synctools.storage.contacts.AndroidContact.COLUMN_UID)
contact.uid = values.getAsString(AndroidContact.COLUMN_UID)
}

fun handleDataRow(values: ContentValues, contact: Contact) {
Expand All @@ -119,20 +81,4 @@ class ContactProcessor(
}
}


fun insertDataRows(dataRowUri: Uri, rawContactId: Long?, contact: Contact, batch: ContactsBatchOperation, readOnly: Boolean) {
for (factory in dataRowBuilderFactories) {
val builder = factory.newInstance(dataRowUri, rawContactId, contact, readOnly)
batch += builder.build()
}
}


fun builderMimeTypes(): Set<String> {
val mimeTypes = mutableSetOf<String>()
for (factory in dataRowBuilderFactories)
mimeTypes += factory.mimeType()
return mimeTypes
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data
import androidx.annotation.CallSuper
import at.bitfire.synctools.mapping.contacts.Contact
import at.bitfire.synctools.mapping.contacts.ContactProcessor
import at.bitfire.synctools.mapping.contacts.RawContactBuilder
import at.bitfire.synctools.mapping.contacts.RawContactHandler
import at.bitfire.synctools.mapping.contacts.builder.PhotoBuilder
import at.bitfire.synctools.storage.BatchOperation
import at.bitfire.synctools.storage.LocalStorageException
import java.io.FileNotFoundException

open class AndroidContact(
open val addressBook: AndroidAddressBook<out AndroidContact, out AndroidGroup>
open val addressBook: AndroidAddressBook<out AndroidContact, out AndroidGroup>,
protected open val rawContactBuilder: RawContactBuilder = RawContactBuilder(),
protected open val rawContactHandler: RawContactHandler = RawContactHandler(addressBook.provider!!)
) {

companion object {
Expand All @@ -43,8 +46,6 @@ open class AndroidContact(

var eTag: String? = null

val processor = ContactProcessor(addressBook.provider)


/**
* Creates a new instance, initialized with some metadata. Usually used to insert a contact to an address book.
Expand All @@ -69,7 +70,7 @@ open class AndroidContact(
* Cached copy of the [Contact]. If this is null, [getContact] must generate the [Contact]
* from the database and then set this property.
*/
protected var _contact: Contact? = null
private var cachedContact: Contact? = null
Comment thread
rfc2822 marked this conversation as resolved.
Comment thread
rfc2822 marked this conversation as resolved.

/**
* Fetches contact data from the contacts provider.
Expand All @@ -79,7 +80,8 @@ open class AndroidContact(
* @throws RemoteException on contact provider errors
*/
fun getContact(): Contact {
_contact?.let { return it }
// use cached version if available
cachedContact?.let { return it }

val id = requireNotNull(id)
var iter: EntityIterator? = null
Expand All @@ -90,16 +92,16 @@ open class AndroidContact(

if (iter.hasNext()) {
Comment thread
rfc2822 marked this conversation as resolved.
val contact = Contact()
_contact = contact

// process raw contact itself
val e = iter.next()
processor.handleRawContact(e.entityValues, contact)
rawContactHandler.handleRawContact(e.entityValues, contact)

// process data rows of raw contact
for (subValue in e.subValues)
processor.handleDataRow(subValue.values, contact)
rawContactHandler.handleDataRow(subValue.values, contact)

cachedContact = contact
return contact

} else
Expand All @@ -111,8 +113,8 @@ open class AndroidContact(
}
}

fun setContact(newContact: Contact) {
_contact = newContact
fun setContact(newContact: Contact?) {
cachedContact = newContact
}


Expand Down Expand Up @@ -152,7 +154,7 @@ open class AndroidContact(
// - We don't delete group memberships because they're managed separately.
// - We'll only delete rows we have inserted so that unknown rows like
// vnd.android.cursor.item/important_people (= contact is in Samsung "edge panel") remain untouched.
val typesToRemove = processor.builderMimeTypes()
val typesToRemove = rawContactBuilder.builderMimeTypes()
val sqlTypesToRemove = typesToRemove.joinToString(",") { mimeType ->
DatabaseUtils.sqlEscapeString(mimeType)
}
Expand Down Expand Up @@ -206,7 +208,7 @@ open class AndroidContact(
*/
protected fun insertDataRows(batch: ContactsBatchOperation) {
val contact = getContact()
processor.insertDataRows(dataSyncURI(), id, contact, batch, addressBook.readOnly)
rawContactBuilder.insertDataRows(dataSyncURI(), id, contact, batch, addressBook.readOnly)
}


Expand All @@ -220,6 +222,6 @@ open class AndroidContact(
fun dataSyncURI() = addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)

override fun toString() =
"AndroidContact(id=$id, fileName=$fileName, eTag=$eTag, _contact=$_contact)"
"AndroidContact(id=$id, fileName=$fileName, eTag=$eTag, cachedContact=$cachedContact)"

}
Loading
Loading