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/LocalGroup.kt

281 lines
9.8 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.ContentProviderClient
import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.Parcel
import android.os.RemoteException
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts.Data
import android.text.TextUtils
import at.bitfire.vcard4android.*
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS
import com.etesync.syncadapter.log.Logger
import ezvcard.VCardVersion
import java.io.ByteArrayOutputStream
import java.util.*
import java.util.logging.Level
class LocalGroup : AndroidGroup, LocalAddress {
companion object {
/** marshalled list of member UIDs, as sent by server */
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
/**
* Processes all groups with non-null {@link #COLUMN_PENDING_MEMBERS}: the pending memberships
* are (if possible) applied, keeping cached memberships in sync.
* @param addressBook address book to take groups from
*/
fun applyPendingMemberships(addressBook: LocalAddressBook) {
addressBook.provider!!.query(
addressBook.groupsSyncUri(),
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
null
)?.use { cursor ->
val batch = BatchOperation(addressBook.provider as ContentProviderClient)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
// extract list of member UIDs
val members = LinkedList<String>()
val raw = cursor.getBlob(1)
val parcel = Parcel.obtain()
try {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
parcel.readStringList(members)
} finally {
parcel.recycle()
}
// insert memberships
val membersIds = members.map {uid ->
Constants.log.fine("Assigning member: $uid")
val contact = addressBook.findByUid(uid) as LocalContact?
if (contact != null) contact.id else null
}.filterNotNull()
val group = addressBook.findGroupById(id)
group.setMembers(batch, membersIds)
batch.commit()
}
}
}
}
private var saveAsDirty = false // When true, the resource will be saved as dirty
override val uuid: String?
get() = fileName
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()
}
override val isLocalOnly: Boolean
get() = TextUtils.isEmpty(eTag)
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
: super(addressBook, values) {}
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?)
: super(addressBook, contact, fileName, eTag) {}
override fun contentValues(): ContentValues {
val values = super.contentValues()
val members = Parcel.obtain()
try {
members.writeStringList(contact?.members)
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
} finally {
members.recycle()
}
if (saveAsDirty) {
values.put(Groups.DIRTY, true)
}
return values
}
override fun clearDirty(eTag: String?) {
val id = requireNotNull(id)
val values = ContentValues(2)
values.put(Groups.DIRTY, 0)
this.eTag = eTag
if (eTag != null) {
values.put(AndroidGroup.COLUMN_ETAG, eTag)
}
update(values)
// update cached group memberships
val batch = BatchOperation(addressBook.provider!!)
// delete cached group memberships
batch.enqueue(BatchOperation.CpoBuilder.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withSelection(
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE, id.toString())
)
)
// insert updated cached group memberships
for (member in getMembers())
batch.enqueue(BatchOperation.CpoBuilder.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, member)
.withValue(CachedGroupMembership.GROUP_ID, id)
)
batch.commit()
}
override fun legacyPrepareForUpload(fileName_: String?) {
val uid = UUID.randomUUID().toString()
val values = ContentValues(2)
val fileName = fileName_ ?: uid
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
values.put(AndroidGroup.COLUMN_UID, uid)
update(values)
this.fileName = fileName
}
override fun prepareForUpload(fileName: String, uid: String) {
val values = ContentValues(2)
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
values.put(AndroidGroup.COLUMN_UID, uid)
addressBook.provider?.update(groupSyncUri(), values, null, null)
contact?.uid = uid
this.fileName = fileName
}
override fun resetDeleted() {
val values = ContentValues(1)
values.put(Groups.DELETED, 0)
addressBook.provider!!.update(groupSyncUri(), values, null, null)
}
private fun setMembers(batch: BatchOperation, members: List<Long>) {
val id = id!!
val addressBook = this.addressBook as LocalAddressBook
Constants.log.fine("Assigning members to group $id")
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val changeContactIDs = HashSet<Long>()
// delete all memberships and cached memberships for this group
for (contact in addressBook.getByGroupMembership(id)) {
contact.removeGroupMemberships(batch)
changeContactIDs += contact.id!!
}
// insert memberships
for (memberId in members) {
Constants.log.fine("Assigning member: $memberId")
addressBook.findContactByID(memberId).let { member ->
member.addToGroup(batch, id)
changeContactIDs += member.id!!
} ?: Constants.log.warning("Group member not found: $memberId")
}
if (LocalContact.HASH_HACK)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
changeContactIDs
.map { addressBook.findContactByID(it) }
.forEach { it.updateHashCode(batch) }
// remove pending memberships
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
)
}
fun updateAsDirty(group: Contact, members: List<Long>): Uri {
saveAsDirty = true
val ret = this.update(group)
val batch = BatchOperation(addressBook.provider!!)
setMembers(batch, members)
batch.commit()
return ret
}
fun createAsDirty(members: List<Long>): Uri {
saveAsDirty = true
val ret = this.add()
val batch = BatchOperation(addressBook.provider!!)
setMembers(batch, members)
batch.commit()
return ret
}
// helpers
private fun groupSyncUri(): Uri {
val id = requireNotNull(id)
return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id)
}
/**
* Lists all members of this group.
* @return list of all members' raw contact IDs
* @throws RemoteException on contact provider errors
*/
internal fun getMembers(): List<Long> {
val id = requireNotNull(id)
val members = LinkedList<Long>()
addressBook.provider!!.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(Data.RAW_CONTACT_ID),
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
null
)?.use { cursor ->
while (cursor.moveToNext())
members += cursor.getLong(0)
}
return members
}
// factory
object Factory: AndroidGroupFactory<LocalGroup> {
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
LocalGroup(addressBook, values)
}
}