1
0
mirror of https://github.com/etesync/android synced 2024-11-22 07:58:09 +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 {
applicationId "com.etesync.syncadapter"
minSdkVersion 16
minSdkVersion 19
targetSdkVersion 26
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
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,70 +76,42 @@ 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<LocalContact,*>, 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<LocalContact, *>, 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)
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
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)
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)
}
}
@Throws(ContactsStorageException::class)
override fun prepareForUpload() {
try {
val uid = UUID.randomUUID().toString()
val values = ContentValues(2)
@ -142,21 +120,16 @@ class LocalContact : AndroidContact, LocalResource {
addressBook.provider.update(rawContactSyncURI(), values, null, null)
fileName = uid
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't update UID", e)
}
}
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)
if (batch == null)
addressBook.provider.update(rawContactSyncURI(), values, null, 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)
}
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()
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<Long> {
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<Long> {
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)
object Factory: AndroidContactFactory<LocalContact> {
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
LocalContact(addressBook, values)
}
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
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<Event> {
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) {
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)
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
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) {
@ -126,9 +116,7 @@ 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())
@ -145,58 +133,25 @@ class LocalEvent : AndroidEvent, LocalResource {
calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName
val event = this.event
if (event != null)
event.uid = uid
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update UID", e)
}
}
@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)
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)
}
}
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<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
object Factory: AndroidEventFactory<LocalEvent> {
override fun fromProvider(calendar: AndroidCalendar<AndroidEvent>, values: ContentValues): LocalEvent =
LocalEvent(calendar, values)
}
}

View File

@ -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<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
@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>()
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
: super(addressBook, values) {}
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, 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 {
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)
members.writeStringList(contact?.members)
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
} finally {
members.recycle()
}
return values
}
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) {
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<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
internal class Factory : AndroidGroupFactory() {
override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalGroup {
return LocalGroup(addressBook, id, fileName, eTag)
object Factory: AndroidGroupFactory<LocalGroup> {
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
LocalGroup(addressBook, values)
}
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
import java.io.IOException
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
interface LocalResource {
interface LocalResource<in TData: Any> {
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)
}

View File

@ -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<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
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 values = ContentValues(2)
values.put(Tasks._SYNC_ID, newFileName)
values.put(TaskContract.Tasks._SYNC_ID, uid)
values.put(COLUMN_UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
fileName = newFileName
fileName = uid
val task = this.task
if (task != null)
task.uid = uid
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update UID", e)
}
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String) {
try {
val values = ContentValues(2)
values.put(Tasks._DIRTY, 0)
values.put(TaskContract.Tasks._DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
if (task != null)
values.put(COLUMN_SEQUENCE, task.sequence)
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)
}
}
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<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
object Factory: AndroidTaskFactory<LocalTask> {
override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) =
LocalTask(taskList, values)
}
}

View File

@ -149,7 +149,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks<String>
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")

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

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