|
|
|
@ -12,6 +12,7 @@ import android.content.ContentProviderOperation
|
|
|
|
|
import android.content.ContentUris
|
|
|
|
|
import android.content.ContentValues
|
|
|
|
|
import android.database.Cursor
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.os.Build
|
|
|
|
|
import android.os.Parcel
|
|
|
|
|
import android.os.RemoteException
|
|
|
|
@ -21,6 +22,7 @@ import android.provider.ContactsContract.Groups
|
|
|
|
|
import android.provider.ContactsContract.RawContacts
|
|
|
|
|
import android.provider.ContactsContract.RawContacts.Data
|
|
|
|
|
import android.text.TextUtils
|
|
|
|
|
import at.bitfire.vcard4android.*
|
|
|
|
|
|
|
|
|
|
import com.etesync.syncadapter.App
|
|
|
|
|
|
|
|
|
@ -34,27 +36,88 @@ import java.util.LinkedList
|
|
|
|
|
import java.util.UUID
|
|
|
|
|
import java.util.logging.Level
|
|
|
|
|
|
|
|
|
|
import at.bitfire.vcard4android.AndroidAddressBook
|
|
|
|
|
import at.bitfire.vcard4android.AndroidGroup
|
|
|
|
|
import at.bitfire.vcard4android.AndroidGroupFactory
|
|
|
|
|
import at.bitfire.vcard4android.BatchOperation
|
|
|
|
|
import at.bitfire.vcard4android.CachedGroupMembership
|
|
|
|
|
import at.bitfire.vcard4android.Contact
|
|
|
|
|
import at.bitfire.vcard4android.ContactsStorageException
|
|
|
|
|
import ezvcard.VCardVersion
|
|
|
|
|
|
|
|
|
|
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS
|
|
|
|
|
|
|
|
|
|
class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
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)
|
|
|
|
|
while (cursor.moveToNext()) {
|
|
|
|
|
val id = cursor.getLong(0)
|
|
|
|
|
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!!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
for (uid in members) {
|
|
|
|
|
Constants.log.fine("Assigning member: $uid")
|
|
|
|
|
addressBook.findContactByUID(uid)?.let { member ->
|
|
|
|
|
member.addToGroup(batch, id)
|
|
|
|
|
changeContactIDs += member.id!!
|
|
|
|
|
} ?: Constants.log.warning("Group member not found: $uid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
|
|
|
|
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
|
|
|
|
changeContactIDs
|
|
|
|
|
.map { addressBook.findContactByID(it) }
|
|
|
|
|
.forEach { it.updateHashCode(batch) }
|
|
|
|
|
|
|
|
|
|
override val uuid: String
|
|
|
|
|
get() = getFileName()
|
|
|
|
|
// remove pending memberships
|
|
|
|
|
batch.enqueue(BatchOperation.Operation(
|
|
|
|
|
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
|
|
|
|
.withValue(COLUMN_PENDING_MEMBERS, null)
|
|
|
|
|
.withYieldAllowed(true)
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
batch.commit()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override val uuid: String?
|
|
|
|
|
get() = fileName
|
|
|
|
|
|
|
|
|
|
override val content: String
|
|
|
|
|
@Throws(IOException::class, ContactsStorageException::class)
|
|
|
|
|
get() {
|
|
|
|
|
val contact: Contact
|
|
|
|
|
contact = getContact()
|
|
|
|
|
contact = this.contact!!
|
|
|
|
|
|
|
|
|
|
App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact)
|
|
|
|
|
|
|
|
|
@ -65,42 +128,29 @@ class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override val isLocalOnly: Boolean
|
|
|
|
|
get() = TextUtils.isEmpty(getETag())
|
|
|
|
|
get() = TextUtils.isEmpty(eTag)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lists all members of this group.
|
|
|
|
|
* @return list of all members' raw contact IDs
|
|
|
|
|
* @throws ContactsStorageException on contact provider errors
|
|
|
|
|
*/
|
|
|
|
|
val members: LongArray
|
|
|
|
|
@Throws(ContactsStorageException::class)
|
|
|
|
|
get() {
|
|
|
|
|
assertID()
|
|
|
|
|
val members = LinkedList<Long>()
|
|
|
|
|
try {
|
|
|
|
|
val cursor = 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
|
|
|
|
|
)
|
|
|
|
|
while (cursor != null && cursor.moveToNext())
|
|
|
|
|
members.add(cursor.getLong(0))
|
|
|
|
|
cursor!!.close()
|
|
|
|
|
} catch (e: RemoteException) {
|
|
|
|
|
throw ContactsStorageException("Couldn't list group members", e)
|
|
|
|
|
}
|
|
|
|
|
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
|
|
|
|
|
: super(addressBook, values) {}
|
|
|
|
|
|
|
|
|
|
return ArrayUtils.toPrimitive(members.toTypedArray())
|
|
|
|
|
}
|
|
|
|
|
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
|
|
|
|
|
: super(addressBook, contact, fileName, eTag) {}
|
|
|
|
|
|
|
|
|
|
constructor(addressBook: AndroidAddressBook, id: Long, fileName: String?, eTag: String?) : super(addressBook, id, fileName, eTag) {}
|
|
|
|
|
override fun contentValues(): ContentValues {
|
|
|
|
|
val values = super.contentValues()
|
|
|
|
|
|
|
|
|
|
constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?) : super(addressBook, contact, fileName, eTag) {}
|
|
|
|
|
val members = Parcel.obtain()
|
|
|
|
|
try {
|
|
|
|
|
members.writeStringList(contact?.members)
|
|
|
|
|
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
|
|
|
|
|
} finally {
|
|
|
|
|
members.recycle()
|
|
|
|
|
}
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(ContactsStorageException::class)
|
|
|
|
|
override fun clearDirty(eTag: String) {
|
|
|
|
|
assertID()
|
|
|
|
|
val id = requireNotNull(id)
|
|
|
|
|
|
|
|
|
|
val values = ContentValues(2)
|
|
|
|
|
values.put(Groups.DIRTY, 0)
|
|
|
|
@ -109,7 +159,7 @@ class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
update(values)
|
|
|
|
|
|
|
|
|
|
// update cached group memberships
|
|
|
|
|
val batch = BatchOperation(addressBook.provider)
|
|
|
|
|
val batch = BatchOperation(addressBook.provider!!)
|
|
|
|
|
|
|
|
|
|
// delete cached group memberships
|
|
|
|
|
batch.enqueue(BatchOperation.Operation(
|
|
|
|
@ -121,7 +171,7 @@ class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
// insert updated cached group memberships
|
|
|
|
|
for (member in members)
|
|
|
|
|
for (member in getMembers())
|
|
|
|
|
batch.enqueue(BatchOperation.Operation(
|
|
|
|
|
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
|
|
|
|
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
|
|
|
@ -133,7 +183,6 @@ class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
batch.commit()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(ContactsStorageException::class)
|
|
|
|
|
override fun prepareForUpload() {
|
|
|
|
|
val uid = UUID.randomUUID().toString()
|
|
|
|
|
|
|
|
|
@ -145,27 +194,14 @@ class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
fileName = uid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun contentValues(): ContentValues {
|
|
|
|
|
val values = super.contentValues()
|
|
|
|
|
|
|
|
|
|
val members = Parcel.obtain()
|
|
|
|
|
members.writeStringList(contact.members)
|
|
|
|
|
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
|
|
|
|
|
|
|
|
|
|
members.recycle()
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Marks all members of the current group as dirty.
|
|
|
|
|
*/
|
|
|
|
|
@Throws(ContactsStorageException::class)
|
|
|
|
|
fun markMembersDirty() {
|
|
|
|
|
assertID()
|
|
|
|
|
val batch = BatchOperation(addressBook.provider)
|
|
|
|
|
] val batch = BatchOperation(addressBook.provider!!)
|
|
|
|
|
|
|
|
|
|
for (member in members)
|
|
|
|
|
for (member in getMembers())
|
|
|
|
|
batch.enqueue(BatchOperation.Operation(
|
|
|
|
|
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
|
|
|
|
|
.withValue(RawContacts.DIRTY, 1)
|
|
|
|
@ -175,117 +211,46 @@ class LocalGroup : AndroidGroup, LocalResource {
|
|
|
|
|
batch.commit()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// helpers
|
|
|
|
|
|
|
|
|
|
private fun assertID() {
|
|
|
|
|
if (id == null)
|
|
|
|
|
throw IllegalStateException("Group has not been saved yet")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun toString(): String {
|
|
|
|
|
return "LocalGroup(super=" + super.toString() + ", uuid=" + this.uuid + ")"
|
|
|
|
|
override fun resetDeleted() {
|
|
|
|
|
val values = ContentValues(1)
|
|
|
|
|
values.put(Groups.DELETED, 0)
|
|
|
|
|
addressBook.provider!!.update(groupSyncUri(), values, null, null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// factory
|
|
|
|
|
|
|
|
|
|
internal class Factory : AndroidGroupFactory() {
|
|
|
|
|
|
|
|
|
|
override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalGroup {
|
|
|
|
|
return LocalGroup(addressBook, id, fileName, eTag)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun newInstance(addressBook: AndroidAddressBook, contact: Contact, fileName: String, eTag: String): LocalGroup {
|
|
|
|
|
return LocalGroup(addressBook, contact, fileName, eTag)
|
|
|
|
|
}
|
|
|
|
|
// helpers
|
|
|
|
|
|
|
|
|
|
override fun newArray(size: Int): Array<LocalGroup> {
|
|
|
|
|
return arrayOfNulls(size)
|
|
|
|
|
}
|
|
|
|
|
private fun groupSyncUri(): Uri {
|
|
|
|
|
val id = requireNotNull(id)
|
|
|
|
|
return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
val INSTANCE = Factory()
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
/** marshalled list of member UIDs, as sent by server */
|
|
|
|
|
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Processes all groups with non-null [.COLUMN_PENDING_MEMBERS]: the pending memberships
|
|
|
|
|
* are (if possible) applied, keeping cached memberships in sync.
|
|
|
|
|
* @param addressBook address book to take groups from
|
|
|
|
|
* @throws ContactsStorageException on contact provider errors
|
|
|
|
|
*/
|
|
|
|
|
@Throws(ContactsStorageException::class)
|
|
|
|
|
fun applyPendingMemberships(addressBook: LocalAddressBook) {
|
|
|
|
|
try {
|
|
|
|
|
val cursor = addressBook.provider.query(
|
|
|
|
|
addressBook.syncAdapterURI(Groups.CONTENT_URI),
|
|
|
|
|
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
|
|
|
|
|
"$COLUMN_PENDING_MEMBERS IS NOT NULL", arrayOf(), null
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
val batch = BatchOperation(addressBook.provider)
|
|
|
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
|
|
|
val id = cursor.getLong(0)
|
|
|
|
|
App.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.add(contact.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extract list of member UIDs
|
|
|
|
|
val members = LinkedList<String>()
|
|
|
|
|
val raw = cursor.getBlob(1)
|
|
|
|
|
val parcel = Parcel.obtain()
|
|
|
|
|
parcel.unmarshall(raw, 0, raw.size)
|
|
|
|
|
parcel.setDataPosition(0)
|
|
|
|
|
parcel.readStringList(members)
|
|
|
|
|
parcel.recycle()
|
|
|
|
|
|
|
|
|
|
// insert memberships
|
|
|
|
|
for (uid in members) {
|
|
|
|
|
App.log.fine("Assigning member: $uid")
|
|
|
|
|
try {
|
|
|
|
|
val member = addressBook.findContactByUID(uid)
|
|
|
|
|
member.addToGroup(batch, id)
|
|
|
|
|
changeContactIDs.add(member.id)
|
|
|
|
|
} catch (e: FileNotFoundException) {
|
|
|
|
|
App.log.log(Level.WARNING, "Group member not found: $uid", e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
|
|
|
|
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
|
|
|
|
for (contactID in changeContactIDs) {
|
|
|
|
|
val contact = LocalContact(addressBook, contactID, null, null)
|
|
|
|
|
contact.updateHashCode(batch)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remove pending memberships
|
|
|
|
|
batch.enqueue(BatchOperation.Operation(
|
|
|
|
|
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
|
|
|
|
.withValue(COLUMN_PENDING_MEMBERS, null)
|
|
|
|
|
.withYieldAllowed(true)
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
batch.commit()
|
|
|
|
|
}
|
|
|
|
|
cursor!!.close()
|
|
|
|
|
} catch (e: RemoteException) {
|
|
|
|
|
throw ContactsStorageException("Couldn't get pending memberships", e)
|
|
|
|
|
}
|
|
|
|
|
// factory
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
object Factory: AndroidGroupFactory<LocalGroup> {
|
|
|
|
|
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
|
|
|
|
|
LocalGroup(addressBook, values)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|