1
0
mirror of https://github.com/etesync/android synced 2025-02-16 17:42:03 +00:00

Upgrade vcard4android and ical4android.

This commit is contained in:
Tom Hacohen 2019-01-06 10:08:33 +00:00
parent c7d75277b5
commit 521cda35f5
10 changed files with 325 additions and 490 deletions

View File

@ -18,7 +18,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.etesync.syncadapter" applicationId "com.etesync.syncadapter"
minSdkVersion 16 minSdkVersion 19
targetSdkVersion 26 targetSdkVersion 26
versionCode 43 versionCode 43

View File

@ -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<Contact> {
fun resetDeleted()
}

View File

@ -42,7 +42,14 @@ import ezvcard.VCardVersion
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS 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 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) get() = TextUtils.isEmpty(eTag)
override val content: String override val content: String
@Throws(IOException::class, ContactsStorageException::class)
get() { get() {
val contact: Contact val contact: Contact
contact = this.contact!! contact = this.contact!!
@ -70,70 +76,42 @@ class LocalContact : AndroidContact, LocalResource {
return os.toString() return os.toString()
} }
val lastHashCode: Int constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
@Throws(ContactsStorageException::class) : super(addressBook, values) {}
get() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
App.log.severe("getLastHashCode() should not be called on Android <7")
try { constructor(addressBook: AndroidAddressBook<LocalContact, *>, contact: Contact, uuid: String?, eTag: String?)
val c = addressBook.provider.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null) : super(addressBook, contact, uuid, eTag) {}
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, 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() { fun resetDirty() {
val values = ContentValues(1) val values = ContentValues(1)
values.put(ContactsContract.RawContacts.DIRTY, 0) values.put(ContactsContract.RawContacts.DIRTY, 0)
try { addressBook.provider?.update(rawContactSyncURI(), values, null, null)
addressBook.provider.update(rawContactSyncURI(), values, null, null)
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't clear dirty flag", e)
} }
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) { override fun clearDirty(eTag: String) {
try {
val values = ContentValues(3) val values = ContentValues(3)
values.put(AndroidContact.COLUMN_ETAG, eTag) values.put(AndroidContact.COLUMN_ETAG, eTag)
values.put(ContactsContract.RawContacts.DIRTY, 0) values.put(ContactsContract.RawContacts.DIRTY, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 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 // workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val hashCode = dataHashCode() val hashCode = dataHashCode()
values.put(COLUMN_HASHCODE, hashCode) values.put(COLUMN_HASHCODE, hashCode)
App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode") App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
} }
addressBook.provider.update(rawContactSyncURI(), values, null, null) addressBook.provider?.update(rawContactSyncURI(), values, null, null)
this.eTag = eTag 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)
} }
}
@Throws(ContactsStorageException::class)
override fun prepareForUpload() { override fun prepareForUpload() {
try {
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
val values = ContentValues(2) val values = ContentValues(2)
@ -142,21 +120,16 @@ class LocalContact : AndroidContact, LocalResource {
addressBook.provider.update(rawContactSyncURI(), values, null, null) addressBook.provider.update(rawContactSyncURI(), values, null, null)
fileName = uid fileName = uid
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't update UID", e)
}
} }
override fun populateData(mimeType: String, row: ContentValues) { override fun populateData(mimeType: String, row: ContentValues) {
when (mimeType) { when (mimeType) {
CachedGroupMembership.CONTENT_ITEM_TYPE -> cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID)) CachedGroupMembership.CONTENT_ITEM_TYPE -> cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
GroupMembership.CONTENT_ITEM_TYPE -> groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_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) { override fun insertDataRows(batch: BatchOperation) {
super.insertDataRows(batch) super.insertDataRows(batch)
@ -176,16 +149,14 @@ class LocalContact : AndroidContact, LocalResource {
} }
@Throws(ContactsStorageException::class) fun updateAsDirty(contact: Contact): Uri {
fun updateAsDirty(contact: Contact): Int {
saveAsDirty = true saveAsDirty = true
return this.update(contact) return this.update(contact)
} }
@Throws(ContactsStorageException::class)
fun createAsDirty(): Uri { fun createAsDirty(): Uri {
saveAsDirty = true saveAsDirty = true
return this.create() return this.add()
} }
override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) { 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. * 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) * @return hash code of contact data (including group memberships)
*/ */
@Throws(FileNotFoundException::class, ContactsStorageException::class) internal fun dataHashCode(): Int {
fun dataHashCode(): Int { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) throw IllegalStateException("dataHashCode() should not be called on Android != 7")
App.log.severe("dataHashCode() should not be called on Android <7")
// reset contact so that getContact() reads from database // reset contact so that getContact() reads from database
contact = null contact = null
// groupMemberships is filled by getContact() // groupMemberships is filled by getContact()
val dataHash = getContact().hashCode() val dataHash = contact!!.hashCode()
val groupHash = groupMemberships.hashCode() val groupHash = groupMemberships.hashCode()
App.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash") App.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash")
return dataHash xor groupHash return dataHash xor groupHash
} }
@Throws(ContactsStorageException::class)
fun updateHashCode(batch: BatchOperation?) { fun updateHashCode(batch: BatchOperation?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
App.log.severe("updateHashCode() should not be called on Android <7") throw IllegalStateException("updateHashCode() should not be called on Android != 7")
val values = ContentValues(1) val values = ContentValues(1)
try {
val hashCode = dataHashCode() val hashCode = dataHashCode()
App.log.fine("Storing contact hash = $hashCode") App.log.fine("Storing contact hash = $hashCode")
values.put(COLUMN_HASHCODE, hashCode) values.put(COLUMN_HASHCODE, hashCode)
if (batch == null) if (batch == null)
addressBook.provider.update(rawContactSyncURI(), values, null, null) addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
else { else {
val builder = ContentProviderOperation val builder = ContentProviderOperation
.newUpdate(rawContactSyncURI()) .newUpdate(rawContactSyncURI())
.withValues(values) .withValues(values)
batch.enqueue(BatchOperation.Operation(builder)) 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)
} }
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) { fun addToGroup(batch: BatchOperation, groupID: Long) {
assertID()
batch.enqueue(BatchOperation.Operation( batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(dataSyncURI()) ContentProviderOperation.newInsert(dataSyncURI())
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
@ -263,7 +235,6 @@ class LocalContact : AndroidContact, LocalResource {
} }
fun removeGroupMemberships(batch: BatchOperation) { fun removeGroupMemberships(batch: BatchOperation) {
assertID()
batch.enqueue(BatchOperation.Operation( batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(dataSyncURI()) ContentProviderOperation.newDelete(dataSyncURI())
.withSelection( .withSelection(
@ -284,9 +255,8 @@ class LocalContact : AndroidContact, LocalResource {
* @throws ContactsStorageException on contact provider errors * @throws ContactsStorageException on contact provider errors
* @throws FileNotFoundException if the current contact can't be found * @throws FileNotFoundException if the current contact can't be found
*/ */
@Throws(ContactsStorageException::class, FileNotFoundException::class)
fun getCachedGroupMemberships(): Set<Long> { fun getCachedGroupMemberships(): Set<Long> {
getContact() contact
return cachedGroupMemberships return cachedGroupMemberships
} }
@ -296,37 +266,16 @@ class LocalContact : AndroidContact, LocalResource {
* @throws ContactsStorageException on contact provider errors * @throws ContactsStorageException on contact provider errors
* @throws FileNotFoundException if the current contact can't be found * @throws FileNotFoundException if the current contact can't be found
*/ */
@Throws(ContactsStorageException::class, FileNotFoundException::class)
fun getGroupMemberships(): Set<Long> { fun getGroupMemberships(): Set<Long> {
getContact() contact
return groupMemberships return groupMemberships
} }
// factory // factory
internal class Factory : AndroidContactFactory() { object Factory: AndroidContactFactory<LocalContact> {
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalContact { LocalContact(addressBook, values)
return LocalContact(addressBook, id, fileName, eTag)
} }
override fun newArray(size: Int): Array<LocalContact?> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
}
companion object {
init {
Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION
}
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
}
} }

View File

@ -8,36 +8,34 @@
package com.etesync.syncadapter.resource package com.etesync.syncadapter.resource
import android.annotation.TargetApi
import android.content.ContentProviderOperation import android.content.ContentProviderOperation
import android.content.ContentValues import android.content.ContentValues
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.RemoteException import android.os.RemoteException
import android.provider.CalendarContract import android.provider.CalendarContract
import android.provider.CalendarContract.Events import android.provider.CalendarContract.Events
import android.text.TextUtils 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.App
import com.etesync.syncadapter.Constants import com.etesync.syncadapter.Constants
import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.ProdId
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.util.UUID import java.util.*
import java.util.logging.Level import java.util.logging.Level
import at.bitfire.ical4android.AndroidCalendar class LocalEvent : AndroidEvent, LocalResource<Event> {
import at.bitfire.ical4android.AndroidEvent companion object {
import at.bitfire.ical4android.AndroidEventFactory init {
import at.bitfire.ical4android.CalendarStorageException ICalendar.prodId = ProdId(Constants.PRODID_BASE + " ical4j/" + ical4jVersion)
import at.bitfire.ical4android.Event }
import at.bitfire.vcard4android.ContactsStorageException
@TargetApi(17) internal const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
class LocalEvent : AndroidEvent, LocalResource { 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 private var saveAsDirty = false // When true, the resource will be saved as dirty
@ -47,12 +45,11 @@ class LocalEvent : AndroidEvent, LocalResource {
var weAreOrganizer = true var weAreOrganizer = true
override val content: String override val content: String
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
get() { 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() val os = ByteArrayOutputStream()
getEvent().write(os) event?.write(os)
return os.toString() return os.toString()
} }
@ -64,34 +61,27 @@ class LocalEvent : AndroidEvent, LocalResource {
val uuid: String? val uuid: String?
get() = fileName 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.fileName = fileName
this.eTag = eTag this.eTag = eTag
} }
protected constructor(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues?) : super(calendar, id, baseInfo) { protected constructor(calendar: AndroidCalendar<*>, baseInfo: ContentValues) : super(calendar, baseInfo) {
if (baseInfo != null) {
fileName = baseInfo.getAsString(Events._SYNC_ID) fileName = baseInfo.getAsString(Events._SYNC_ID)
eTag = baseInfo.getAsString(COLUMN_ETAG) eTag = baseInfo.getAsString(COLUMN_ETAG)
} }
}
/* process LocalEvent-specific fields */ /* process LocalEvent-specific fields */
override fun populateEvent(values: ContentValues) { override fun populateEvent(row: ContentValues) {
super.populateEvent(values) super.populateEvent(row)
fileName = values.getAsString(Events._SYNC_ID) fileName = row.getAsString(Events._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG) eTag = row.getAsString(COLUMN_ETAG)
event.uid = values.getAsString(COLUMN_UID) event?.uid = row.getAsString(COLUMN_UID)
event.sequence = values.getAsInteger(COLUMN_SEQUENCE) event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
if (Build.VERSION.SDK_INT >= 17) { val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
val isOrganizer = values.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0 weAreOrganizer = isOrganizer != null && isOrganizer != 0
} else {
val organizer = values.getAsString(Events.ORGANIZER)
weAreOrganizer = organizer == null || organizer == calendar.account.name
}
} }
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) { override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
@ -126,9 +116,7 @@ class LocalEvent : AndroidEvent, LocalResource {
/* custom queries */ /* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() { override fun prepareForUpload() {
try {
var uid: String? = null var uid: String? = null
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
if (c.moveToNext()) if (c.moveToNext())
@ -145,58 +133,25 @@ class LocalEvent : AndroidEvent, LocalResource {
calendar.provider.update(eventSyncURI(), values, null, null) calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName fileName = newFileName
val event = this.event
if (event != null) if (event != null)
event.uid = uid event.uid = uid
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update UID", e)
} }
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String) { override fun clearDirty(eTag: String) {
try {
val values = ContentValues(2) val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0) values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag) values.put(COLUMN_ETAG, eTag)
if (event != null) if (event != null)
values.put(COLUMN_SEQUENCE, event.sequence) values.put(COLUMN_SEQUENCE, event?.sequence)
calendar.provider.update(eventSyncURI(), values, null, null) calendar.provider.update(eventSyncURI(), values, null, null)
this.eTag = eTag this.eTag = eTag
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update UID", e)
} }
} object Factory: AndroidEventFactory<LocalEvent> {
override fun fromProvider(calendar: AndroidCalendar<AndroidEvent>, values: ContentValues): LocalEvent =
internal class Factory : AndroidEventFactory { LocalEvent(calendar, values)
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<AndroidEvent?> {
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
} }
} }

View File

@ -12,6 +12,7 @@ import android.content.ContentProviderOperation
import android.content.ContentUris import android.content.ContentUris
import android.content.ContentValues import android.content.ContentValues
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Parcel import android.os.Parcel
import android.os.RemoteException import android.os.RemoteException
@ -21,6 +22,7 @@ import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data import android.provider.ContactsContract.RawContacts.Data
import android.text.TextUtils import android.text.TextUtils
import at.bitfire.vcard4android.*
import com.etesync.syncadapter.App import com.etesync.syncadapter.App
@ -34,27 +36,88 @@ import java.util.LinkedList
import java.util.UUID import java.util.UUID
import java.util.logging.Level 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 ezvcard.VCardVersion
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS 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<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) }
// 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 override val content: String
@Throws(IOException::class, ContactsStorageException::class)
get() { get() {
val contact: Contact val contact: Contact
contact = getContact() contact = this.contact!!
App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact) App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact)
@ -65,42 +128,29 @@ class LocalGroup : AndroidGroup, LocalResource {
} }
override val isLocalOnly: Boolean override val isLocalOnly: Boolean
get() = TextUtils.isEmpty(getETag()) get() = TextUtils.isEmpty(eTag)
/** constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
* Lists all members of this group. : super(addressBook, values) {}
* @return list of all members' raw contact IDs
* @throws ContactsStorageException on contact provider errors constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
*/ : super(addressBook, contact, fileName, eTag) {}
val members: LongArray
@Throws(ContactsStorageException::class) override fun contentValues(): ContentValues {
get() { val values = super.contentValues()
assertID()
val members = LinkedList<Long>() val members = Parcel.obtain()
try { try {
val cursor = addressBook.provider.query( members.writeStringList(contact?.members)
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), values.put(COLUMN_PENDING_MEMBERS, members.marshall())
arrayOf(Data.RAW_CONTACT_ID), } finally {
GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", members.recycle()
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), null }
) return values
while (cursor != null && cursor.moveToNext())
members.add(cursor.getLong(0))
cursor!!.close()
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't list group members", e)
} }
return ArrayUtils.toPrimitive(members.toTypedArray())
}
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) { override fun clearDirty(eTag: String) {
assertID() val id = requireNotNull(id)
val values = ContentValues(2) val values = ContentValues(2)
values.put(Groups.DIRTY, 0) values.put(Groups.DIRTY, 0)
@ -109,7 +159,7 @@ class LocalGroup : AndroidGroup, LocalResource {
update(values) update(values)
// update cached group memberships // update cached group memberships
val batch = BatchOperation(addressBook.provider) val batch = BatchOperation(addressBook.provider!!)
// delete cached group memberships // delete cached group memberships
batch.enqueue(BatchOperation.Operation( batch.enqueue(BatchOperation.Operation(
@ -121,7 +171,7 @@ class LocalGroup : AndroidGroup, LocalResource {
)) ))
// insert updated cached group memberships // insert updated cached group memberships
for (member in members) for (member in getMembers())
batch.enqueue(BatchOperation.Operation( batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
@ -133,7 +183,6 @@ class LocalGroup : AndroidGroup, LocalResource {
batch.commit() batch.commit()
} }
@Throws(ContactsStorageException::class)
override fun prepareForUpload() { override fun prepareForUpload() {
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
@ -145,27 +194,14 @@ class LocalGroup : AndroidGroup, LocalResource {
fileName = uid 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. * Marks all members of the current group as dirty.
*/ */
@Throws(ContactsStorageException::class)
fun markMembersDirty() { 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( batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member))) ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
.withValue(RawContacts.DIRTY, 1) .withValue(RawContacts.DIRTY, 1)
@ -175,117 +211,46 @@ class LocalGroup : AndroidGroup, LocalResource {
batch.commit() batch.commit()
} }
override fun resetDeleted() {
val values = ContentValues(1)
values.put(Groups.DELETED, 0)
addressBook.provider!!.update(groupSyncUri(), values, null, null)
}
// helpers // helpers
private fun assertID() { private fun groupSyncUri(): Uri {
if (id == null) val id = requireNotNull(id)
throw IllegalStateException("Group has not been saved yet") 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<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 // factory
internal class Factory : AndroidGroupFactory() { object Factory: AndroidGroupFactory<LocalGroup> {
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalGroup { LocalGroup(addressBook, values)
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<LocalGroup> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
}
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)
}
}
}
} }

View File

@ -8,12 +8,10 @@
package com.etesync.syncadapter.resource package com.etesync.syncadapter.resource
import java.io.IOException
import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.ContactsStorageException
interface LocalResource { interface LocalResource<in TData: Any> {
val uuid: String? val uuid: String?
/** True if doesn't exist on server yet, false otherwise. */ /** 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. */ /** Returns a string of how this should be represented for example: vCard. */
val content: String val content: String
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun delete(): Int fun delete(): Int
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun prepareForUpload() fun prepareForUpload()
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun clearDirty(eTag: String) fun clearDirty(eTag: String)
} }

View File

@ -10,70 +10,62 @@ package com.etesync.syncadapter.resource
import android.content.ContentProviderOperation import android.content.ContentProviderOperation
import android.content.ContentValues import android.content.ContentValues
import android.os.RemoteException import android.text.TextUtils
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 at.bitfire.ical4android.AndroidTask import at.bitfire.ical4android.AndroidTask
import at.bitfire.ical4android.AndroidTaskFactory import at.bitfire.ical4android.AndroidTaskFactory
import at.bitfire.ical4android.AndroidTaskList import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Task 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<Task> {
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 private var fileName: String? = null
var eTag: String? = null var eTag: String? = null
override val content: String 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 override val isLocalOnly: Boolean
get() = false get() = TextUtils.isEmpty(eTag)
override// Now the same override// Now the same
val uuid: String? val uuid: String?
get() = fileName 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.fileName = fileName
this.eTag = eTag this.eTag = eTag
} }
protected constructor(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues?) : super(taskList, id) { private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) {
if (baseInfo != null) { id = values.getAsLong(TaskContract.Tasks._ID)
fileName = baseInfo.getAsString(Events._SYNC_ID) fileName = values.getAsString(TaskContract.Tasks._SYNC_ID)
eTag = baseInfo.getAsString(COLUMN_ETAG) eTag = values.getAsString(COLUMN_ETAG)
}
} }
/* process LocalTask-specific fields */ /* 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) { override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
super.buildTask(builder, update) super.buildTask(builder, update)
builder.withValue(Tasks._SYNC_ID, fileName) builder.withValue(TaskContract.Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task.uid) .withValue(COLUMN_UID, task.uid)
.withValue(COLUMN_SEQUENCE, task.sequence) .withValue(COLUMN_SEQUENCE, task.sequence)
.withValue(COLUMN_ETAG, eTag) .withValue(COLUMN_ETAG, eTag)
@ -82,71 +74,34 @@ class LocalTask : AndroidTask, LocalResource {
/* custom queries */ /* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() { override fun prepareForUpload() {
try {
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
val values = ContentValues(2) val values = ContentValues(2)
values.put(Tasks._SYNC_ID, newFileName) values.put(TaskContract.Tasks._SYNC_ID, uid)
values.put(COLUMN_UID, uid) values.put(COLUMN_UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null) taskList.provider.client.update(taskSyncURI(), values, null, null)
fileName = newFileName fileName = uid
val task = this.task
if (task != null) if (task != null)
task.uid = uid task.uid = uid
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update UID", e)
} }
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String) { override fun clearDirty(eTag: String) {
try {
val values = ContentValues(2) val values = ContentValues(2)
values.put(Tasks._DIRTY, 0) values.put(TaskContract.Tasks._DIRTY, 0)
values.put(COLUMN_ETAG, eTag) values.put(COLUMN_ETAG, eTag)
if (task != null) if (task != null)
values.put(COLUMN_SEQUENCE, task.sequence) values.put(COLUMN_SEQUENCE, task?.sequence)
taskList.provider.client.update(taskSyncURI(), values, null, null) taskList.provider.client.update(taskSyncURI(), values, null, null)
this.eTag = eTag this.eTag = eTag
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e)
}
} }
internal class Factory : AndroidTaskFactory { object Factory: AndroidTaskFactory<LocalTask> {
override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) =
override fun newInstance(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues): LocalTask { LocalTask(taskList, values)
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<LocalTask?> {
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
} }
} }

View File

@ -149,7 +149,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks<String>
report.append("CONFIGURATION\n") report.append("CONFIGURATION\n")
// power saving // 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) if (powerManager != null && Build.VERSION.SDK_INT >= 23)
report.append("Power saving disabled: ") report.append("Power saving disabled: ")
.append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no")

@ -1 +1 @@
Subproject commit 268473341cb761a0676f1746ff4467e48973f972 Subproject commit fef93f94bbc1265e53e55c95fe86e8c33e2e4f0f

@ -1 +1 @@
Subproject commit 3974799d7790f47987f7ae95fe444ab4442e7786 Subproject commit 42d5cc3f8b16c628fa13a5a3b0f211e6660fb084