diff --git a/app/build.gradle b/app/build.gradle index 04b5471c..e027625f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ android { defaultConfig { applicationId "com.etesync.syncadapter" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 26 versionCode 43 diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt new file mode 100644 index 00000000..8fbdf95f --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt @@ -0,0 +1,17 @@ +/* + * Copyright © 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 at.bitfire.vcard4android.Contact + +interface LocalAddress: LocalResource { + + fun resetDeleted() + +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt index de184790..da23b459 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt @@ -42,7 +42,14 @@ import ezvcard.VCardVersion import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS -class LocalContact : AndroidContact, LocalResource { +class LocalContact : AndroidContact, LocalAddress { + companion object { + init { + Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION + } + + internal const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3 + } private var saveAsDirty = false // When true, the resource will be saved as dirty @@ -57,7 +64,6 @@ class LocalContact : AndroidContact, LocalResource { get() = TextUtils.isEmpty(eTag) override val content: String - @Throws(IOException::class, ContactsStorageException::class) get() { val contact: Contact contact = this.contact!! @@ -70,93 +76,60 @@ class LocalContact : AndroidContact, LocalResource { return os.toString() } - val lastHashCode: Int - @Throws(ContactsStorageException::class) - get() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.log.severe("getLastHashCode() should not be called on Android <7") + constructor(addressBook: AndroidAddressBook, values: ContentValues) + : super(addressBook, values) {} - try { - val c = addressBook.provider.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null) - try { - return if (c == null || !c.moveToNext() || c.isNull(0)) 0 else c.getInt(0) - } finally { - c?.close() - } - } catch (e: RemoteException) { - throw ContactsStorageException("Could't read last hash code", e) - } + constructor(addressBook: AndroidAddressBook, contact: Contact, uuid: String?, eTag: String?) + : super(addressBook, contact, uuid, eTag) {} - } - - - constructor(addressBook: AndroidAddressBook, id: Long, uuid: String?, eTag: String?) : super(addressBook, id, uuid, eTag) {} - - constructor(addressBook: AndroidAddressBook, contact: Contact, uuid: String?, eTag: String?) : super(addressBook, contact, uuid, eTag) {} - - @Throws(ContactsStorageException::class) fun resetDirty() { val values = ContentValues(1) values.put(ContactsContract.RawContacts.DIRTY, 0) - try { - addressBook.provider.update(rawContactSyncURI(), values, null, null) - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't clear dirty flag", e) - } - + addressBook.provider?.update(rawContactSyncURI(), values, null, null) + } + + override fun resetDeleted() { + val values = ContentValues(1) + values.put(ContactsContract.Groups.DELETED, 0) + addressBook.provider?.update(rawContactSyncURI(), values, null, null) } - @Throws(ContactsStorageException::class) override fun clearDirty(eTag: String) { - try { - val values = ContentValues(3) - values.put(AndroidContact.COLUMN_ETAG, eTag) - values.put(ContactsContract.RawContacts.DIRTY, 0) + val values = ContentValues(3) + values.put(AndroidContact.COLUMN_ETAG, eTag) + values.put(ContactsContract.RawContacts.DIRTY, 0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // workaround for Android 7 which sets DIRTY flag when only meta-data is changed - val hashCode = dataHashCode() - values.put(COLUMN_HASHCODE, hashCode) - App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode") - } - - addressBook.provider.update(rawContactSyncURI(), values, null, null) - - this.eTag = eTag - } catch (e: FileNotFoundException) { - throw ContactsStorageException("Couldn't clear dirty flag", e) - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't clear dirty flag", e) + 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 + val hashCode = dataHashCode() + values.put(COLUMN_HASHCODE, hashCode) + App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode") } + addressBook.provider?.update(rawContactSyncURI(), values, null, null) + + this.eTag = eTag } - @Throws(ContactsStorageException::class) override fun prepareForUpload() { - try { - val uid = UUID.randomUUID().toString() + val uid = UUID.randomUUID().toString() - val values = ContentValues(2) - values.put(AndroidContact.COLUMN_FILENAME, uid) - values.put(AndroidContact.COLUMN_UID, uid) - addressBook.provider.update(rawContactSyncURI(), values, null, null) - - fileName = uid - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't update UID", e) - } + val values = ContentValues(2) + values.put(AndroidContact.COLUMN_FILENAME, uid) + values.put(AndroidContact.COLUMN_UID, uid) + addressBook.provider.update(rawContactSyncURI(), values, null, null) + fileName = uid } 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) + UnknownProperties.CONTENT_ITEM_TYPE -> contact?.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES) } } - @Throws(ContactsStorageException::class) override fun insertDataRows(batch: BatchOperation) { super.insertDataRows(batch) @@ -176,16 +149,14 @@ class LocalContact : AndroidContact, LocalResource { } - @Throws(ContactsStorageException::class) - fun updateAsDirty(contact: Contact): Int { + fun updateAsDirty(contact: Contact): Uri { saveAsDirty = true return this.update(contact) } - @Throws(ContactsStorageException::class) fun createAsDirty(): Uri { saveAsDirty = true - return this.create() + return this.add() } override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) { @@ -195,55 +166,56 @@ class LocalContact : AndroidContact, LocalResource { /** * Calculates a hash code from the contact's data (VCard) and group memberships. - * Attention: re-reads [.contact] from the database, discarding all changes in memory + * Attention: re-reads {@link #contact} from the database, discarding all changes in memory * @return hash code of contact data (including group memberships) */ - @Throws(FileNotFoundException::class, ContactsStorageException::class) - fun dataHashCode(): Int { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.log.severe("dataHashCode() should not be called on Android <7") + internal fun dataHashCode(): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + 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 = getContact().hashCode() + val dataHash = contact!!.hashCode() val groupHash = groupMemberships.hashCode() App.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash") return dataHash xor groupHash } - @Throws(ContactsStorageException::class) fun updateHashCode(batch: BatchOperation?) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.log.severe("updateHashCode() should not be called on Android <7") + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + throw IllegalStateException("updateHashCode() should not be called on Android != 7") val values = ContentValues(1) - try { - val hashCode = dataHashCode() - App.log.fine("Storing contact hash = $hashCode") - values.put(COLUMN_HASHCODE, hashCode) + val hashCode = dataHashCode() + App.log.fine("Storing contact hash = $hashCode") + values.put(COLUMN_HASHCODE, hashCode) - if (batch == null) - addressBook.provider.update(rawContactSyncURI(), values, null, null) - else { - val builder = ContentProviderOperation - .newUpdate(rawContactSyncURI()) - .withValues(values) - batch.enqueue(BatchOperation.Operation(builder)) - } - } catch (e: FileNotFoundException) { - throw ContactsStorageException("Couldn't store contact checksum", e) - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't store contact checksum", e) + if (batch == null) + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + else { + val builder = ContentProviderOperation + .newUpdate(rawContactSyncURI()) + .withValues(values) + batch.enqueue(BatchOperation.Operation(builder)) } + } + fun getLastHashCode(): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + 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) { - assertID() + fun addToGroup(batch: BatchOperation, groupID: Long) { batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newInsert(dataSyncURI()) .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) @@ -263,7 +235,6 @@ class LocalContact : AndroidContact, LocalResource { } fun removeGroupMemberships(batch: BatchOperation) { - assertID() batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newDelete(dataSyncURI()) .withSelection( @@ -284,9 +255,8 @@ class LocalContact : AndroidContact, LocalResource { * @throws ContactsStorageException on contact provider errors * @throws FileNotFoundException if the current contact can't be found */ - @Throws(ContactsStorageException::class, FileNotFoundException::class) fun getCachedGroupMemberships(): Set { - getContact() + contact return cachedGroupMemberships } @@ -296,37 +266,16 @@ class LocalContact : AndroidContact, LocalResource { * @throws ContactsStorageException on contact provider errors * @throws FileNotFoundException if the current contact can't be found */ - @Throws(ContactsStorageException::class, FileNotFoundException::class) fun getGroupMemberships(): Set { - getContact() + contact return groupMemberships } // factory - internal class Factory : AndroidContactFactory() { - - override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalContact { - return LocalContact(addressBook, id, fileName, eTag) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - + object Factory: AndroidContactFactory { + override fun fromProvider(addressBook: AndroidAddressBook, values: ContentValues) = + LocalContact(addressBook, values) } - - companion object { - init { - Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION - } - - val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3 - } - } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt index dc90e2af..83c8f85d 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt @@ -8,36 +8,34 @@ package com.etesync.syncadapter.resource -import android.annotation.TargetApi import android.content.ContentProviderOperation import android.content.ContentValues -import android.database.Cursor import android.net.Uri -import android.os.Build import android.os.RemoteException import android.provider.CalendarContract import android.provider.CalendarContract.Events import android.text.TextUtils - +import at.bitfire.ical4android.* +import at.bitfire.ical4android.Constants.ical4jVersion +import at.bitfire.vcard4android.ContactsStorageException import com.etesync.syncadapter.App import com.etesync.syncadapter.Constants - import net.fortuna.ical4j.model.property.ProdId - import java.io.ByteArrayOutputStream import java.io.IOException -import java.util.UUID +import java.util.* import java.util.logging.Level -import at.bitfire.ical4android.AndroidCalendar -import at.bitfire.ical4android.AndroidEvent -import at.bitfire.ical4android.AndroidEventFactory -import at.bitfire.ical4android.CalendarStorageException -import at.bitfire.ical4android.Event -import at.bitfire.vcard4android.ContactsStorageException +class LocalEvent : AndroidEvent, LocalResource { + companion object { + init { + ICalendar.prodId = ProdId(Constants.PRODID_BASE + " ical4j/" + ical4jVersion) + } -@TargetApi(17) -class LocalEvent : AndroidEvent, LocalResource { + internal const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1 + internal const val COLUMN_UID = Events.UID_2445 + internal const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3 + } private var saveAsDirty = false // When true, the resource will be saved as dirty @@ -47,12 +45,11 @@ class LocalEvent : AndroidEvent, LocalResource { var weAreOrganizer = true override val content: String - @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class) get() { - App.log.log(Level.FINE, "Preparing upload of event " + fileName!!, getEvent()) + App.log.log(Level.FINE, "Preparing upload of event " + fileName!!, event) val os = ByteArrayOutputStream() - getEvent().write(os) + event?.write(os) return os.toString() } @@ -64,34 +61,27 @@ class LocalEvent : AndroidEvent, LocalResource { val uuid: String? get() = fileName - constructor(calendar: AndroidCalendar, event: Event, fileName: String?, eTag: String?) : super(calendar, event) { + constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) { this.fileName = fileName this.eTag = eTag } - protected constructor(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues?) : super(calendar, id, baseInfo) { - if (baseInfo != null) { - fileName = baseInfo.getAsString(Events._SYNC_ID) - eTag = baseInfo.getAsString(COLUMN_ETAG) - } + protected constructor(calendar: AndroidCalendar<*>, baseInfo: ContentValues) : super(calendar, baseInfo) { + fileName = baseInfo.getAsString(Events._SYNC_ID) + eTag = baseInfo.getAsString(COLUMN_ETAG) } /* process LocalEvent-specific fields */ - override fun populateEvent(values: ContentValues) { - super.populateEvent(values) - fileName = values.getAsString(Events._SYNC_ID) - eTag = values.getAsString(COLUMN_ETAG) - event.uid = values.getAsString(COLUMN_UID) + override fun populateEvent(row: ContentValues) { + super.populateEvent(row) + fileName = row.getAsString(Events._SYNC_ID) + eTag = row.getAsString(COLUMN_ETAG) + event?.uid = row.getAsString(COLUMN_UID) - event.sequence = values.getAsInteger(COLUMN_SEQUENCE) - if (Build.VERSION.SDK_INT >= 17) { - val isOrganizer = values.getAsInteger(Events.IS_ORGANIZER) - weAreOrganizer = isOrganizer != null && isOrganizer != 0 - } else { - val organizer = values.getAsString(Events.ORGANIZER) - weAreOrganizer = organizer == null || organizer == calendar.account.name - } + event.sequence = row.getAsInteger(COLUMN_SEQUENCE) + val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER) + weAreOrganizer = isOrganizer != null && isOrganizer != 0 } override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) { @@ -126,77 +116,42 @@ class LocalEvent : AndroidEvent, LocalResource { /* custom queries */ - @Throws(CalendarStorageException::class) override fun prepareForUpload() { - try { - var uid: String? = null - val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) - if (c.moveToNext()) - uid = c.getString(0) - if (uid == null) - uid = UUID.randomUUID().toString() + var uid: String? = null + val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) + if (c.moveToNext()) + uid = c.getString(0) + if (uid == null) + uid = UUID.randomUUID().toString() - c.close() - val newFileName = uid + c.close() + val newFileName = uid - val values = ContentValues(2) - values.put(Events._SYNC_ID, newFileName) - values.put(COLUMN_UID, uid) - calendar.provider.update(eventSyncURI(), values, null, null) + val values = ContentValues(2) + values.put(Events._SYNC_ID, newFileName) + values.put(COLUMN_UID, uid) + calendar.provider.update(eventSyncURI(), values, null, null) - fileName = newFileName - if (event != null) - event.uid = uid - - } catch (e: RemoteException) { - throw CalendarStorageException("Couldn't update UID", e) - } + fileName = newFileName + val event = this.event + if (event != null) + event.uid = uid } - @Throws(CalendarStorageException::class) override fun clearDirty(eTag: String) { - try { - val values = ContentValues(2) - values.put(CalendarContract.Events.DIRTY, 0) - values.put(COLUMN_ETAG, eTag) - if (event != null) - values.put(COLUMN_SEQUENCE, event.sequence) - calendar.provider.update(eventSyncURI(), values, null, null) - - this.eTag = eTag - } catch (e: RemoteException) { - throw CalendarStorageException("Couldn't update UID", e) - } + val values = ContentValues(2) + values.put(CalendarContract.Events.DIRTY, 0) + values.put(COLUMN_ETAG, eTag) + if (event != null) + values.put(COLUMN_SEQUENCE, event?.sequence) + calendar.provider.update(eventSyncURI(), values, null, null) + this.eTag = eTag } - internal class Factory : AndroidEventFactory { - - override fun newInstance(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues): AndroidEvent { - return LocalEvent(calendar, id, baseInfo) - } - - override fun newInstance(calendar: AndroidCalendar, event: Event): AndroidEvent { - return LocalEvent(calendar, event, null, null) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - } - - companion object { - init { - Event.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x") - } - - internal val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1 - internal val COLUMN_UID = if (Build.VERSION.SDK_INT >= 17) Events.UID_2445 else Events.SYNC_DATA2 - internal val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3 + object Factory: AndroidEventFactory { + override fun fromProvider(calendar: AndroidCalendar, values: ContentValues): LocalEvent = + LocalEvent(calendar, values) } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index bc2c30a5..b6c942b2 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -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 - override val uuid: String - get() = getFileName() + /** + * 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() + + // 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() + 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) } + + // 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() - 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, values: ContentValues) + : super(addressBook, values) {} - return ArrayUtils.toPrimitive(members.toTypedArray()) + constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?, flags: Int) + : 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() } + return values + } - constructor(addressBook: AndroidAddressBook, id: Long, fileName: String?, eTag: String?) : super(addressBook, id, fileName, eTag) {} - - constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?) : super(addressBook, contact, fileName, eTag) {} - - @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() } + override fun resetDeleted() { + val values = ContentValues(1) + values.put(Groups.DELETED, 0) + addressBook.provider!!.update(groupSyncUri(), values, null, null) + } + // helpers - private fun assertID() { - if (id == null) - throw IllegalStateException("Group has not been saved yet") + private fun groupSyncUri(): Uri { + val id = requireNotNull(id) + return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id) } - override fun toString(): String { - return "LocalGroup(super=" + super.toString() + ", uuid=" + this.uuid + ")" + /** + * Lists all members of this group. + * @return list of all members' raw contact IDs + * @throws RemoteException on contact provider errors + */ + internal fun getMembers(): List { + val id = requireNotNull(id) + val members = LinkedList() + 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 - 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) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - + object Factory: AndroidGroupFactory { + override fun fromProvider(addressBook: AndroidAddressBook, values: ContentValues) = + LocalGroup(addressBook, values) } - - 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() - - // 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() - 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) - } - - } - } - } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt index 56aafebf..d4413fac 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt @@ -8,12 +8,10 @@ package com.etesync.syncadapter.resource -import java.io.IOException - import at.bitfire.ical4android.CalendarStorageException import at.bitfire.vcard4android.ContactsStorageException -interface LocalResource { +interface LocalResource { val uuid: String? /** True if doesn't exist on server yet, false otherwise. */ @@ -22,13 +20,9 @@ interface LocalResource { /** Returns a string of how this should be represented for example: vCard. */ val content: String - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun delete(): Int - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun prepareForUpload() - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun clearDirty(eTag: String) - } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt index 10b42062..00e26e53 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -10,70 +10,62 @@ package com.etesync.syncadapter.resource import android.content.ContentProviderOperation import android.content.ContentValues -import android.os.RemoteException -import android.provider.CalendarContract.Events - -import com.etesync.syncadapter.Constants - -import net.fortuna.ical4j.model.property.ProdId - -import org.dmfs.provider.tasks.TaskContract.Tasks - -import java.io.FileNotFoundException -import java.io.IOException -import java.text.ParseException -import java.util.UUID - +import android.text.TextUtils import at.bitfire.ical4android.AndroidTask import at.bitfire.ical4android.AndroidTaskFactory import at.bitfire.ical4android.AndroidTaskList -import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.Task -import at.bitfire.vcard4android.ContactsStorageException +import com.etesync.syncadapter.App +import org.dmfs.tasks.contract.TaskContract +import java.io.ByteArrayOutputStream +import java.util.* +import java.util.logging.Level + +class LocalTask : AndroidTask, LocalResource { + companion object { + internal const val COLUMN_ETAG = TaskContract.Tasks.SYNC1 + internal const val COLUMN_UID = TaskContract.Tasks.SYNC2 + internal const val COLUMN_SEQUENCE = TaskContract.Tasks.SYNC3 + } -class LocalTask : AndroidTask, LocalResource { private var fileName: String? = null var eTag: String? = null override val content: String - @Throws(IOException::class, ContactsStorageException::class) - get() = "" + get() { + App.log.log(Level.FINE, "Preparing upload of task " + fileName!!, task) + + val os = ByteArrayOutputStream() + task?.write(os) + + return os.toString() + } override val isLocalOnly: Boolean - get() = false + get() = TextUtils.isEmpty(eTag) override// Now the same val uuid: String? get() = fileName - constructor(taskList: AndroidTaskList, task: Task, fileName: String?, eTag: String?) : super(taskList, task) { + constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int) + : super(taskList, task) { this.fileName = fileName this.eTag = eTag } - protected constructor(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues?) : super(taskList, id) { - if (baseInfo != null) { - fileName = baseInfo.getAsString(Events._SYNC_ID) - eTag = baseInfo.getAsString(COLUMN_ETAG) - } + private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) { + id = values.getAsLong(TaskContract.Tasks._ID) + fileName = values.getAsString(TaskContract.Tasks._SYNC_ID) + eTag = values.getAsString(COLUMN_ETAG) } + /* process LocalTask-specific fields */ - @Throws(FileNotFoundException::class, RemoteException::class, ParseException::class) - override fun populateTask(values: ContentValues) { - super.populateTask(values) - - fileName = values.getAsString(Events._SYNC_ID) - eTag = values.getAsString(COLUMN_ETAG) - task.uid = values.getAsString(COLUMN_UID) - - task.sequence = values.getAsInteger(COLUMN_SEQUENCE) - } - override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) { super.buildTask(builder, update) - builder.withValue(Tasks._SYNC_ID, fileName) + builder.withValue(TaskContract.Tasks._SYNC_ID, fileName) .withValue(COLUMN_UID, task.uid) .withValue(COLUMN_SEQUENCE, task.sequence) .withValue(COLUMN_ETAG, eTag) @@ -82,71 +74,34 @@ class LocalTask : AndroidTask, LocalResource { /* custom queries */ - @Throws(CalendarStorageException::class) override fun prepareForUpload() { - try { - val uid = UUID.randomUUID().toString() - val newFileName = "$uid.ics" + val uid = UUID.randomUUID().toString() - val values = ContentValues(2) - values.put(Tasks._SYNC_ID, newFileName) - values.put(COLUMN_UID, uid) - taskList.provider.client.update(taskSyncURI(), values, null, null) - - fileName = newFileName - if (task != null) - task.uid = uid - - } catch (e: RemoteException) { - throw CalendarStorageException("Couldn't update UID", e) - } + val values = ContentValues(2) + values.put(TaskContract.Tasks._SYNC_ID, uid) + values.put(COLUMN_UID, uid) + taskList.provider.client.update(taskSyncURI(), values, null, null) + fileName = uid + val task = this.task + if (task != null) + task.uid = uid } - @Throws(CalendarStorageException::class) override fun clearDirty(eTag: String) { - try { - val values = ContentValues(2) - values.put(Tasks._DIRTY, 0) - values.put(COLUMN_ETAG, eTag) - if (task != null) - values.put(COLUMN_SEQUENCE, task.sequence) - taskList.provider.client.update(taskSyncURI(), values, null, null) - - this.eTag = eTag - } catch (e: RemoteException) { - throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e) - } + val values = ContentValues(2) + values.put(TaskContract.Tasks._DIRTY, 0) + values.put(COLUMN_ETAG, eTag) + if (task != null) + values.put(COLUMN_SEQUENCE, task?.sequence) + taskList.provider.client.update(taskSyncURI(), values, null, null) + this.eTag = eTag } - internal class Factory : AndroidTaskFactory { - - override fun newInstance(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues): LocalTask { - return LocalTask(taskList, id, baseInfo) - } - - override fun newInstance(taskList: AndroidTaskList, task: Task): LocalTask { - return LocalTask(taskList, task, null, null) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - } - - companion object { - init { - Task.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x") - } - - internal val COLUMN_ETAG = Tasks.SYNC1 - internal val COLUMN_UID = Tasks.SYNC2 - internal val COLUMN_SEQUENCE = Tasks.SYNC3 + object Factory: AndroidTaskFactory { + override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) = + LocalTask(taskList, values) } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt index d9a41cca..0cb9f211 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt @@ -149,7 +149,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks report.append("CONFIGURATION\n") // power saving - val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager? if (powerManager != null && Build.VERSION.SDK_INT >= 23) report.append("Power saving disabled: ") .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") diff --git a/ical4android b/ical4android index 26847334..fef93f94 160000 --- a/ical4android +++ b/ical4android @@ -1 +1 @@ -Subproject commit 268473341cb761a0676f1746ff4467e48973f972 +Subproject commit fef93f94bbc1265e53e55c95fe86e8c33e2e4f0f diff --git a/vcard4android b/vcard4android index 3974799d..42d5cc3f 160000 --- a/vcard4android +++ b/vcard4android @@ -1 +1 @@ -Subproject commit 3974799d7790f47987f7ae95fe444ab4442e7786 +Subproject commit 42d5cc3f8b16c628fa13a5a3b0f211e6660fb084