You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt

282 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.resource
import android.content.ContentProviderOperation
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.RawContacts.Data
import android.text.TextUtils
import at.bitfire.vcard4android.AndroidAddressBook
import at.bitfire.vcard4android.AndroidContact
import at.bitfire.vcard4android.AndroidContactFactory
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.CachedGroupMembership
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.UnknownProperties
import ezvcard.Ezvcard
import ezvcard.VCardVersion
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
class LocalContact : AndroidContact, LocalAddress {
companion object {
init {
Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION
}
internal const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
internal val HASH_HACK = Build.VERSION_CODES.N <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < Build.VERSION_CODES.O
}
private var saveAsDirty = false // When true, the resource will be saved as dirty
internal val cachedGroupMemberships: MutableSet<Long> = HashSet()
internal val groupMemberships: MutableSet<Long> = HashSet()
override// The same now
val uuid: String?
get() = contact?.uid
override val isLocalOnly: Boolean
get() = TextUtils.isEmpty(eTag)
override val content: String
get() {
val contact: Contact
contact = this.contact!!
Logger.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact)
val os = ByteArrayOutputStream()
contact.write(VCardVersion.V4_0, GROUP_VCARDS, os)
return os.toString()
}
constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
: super(addressBook, values) {}
constructor(addressBook: AndroidAddressBook<LocalContact, *>, contact: Contact, uuid: String?, eTag: String?)
: super(addressBook, contact, uuid, eTag) {}
fun resetDirty() {
val values = ContentValues(1)
values.put(ContactsContract.RawContacts.DIRTY, 0)
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
}
override fun resetDeleted() {
val values = ContentValues(1)
values.put(ContactsContract.RawContacts.DELETED, 0)
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
}
override fun clearDirty(eTag: String?) {
val values = ContentValues(3)
if (eTag != null) {
values.put(AndroidContact.COLUMN_ETAG, eTag)
}
values.put(ContactsContract.RawContacts.DIRTY, 0)
if (LocalContact.HASH_HACK) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val hashCode = dataHashCode()
values.put(COLUMN_HASHCODE, hashCode)
Logger.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
}
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
this.eTag = eTag
}
override fun legacyPrepareForUpload(fileName_: String?) {
val uid = UUID.randomUUID().toString()
val values = ContentValues(2)
val fileName = fileName_ ?: uid
values.put(AndroidContact.COLUMN_FILENAME, fileName)
values.put(AndroidContact.COLUMN_UID, uid)
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
this.fileName = fileName
}
override fun prepareForUpload(fileName: String, uid: String) {
val values = ContentValues(2)
values.put(AndroidContact.COLUMN_FILENAME, fileName)
values.put(AndroidContact.COLUMN_UID, uid)
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
contact?.uid = uid
this.fileName = fileName
}
override fun populateData(mimeType: String, row: ContentValues) {
when (mimeType) {
CachedGroupMembership.CONTENT_ITEM_TYPE -> cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
GroupMembership.CONTENT_ITEM_TYPE -> groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID))
UnknownProperties.CONTENT_ITEM_TYPE -> contact?.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
}
}
override fun insertDataRows(batch: BatchOperation) {
super.insertDataRows(batch)
if (contact?.unknownProperties != null) {
var builder = BatchOperation.CpoBuilder.newInsert(dataSyncURI())
if (id == null) {
builder = builder.withValue(UnknownProperties.RAW_CONTACT_ID, 0)
} else {
builder = builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
}
builder.withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact?.unknownProperties)
batch.enqueue(builder)
}
}
fun updateAsDirty(contact: Contact): Uri {
saveAsDirty = true
return this.update(contact)
}
fun createAsDirty(): Uri {
saveAsDirty = true
return this.add()
}
override fun buildContact(builder: BatchOperation.CpoBuilder, update: Boolean) {
super.buildContact(builder, update)
builder.withValue(ContactsContract.RawContacts.DIRTY, if (saveAsDirty) 1 else 0)
}
/**
* Calculates a hash code from the contact's data (VCard) and group memberships.
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory
* @return hash code of contact data (including group memberships)
*/
internal fun dataHashCode(): Int {
if (!LocalContact.HASH_HACK)
throw IllegalStateException("dataHashCode() should not be called on Android != 7")
// reset contact so that getContact() reads from database
contact = null
// groupMemberships is filled by getContact()
val dataHash = contact!!.hashCode()
val groupHash = groupMemberships.hashCode()
Logger.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash")
return dataHash xor groupHash
}
fun updateHashCode(batch: BatchOperation?) {
if (!LocalContact.HASH_HACK)
throw IllegalStateException("updateHashCode() should not be called on Android != 7")
val values = ContentValues(1)
val hashCode = dataHashCode()
Logger.log.fine("Storing contact hash = $hashCode")
values.put(COLUMN_HASHCODE, hashCode)
if (batch == null)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
else {
val builder = BatchOperation.CpoBuilder
.newUpdate(rawContactSyncURI())
.withValue(COLUMN_HASHCODE, hashCode)
batch.enqueue(builder)
}
}
fun getLastHashCode(): Int {
if (!LocalContact.HASH_HACK)
throw IllegalStateException("getLastHashCode() should not be called on Android != 7")
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
if (c.moveToNext() && !c.isNull(0))
return c.getInt(0)
}
return 0
}
fun addToGroup(batch: BatchOperation, groupID: Long) {
batch.enqueue(BatchOperation.CpoBuilder.newInsert(dataSyncURI())
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
.withValue(GroupMembership.RAW_CONTACT_ID, id)
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
)
groupMemberships.add(groupID)
batch.enqueue(BatchOperation.CpoBuilder.newInsert(dataSyncURI())
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
.withValue(CachedGroupMembership.GROUP_ID, groupID)
)
cachedGroupMemberships.add(groupID)
}
fun removeGroupMemberships(batch: BatchOperation) {
batch.enqueue(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 DAVdroid 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 ContactsStorageException on contact provider errors
* @throws FileNotFoundException if the current contact can't be found
*/
fun getCachedGroupMemberships(): Set<Long> {
contact
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 ContactsStorageException on contact provider errors
* @throws FileNotFoundException if the current contact can't be found
*/
fun getGroupMemberships(): Set<Long> {
contact
return groupMemberships
}
// factory
object Factory: AndroidContactFactory<LocalContact> {
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
LocalContact(addressBook, values)
}
}