Upgrade vcard4android and ical4android.

This is a monster commit because to be honest, it's a monster change. It
was impossible to do it in smaller steps because things just wouldn't
compile.
We couldn't do the migration step by step because they moved to Kotlin
which was causing a lot of troubles.

Now we are all on Kotlin, so things should hopefully work just fine.
pull/61/head
Tom Hacohen 6 years ago
parent b1d974af5a
commit c4daed9391

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

@ -67,6 +67,7 @@ import java.util.logging.Logger
import javax.net.ssl.HostnameVerifier
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import io.requery.Persistable
@ -296,8 +297,8 @@ class App : Application() {
// Generate account settings to make sure account is migrated.
AccountSettings(this, account)
val calendars = LocalCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
LocalCalendar.Factory.INSTANCE, null, null) as Array<LocalCalendar>
val calendars = AndroidCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
LocalCalendar.Factory, null, null)
for (calendar in calendars) {
calendar.fixEtags()
}

@ -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()
}

@ -12,6 +12,7 @@ import android.accounts.AccountManager
import android.accounts.AccountManagerCallback
import android.accounts.AccountManagerFuture
import android.accounts.AuthenticatorException
import android.annotation.TargetApi
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.ContentUris
@ -47,110 +48,124 @@ import at.bitfire.vcard4android.CachedGroupMembership
import at.bitfire.vcard4android.ContactsStorageException
class LocalAddressBook(protected val context: Context, account: Account, provider: ContentProviderClient?) : AndroidAddressBook(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE), LocalCollection<LocalResource> {
private val syncState = Bundle()
class LocalAddressBook(
private val context: Context,
account: Account,
provider: ContentProviderClient?
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
/**
* Whether contact groups (LocalGroup resources) are included in query results for
* [.getDeleted], [.getDirty] and
* [.getWithoutFileName].
*/
var includeGroups = true
companion object {
val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
val USER_DATA_URL = "url"
const val USER_DATA_READ_ONLY = "read_only"
/**
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
*/
override val deleted: Array<LocalResource>
@Throws(ContactsStorageException::class)
get() {
val deleted = LinkedList<LocalResource>()
Collections.addAll(deleted, *deletedContacts)
if (includeGroups)
Collections.addAll(deleted, *deletedGroups)
return deleted.toTypedArray()
}
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, journalEntity: JournalEntity): LocalAddressBook {
val info = journalEntity.info
val accountManager = AccountManager.get(context)
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
*/
override val dirty: Array<LocalResource>
@Throws(ContactsStorageException::class)
get() {
val dirty = LinkedList<LocalResource>()
Collections.addAll(dirty, *dirtyContacts)
if (includeGroups)
Collections.addAll(dirty, *dirtyGroups)
return dirty.toTypedArray()
}
val account = Account(accountName(mainAccount, info), App.addressBookAccountType)
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.uid!!)))
throw ContactsStorageException("Couldn't create address book account")
/**
* Returns an array of local contacts which don't have a file name yet.
*/
override val withoutFileName: Array<LocalResource>
@Throws(ContactsStorageException::class)
get() {
val nameless = LinkedList<LocalResource>()
Collections.addAll(nameless, *queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null) as Array<LocalContact>)
if (includeGroups)
Collections.addAll(nameless, *queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null) as Array<LocalGroup>)
return nameless.toTypedArray()
}
val addressBook = LocalAddressBook(context, account, provider)
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
val deletedContacts: Array<LocalContact>
@Throws(ContactsStorageException::class)
get() = queryContacts(RawContacts.DELETED + "!= 0", null) as Array<LocalContact>
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addressBook.settings = values
val dirtyContacts: Array<LocalContact>
@Throws(ContactsStorageException::class)
get() = queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null) as Array<LocalContact>
return addressBook
}
val all: Array<LocalContact>
@Throws(ContactsStorageException::class)
get() = queryContacts(RawContacts.DELETED + "== 0", null) as Array<LocalContact>
val deletedGroups: Array<LocalGroup>
@Throws(ContactsStorageException::class)
get() = queryGroups(Groups.DELETED + "!= 0", null) as Array<LocalGroup>
fun find(context: Context, provider: ContentProviderClient?, mainAccount: Account?) = AccountManager.get(context)
.getAccountsByType(App.addressBookAccountType)
.map { LocalAddressBook(context, it, provider) }
.filter { mainAccount == null || it.mainAccount == mainAccount }
.toList()
val dirtyGroups: Array<LocalGroup>
@Throws(ContactsStorageException::class)
get() = queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null) as Array<LocalGroup>
var mainAccount: Account
@Throws(ContactsStorageException::class)
get() {
fun findByUid(context: Context, provider: ContentProviderClient, mainAccount: Account?, uid: String): LocalAddressBook? {
val accountManager = AccountManager.get(context)
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
return if (name != null && type != null)
Account(name, type)
else
throw ContactsStorageException("Address book doesn't exist anymore")
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, account, provider)
if (addressBook.url == uid && (mainAccount == null || addressBook.mainAccount == mainAccount))
return addressBook
}
return null
}
@Throws(ContactsStorageException::class)
set(mainAccount) {
val accountManager = AccountManager.get(context)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
// HELPERS
fun accountName(mainAccount: Account, info: CollectionInfo): String {
val displayName = if (info.displayName != null) info.displayName else info.uid
val sb = StringBuilder(displayName)
sb.append(" (")
.append(mainAccount.name)
.append(" ")
.append(info.uid!!.substring(0, 4))
.append(")")
return sb.toString()
}
fun initialUserData(mainAccount: Account, url: String): Bundle {
val bundle = Bundle(3)
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
bundle.putString(USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
bundle.putString(USER_DATA_URL, url)
return bundle
}
}
/**
* Whether contact groups (LocalGroup resources) are included in query results for
* [.getDeleted], [.getDirty] and
* [.getWithoutFileName].
*/
var includeGroups = true
var url: String?
@Throws(ContactsStorageException::class)
private var _mainAccount: Account? = null
var mainAccount: Account
get() {
val accountManager = AccountManager.get(context)
return accountManager.getUserData(account, USER_DATA_URL)
_mainAccount?.let { return it }
AccountManager.get(context).let { accountManager ->
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
if (name != null && type != null)
return Account(name, type)
else
throw IllegalStateException("Address book doesn't exist anymore")
}
}
@Throws(ContactsStorageException::class)
set(url) {
val accountManager = AccountManager.get(context)
accountManager.setUserData(account, USER_DATA_URL, url)
set(newMainAccount) {
AccountManager.get(context).let { accountManager ->
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, newMainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, newMainAccount.type)
}
_mainAccount = newMainAccount
}
@Throws(AuthenticatorException::class, OperationCanceledException::class, IOException::class, ContactsStorageException::class, android.accounts.OperationCanceledException::class)
var url: String
get() = AccountManager.get(context).getUserData(account, USER_DATA_URL)
?: throw IllegalStateException("Address book has no URL")
set(url) = AccountManager.get(context).setUserData(account, USER_DATA_URL, url)
var readOnly: Boolean
get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
set(readOnly) = AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
fun update(journalEntity: JournalEntity) {
val info = journalEntity.info
val newAccountName = accountName(mainAccount, info)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
val accountManager = AccountManager.get(context)
val future = accountManager.renameAccount(account, newAccountName, {
try {
@ -168,22 +183,61 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
account = future.result
}
App.log.info("Address book write permission? = ${journalEntity.isReadOnly}")
readOnly = journalEntity.isReadOnly
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
}
fun delete() {
val accountManager = AccountManager.get(context)
AndroidCompat.removeAccount(accountManager, account)
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= 22)
accountManager.removeAccount(account, null, null, null)
else
accountManager.removeAccount(account, null, null)
}
@Throws(ContactsStorageException::class, FileNotFoundException::class)
fun findContactByUID(uid: String): LocalContact {
val contacts = queryContacts(LocalContact.COLUMN_UID + "=?", arrayOf(uid)) as Array<LocalContact>
if (contacts.size == 0)
throw FileNotFoundException()
return contacts[0]
}
override fun findAll(): List<LocalContact> = queryContacts(RawContacts.DELETED + "== 0", null)
/**
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
* @throws RemoteException on content provider errors
*/
override fun findDeleted() =
if (includeGroups)
findDeletedContacts() + findDeletedGroups()
else
findDeletedContacts()
fun findDeletedContacts() = queryContacts("${RawContacts.DELETED}!=0", null)
fun findDeletedGroups() = queryGroups("${Groups.DELETED}!=0", null)
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
* @throws RemoteException on content provider errors
*/
override fun findDirty() =
if (includeGroups)
findDirtyContacts() + findDirtyGroups()
else
findDirtyContacts()
fun findDirtyContacts() = queryContacts("${RawContacts.DIRTY}!=0", null)
fun findDirtyGroups() = queryGroups("${Groups.DIRTY}!=0", null)
/**
* Returns an array of local contacts which don't have a file name yet.
*/
override fun findWithoutFileName() =
if (includeGroups)
findWithoutFileNameContacts() + findWithoutFileNameGroups()
else
findWithoutFileNameContacts()
fun findWithoutFileNameContacts() = queryContacts("${AndroidContact.COLUMN_FILENAME} IS NULL", null)
fun findWithoutFileNameGroups() = queryGroups("${AndroidGroup.COLUMN_FILENAME} IS NULL", null)
/**
* Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e.
@ -192,52 +246,39 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
* whose contact data checksum has not changed.
* @return number of "really dirty" contacts
*/
@Throws(ContactsStorageException::class)
fun verifyDirty(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
App.log.severe("verifyDirty() 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("verifyDirty() should not be called on Android != 7")
var reallyDirty = 0
for (contact in dirtyContacts) {
try {
val lastHash = contact.lastHashCode
val currentHash = contact.dataHashCode()
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
contact.resetDirty()
} else {
App.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
reallyDirty++
}
} catch (e: FileNotFoundException) {
throw ContactsStorageException("Couldn't calculate hash code", e)
for (contact in findDirtyContacts()) {
val lastHash = contact.getLastHashCode()
val currentHash = contact.dataHashCode()
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
contact.resetDirty()
} else {
App.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
reallyDirty++
}
}
if (includeGroups)
reallyDirty += dirtyGroups.size
reallyDirty += findDirtyGroups().size
return reallyDirty
}
@Throws(ContactsStorageException::class)
override fun getByUid(uid: String): LocalResource? {
val ret = queryContacts(AndroidContact.COLUMN_FILENAME + " =? ", arrayOf(uid)) as Array<LocalContact>
return if (ret != null && ret.size > 0) {
ret[0]
} else null
}
override fun findByUid(uid: String): LocalAddress? = findContactByUID(uid)
@Throws(ContactsStorageException::class)
override fun count(): Long {
try {
val cursor = provider.query(syncAdapterURI(RawContacts.CONTENT_URI), null, null, null, null)
val cursor = provider?.query(syncAdapterURI(RawContacts.CONTENT_URI), null, null, null, null)
try {
return cursor.count.toLong()
return cursor?.count?.toLong()!!
} finally {
cursor.close()
cursor?.close()
}
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't query contacts", e)
@ -245,98 +286,67 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
}
@Throws(ContactsStorageException::class)
internal fun getByGroupMembership(groupID: Long): Array<LocalContact> {
fun deleteAll() {
try {
val cursor = provider.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()), null)
val ids = HashSet<Long>()
while (cursor != null && cursor.moveToNext())
ids.add(cursor.getLong(0))
cursor!!.close()
val contacts = arrayOfNulls<LocalContact>(ids.size)
var i = 0
for (id in ids)
contacts[i++] = LocalContact(this, id, null, null)
return contacts
provider?.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null)
provider?.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't query contacts", e)
throw ContactsStorageException("Couldn't delete all local contacts and groups", e)
}
}
@Throws(ContactsStorageException::class)
fun deleteAll() {
try {
provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null)
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't delete all local contacts and groups", e)
/* special group operations */
fun getByGroupMembership(groupID: Long): List<LocalContact> {
val ids = HashSet<Long>()
provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
"(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?) OR (${CachedGroupMembership.MIMETYPE}=? AND ${CachedGroupMembership.GROUP_ID}=?)",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()),
null)?.use { cursor ->
while (cursor.moveToNext())
ids += cursor.getLong(0)
}
return ids.map { findContactByID(it) }
}
/* special group operations */
/**
* Finds the first group with the given title. If there is no group with this
* title, a new group is created.
* @param title title of the group to look for
* @return id of the group with given title
* @throws ContactsStorageException on contact provider errors
* @param title title of the group to look for
* @return id of the group with given title
* @throws RemoteException on content provider errors
*/
@Throws(ContactsStorageException::class)
fun findOrCreateGroup(title: String): Long {
try {
val cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
arrayOf(Groups._ID),
Groups.TITLE + "=?", arrayOf(title), null)
try {
if (cursor != null && cursor.moveToNext())
return cursor.getLong(0)
} finally {
cursor!!.close()
}
val values = ContentValues()
values.put(Groups.TITLE, title)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
return ContentUris.parseId(uri)
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't find local contact group", e)
provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID),
"${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor ->
if (cursor.moveToNext())
return cursor.getLong(0)
}
val values = ContentValues(1)
values.put(Groups.TITLE, title)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
return ContentUris.parseId(uri)
}
@Throws(ContactsStorageException::class)
fun removeEmptyGroups() {
// find groups without members
/** should be done using [Groups.SUMMARY_COUNT], but it's not implemented in Android yet */
for (group in queryGroups(null, null) as Array<LocalGroup>)
if (group.members.size == 0) {
App.log.log(Level.FINE, "Deleting group", group)
group.delete()
}
}
@Throws(ContactsStorageException::class)
fun removeGroups() {
try {
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't remove all groups", e)
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
queryGroups(null, null).filter { it.getMembers().isEmpty() }.forEach { group ->
App.log.log(Level.FINE, "Deleting group", group)
group.delete()
}
}
/** Fix all of the etags of all of the non-dirty contacts to be non-null.
* Currently set to all ones. */
@Throws(ContactsStorageException::class)
fun fixEtags() {
val newEtag = "1111111111111111111111111111111111111111111111111111111111111111"
val where = ContactsContract.RawContacts.DIRTY + "=0 AND " + AndroidContact.COLUMN_ETAG + " IS NULL"
@ -344,7 +354,7 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
val values = ContentValues(1)
values.put(AndroidContact.COLUMN_ETAG, newEtag)
try {
val fixed = provider.update(syncAdapterURI(RawContacts.CONTENT_URI),
val fixed = provider?.update(syncAdapterURI(RawContacts.CONTENT_URI),
values, where, null)
App.log.info("Fixed entries: " + fixed.toString())
} catch (e: RemoteException) {
@ -352,81 +362,4 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
}
}
companion object {
protected val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
protected val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
protected val USER_DATA_URL = "url"
@Throws(ContactsStorageException::class)
fun find(context: Context, provider: ContentProviderClient, mainAccount: Account?): Array<LocalAddressBook> {
val accountManager = AccountManager.get(context)
val result = LinkedList<LocalAddressBook>()
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, account, provider)
if (mainAccount == null || addressBook.mainAccount == mainAccount)
result.add(addressBook)
}
return result.toTypedArray()
}
@Throws(ContactsStorageException::class)
fun findByUid(context: Context, provider: ContentProviderClient, mainAccount: Account?, uid: String): LocalAddressBook? {
val accountManager = AccountManager.get(context)
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, account, provider)
if (addressBook.url == uid && (mainAccount == null || addressBook.mainAccount == mainAccount))
return addressBook
}
return null
}
@Throws(ContactsStorageException::class)
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, journalEntity: JournalEntity): LocalAddressBook {
val info = journalEntity.info
val accountManager = AccountManager.get(context)
val account = Account(accountName(mainAccount, info), App.addressBookAccountType)
if (!accountManager.addAccountExplicitly(account, null, null))
throw ContactsStorageException("Couldn't create address book account")
setUserData(accountManager, account, mainAccount, info.uid!!)
val addressBook = LocalAddressBook(context, account, provider)
addressBook.mainAccount = mainAccount
addressBook.url = info.uid
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
return addressBook
}
// SETTINGS
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
fun setUserData(accountManager: AccountManager, account: Account, mainAccount: Account, url: String) {
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
accountManager.setUserData(account, USER_DATA_URL, url)
}
// HELPERS
fun accountName(mainAccount: Account, info: CollectionInfo): String {
val displayName = if (info.displayName != null) info.displayName else info.uid
val sb = StringBuilder(displayName)
sb.append(" (")
.append(mainAccount.name)
.append(" ")
.append(info.uid!!.substring(0, 4))
.append(")")
return sb.toString()
}
}
}

@ -13,82 +13,114 @@ import android.content.ContentProviderClient
import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import android.provider.CalendarContract.Reminders
import android.text.TextUtils
import android.provider.CalendarContract.*
import at.bitfire.ical4android.*
import com.etesync.syncadapter.App
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import org.apache.commons.lang3.StringUtils
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
import net.fortuna.ical4j.model.component.VTimeZone
class LocalCalendar private constructor(
account: Account,
provider: ContentProviderClient,
id: Long
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
import org.apache.commons.lang3.StringUtils
companion object {
val defaultColor = -0x743cb6 // light green 500
import java.io.FileNotFoundException
import java.util.LinkedList
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidCalendarFactory
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.DateUtils
class LocalCalendar protected constructor(account: Account, provider: ContentProviderClient, id: Long) : AndroidCalendar(account, provider, LocalEvent.Factory.INSTANCE, id), LocalCollection<LocalEvent> {
override val deleted: Array<LocalEvent>
@Throws(CalendarStorageException::class)
get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
override val withoutFileName: Array<LocalEvent>
@Throws(CalendarStorageException::class)
get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
val all: Array<LocalEvent>
@Throws(CalendarStorageException::class)
get() = queryEvents(null, null) as Array<LocalEvent>
override// get dirty events which are required to have an increased SEQUENCE value
// sequence has not been assigned yet (i.e. this event was just locally created)
val dirty: Array<LocalEvent>
@Throws(CalendarStorageException::class, FileNotFoundException::class)
get() {
val dirty = LinkedList<LocalEvent>()
for (event in queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>) {
if (event.event.sequence == null)
event.event.sequence = 0
else if (event.weAreOrganizer)
event.event.sequence++
dirty.add(event)
}
val COLUMN_CTAG = Calendars.CAL_SYNC1
fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri {
val values = valuesFromCollectionInfo(journalEntity, true)
return dirty.toTypedArray()
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
values.put(Calendars.ACCOUNT_NAME, account.name)
values.put(Calendars.ACCOUNT_TYPE, account.type)
values.put(Calendars.OWNER_ACCOUNT, account.name)
// flag as visible & synchronizable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1)
values.put(Calendars.SYNC_EVENTS, 1)
return AndroidCalendar.create(account, provider, values)
}
override fun eventBaseInfoColumns(): Array<String> {
return BASE_INFO_COLUMNS
}
fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar?
= AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull()
@Throws(CalendarStorageException::class)
fun update(journalEntity: JournalEntity, updateColor: Boolean) {
update(valuesFromCollectionInfo(journalEntity, updateColor))
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
val info = journalEntity.info
val values = ContentValues()
values.put(Calendars.NAME, info.uid)
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName)
if (withColor)
values.put(Calendars.CALENDAR_COLOR, if (info.color != null) info.color else defaultColor)
if (journalEntity.isReadOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
else {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
}
info.timeZone?.let { tzData ->
try {
val timeZone = DateUtils.parseVTimeZone(tzData)
timeZone.timeZoneId?.let { tzId ->
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId.value))
}
} catch(e: IllegalArgumentException) {
App.log.log(Level.WARNING, "Couldn't parse calendar default time zone", e)
}
}
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
return values
}
}
@Throws(CalendarStorageException::class)
override fun getByUid(uid: String): LocalEvent? {
val ret = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)) as Array<LocalEvent>
return if (ret != null && ret.size > 0) {
ret[0]
} else null
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
update(valuesFromCollectionInfo(journalEntity, updateColor))
override fun findDeleted() =
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
override fun findDirty(): List<LocalEvent> {
val dirty = LinkedList<LocalEvent>()
// get dirty events which are required to have an increased SEQUENCE value
for (localEvent in queryEvents("${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)) {
val event = localEvent.event!!
val sequence = event.sequence
if (event.sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
event.sequence = 0
else if (localEvent.weAreOrganizer)
event.sequence = sequence!! + 1
dirty += localEvent
}
return dirty
}
@Throws(CalendarStorageException::class)
override fun findWithoutFileName(): List<LocalEvent>
= queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null)
override fun findAll(): List<LocalEvent>
= queryEvents(null, null)
override fun findByUid(uid: String): LocalEvent?
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
fun processDirtyExceptions() {
// process deleted exceptions
App.log.info("Processing deleted exceptions")
@ -163,19 +195,15 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
}
@Throws(CalendarStorageException::class)
override fun count(): Long {
val where = Events.CALENDAR_ID + "=?"
val whereArgs = arrayOf(id.toString())
try {
val cursor = provider.query(
syncAdapterURI(Events.CONTENT_URI), null,
where, whereArgs, null)
Events.CALENDAR_ID + "=?", arrayOf(id.toString()), null)
try {
return cursor.count.toLong()
return cursor?.count?.toLong()!!
} finally {
cursor.close()
cursor?.close()
}
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't query calendar events", e)
@ -183,20 +211,6 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
}
class Factory : AndroidCalendarFactory {
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long): AndroidCalendar {
return LocalCalendar(account, provider, id)
}
override fun newArray(size: Int): Array<AndroidCalendar?> {
return arrayOfNulls<LocalCalendar>(size) as Array<AndroidCalendar?>
}
companion object {
val INSTANCE = Factory()
}
}
/** Fix all of the etags of all of the non-dirty events to be non-null.
* Currently set to all ones.. */
@ -218,67 +232,9 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
}
companion object {
val defaultColor = -0x743cb6 // light green 500
val COLUMN_CTAG = Calendars.CAL_SYNC1
object Factory: AndroidCalendarFactory<LocalCalendar> {
internal var BASE_INFO_COLUMNS = arrayOf(Events._ID, Events._SYNC_ID, LocalEvent.COLUMN_ETAG)
@Throws(CalendarStorageException::class)
fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri {
val values = valuesFromCollectionInfo(journalEntity, true)
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
values.put(Calendars.ACCOUNT_NAME, account.name)
values.put(Calendars.ACCOUNT_TYPE, account.type)
values.put(Calendars.OWNER_ACCOUNT, account.name)
// flag as visible & synchronizable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1)
values.put(Calendars.SYNC_EVENTS, 1)
return AndroidCalendar.create(account, provider, values)
}
@Throws(FileNotFoundException::class, CalendarStorageException::class)
fun findByName(account: Account, provider: ContentProviderClient, factory: AndroidCalendarFactory, name: String): LocalCalendar? {
val ret = LocalCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name))
if (ret.size == 1) {
return ret[0]
} else {
App.log.severe("No calendar found for name $name")
return null
}
}
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
val info = journalEntity.info
val values = ContentValues()
values.put(Calendars.NAME, info.uid)
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName)
if (withColor)
values.put(Calendars.CALENDAR_COLOR, if (info.color != null) info.color else defaultColor)
if (journalEntity.isReadOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
else {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
}
if (!TextUtils.isEmpty(info.timeZone)) {
val timeZone = DateUtils.parseVTimeZone(info.timeZone)
if (timeZone != null && timeZone.timeZoneId != null)
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.timeZoneId.value))
}
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
return values
}
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
LocalCalendar(account, provider, id)
}
}

@ -8,21 +8,14 @@
package com.etesync.syncadapter.resource
import java.io.FileNotFoundException
interface LocalCollection<out T: LocalResource<*>> {
fun findDeleted(): List<T>
fun findDirty(): List<T>
fun findWithoutFileName(): List<T>
fun findAll(): List<T>
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
fun findByUid(uid: String): T?
interface LocalCollection<T> {
val deleted: Array<T>
val withoutFileName: Array<T>
/** Dirty *non-deleted* entries */
val dirty: Array<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getByUid(uid: String): T?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun count(): Long
}

@ -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,97 +76,64 @@ 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")
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, id: Long, uuid: String?, eTag: String?) : super(addressBook, id, uuid, eTag) {}
constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
: super(addressBook, values) {}
constructor(addressBook: AndroidAddressBook, contact: Contact, uuid: String?, eTag: String?) : super(addressBook, contact, uuid, eTag) {}
constructor(addressBook: AndroidAddressBook<LocalContact, *>, 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) {
// 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)
val values = ContentValues(3)
values.put(AndroidContact.COLUMN_ETAG, eTag)
values.put(ContactsContract.RawContacts.DIRTY, 0)
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)
if (contact.unknownProperties != null) {
if (contact?.unknownProperties != null) {
val op: BatchOperation.Operation
val builder = ContentProviderOperation.newInsert(dataSyncURI())
if (id == null) {
@ -170,22 +143,20 @@ class LocalContact : AndroidContact, LocalResource {
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
}
builder.withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact?.unknownProperties)
batch.enqueue(op)
}
}
@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)
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)
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))
}
}
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<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)
}
override fun newArray(size: Int): Array<LocalContact?> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
object Factory: AndroidContactFactory<LocalContact> {
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
LocalContact(addressBook, values)
}
companion object {
init {
Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION
}
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
}
}

@ -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) {
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)
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
}
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 = row.getAsInteger(COLUMN_SEQUENCE)
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
}
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
@ -100,7 +90,7 @@ class LocalEvent : AndroidEvent, LocalResource {
val buildException = recurrence != null
val eventToBuild = if (buildException) recurrence else event
builder.withValue(COLUMN_UID, event.uid)
builder.withValue(COLUMN_UID, event?.uid)
.withValue(COLUMN_SEQUENCE, eventToBuild?.sequence)
.withValue(CalendarContract.Events.DIRTY, if (saveAsDirty) 1 else 0)
.withValue(CalendarContract.Events.DELETED, 0)
@ -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()
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)
fileName = newFileName
if (event != null)
event.uid = uid
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't update UID", e)
}
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
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(COLUMN_UID, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
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)
}
}
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)
}
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)
override fun newArray(size: Int): Array<AndroidEvent?> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
this.eTag = eTag
}
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)
}
}

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

@ -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)
}

@ -10,143 +10,98 @@ 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)
}
/* process LocalTask-specific fields */
override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
super.buildTask(builder, update)
builder.withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task.uid)
.withValue(COLUMN_SEQUENCE, task.sequence)
builder.withValue(TaskContract.Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task?.uid)
.withValue(COLUMN_SEQUENCE, task?.sequence)
.withValue(COLUMN_ETAG, eTag)
}
/* 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)
}
}
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)
}
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)
companion object {
val INSTANCE = Factory()
}
this.eTag = eTag
}
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)
}
}

@ -9,161 +9,106 @@
package com.etesync.syncadapter.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.RemoteException
import com.etesync.syncadapter.model.CollectionInfo
import org.dmfs.provider.tasks.TaskContract.TaskLists
import org.dmfs.provider.tasks.TaskContract.Tasks
import java.io.FileNotFoundException
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.AndroidTaskListFactory
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
class LocalTaskList protected constructor(account: Account, provider: TaskProvider, id: Long) : AndroidTaskList(account, provider, LocalTask.Factory.INSTANCE, id), LocalCollection<LocalTask> {
override val deleted: Array<LocalTask>
@Throws(CalendarStorageException::class)
get() = queryTasks(Tasks._DELETED + "!=0", null) as Array<LocalTask>
override val withoutFileName: Array<LocalTask>
@Throws(CalendarStorageException::class)
get() = queryTasks(Tasks._SYNC_ID + " IS NULL", null) as Array<LocalTask>
override// sequence has not been assigned yet (i.e. this task was just locally created)
val dirty: Array<LocalTask>
@Throws(CalendarStorageException::class, FileNotFoundException::class)
get() {
val tasks = queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null) as Array<LocalTask>
for (task in tasks) {
if (task.task.sequence == null)
task.task.sequence = 0
else
task.task.sequence++
}
return tasks
}
override fun taskBaseInfoColumns(): Array<String> {
return BASE_INFO_COLUMNS
}
@Throws(CalendarStorageException::class)
fun update(info: CollectionInfo, updateColor: Boolean) {
update(valuesFromCollectionInfo(info, updateColor))
}
@Throws(CalendarStorageException::class)
override fun getByUid(uid: String): LocalTask? {
val ret = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)) as Array<LocalTask>
return if (ret != null && ret.size > 0) {
ret[0]
} else null
}
@Throws(CalendarStorageException::class)
override fun count(): Long {
val where = Tasks.LIST_ID + "=?"
val whereArgs = arrayOf(id.toString())
try {
val cursor = provider.client.query(
syncAdapterURI(provider.tasksUri()), null,
where, whereArgs, null)
try {
return cursor.count.toLong()
} finally {
cursor.close()
}
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't query calendar events", e)
}
}
class Factory : AndroidTaskListFactory {
override fun newInstance(account: Account, provider: TaskProvider, id: Long): AndroidTaskList {
return LocalTaskList(account, provider, id)
}
override fun newArray(size: Int): Array<AndroidTaskList?> {
return arrayOfNulls<LocalTaskList>(size) as Array<AndroidTaskList?>
}
companion object {
val INSTANCE = Factory()
}
}
import com.etesync.syncadapter.model.JournalEntity
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
class LocalTaskList private constructor(
account: Account,
provider: TaskProvider,
id: Long
): AndroidTaskList<LocalTask>(account, provider, LocalTask.Factory, id), LocalCollection<LocalTask> {
companion object {
val defaultColor = -0x3c1592 // "DAVdroid green"
val COLUMN_CTAG = TaskLists.SYNC_VERSION
internal var BASE_INFO_COLUMNS = arrayOf(Tasks._ID, Tasks._SYNC_ID, LocalTask.COLUMN_ETAG)
fun tasksProviderAvailable(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
else {
val provider = TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)
provider?.use { return true }
return false
}
}
@Throws(CalendarStorageException::class)
fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri {
val values = valuesFromCollectionInfo(info, true)
fun create(account: Account, provider: TaskProvider, journalEntity: JournalEntity): Uri {
val values = valuesFromCollectionInfo(journalEntity, true)
values.put(TaskLists.OWNER, account.name)
values.put(TaskLists.SYNC_ENABLED, 1)
values.put(TaskLists.VISIBLE, 1)
return AndroidTaskList.create(account, provider, values)
return create(account, provider, values)
}
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
val values = ContentValues()
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
val info = journalEntity.info
val values = ContentValues(3)
values.put(TaskLists._SYNC_ID, info.uid)
values.put(TaskLists.LIST_NAME, info.displayName)
values.put(TaskLists.LIST_NAME, if (info.displayName.isNullOrBlank()) info.uid else info.displayName)
if (withColor)
values.put(TaskLists.LIST_COLOR, if (info.color != null) info.color else defaultColor)
values.put(TaskLists.LIST_COLOR, info.color ?: defaultColor)
return values
}
// helpers
}
fun tasksProviderAvailable(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
else {
val provider = TaskProvider.acquire(context.contentResolver, TaskProvider.ProviderName.OpenTasks)
try {
return provider != null
} finally {
provider?.close()
}
}
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
update(valuesFromCollectionInfo(journalEntity, updateColor))
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
override fun findDirty(): List<LocalTask> {
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
for (localTask in tasks) {
val task = requireNotNull(localTask.task)
val sequence = task.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.sequence = 0
else
task.sequence = sequence + 1
}
return tasks
}
override fun findAll(): List<LocalTask>
= queryTasks(null, null)
// HELPERS
override fun findWithoutFileName(): List<LocalTask>
= queryTasks(Tasks._SYNC_ID + " IS NULL", null)
@Throws(RemoteException::class)
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
val client = resolver.acquireContentProviderClient(TaskProvider.ProviderName.OpenTasks.authority)
if (client != null) {
val values = ContentValues(1)
values.put(Tasks.ACCOUNT_NAME, newName)
client.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), values, Tasks.ACCOUNT_NAME + "=?", arrayOf(oldName))
client.release()
override fun findByUid(uid: String): LocalTask?
= queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
override fun count(): Long {
try {
val cursor = provider.client.query(
TaskProvider.syncAdapterUri(provider.tasksUri(), account), null,
Tasks.LIST_ID + "=?", arrayOf(id.toString()), null)
try {
return cursor?.count?.toLong()!!
} finally {
cursor?.close()
}
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't query calendar events", e)
}
}
object Factory: AndroidTaskListFactory<LocalTaskList> {
override fun newInstance(account: Account, provider: TaskProvider, id: Long) =
LocalTaskList(account, provider, id)
}
}

@ -108,10 +108,6 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
notificationManager.notify(title, context.getString(syncPhase))
} catch (e: OutOfMemoryError) {
if (e is ContactsStorageException || e is SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e)
syncResult.databaseError = true
}
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_contacts, account.name)
notificationManager.setThrowable(e)

@ -38,12 +38,9 @@ import org.apache.commons.codec.Charsets
import java.io.ByteArrayInputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.ArrayList
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
@ -51,25 +48,25 @@ import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.resource.LocalCollection
import okhttp3.HttpUrl
import java.io.StringReader
/**
*
* Synchronization manager for CardDAV collections; handles contacts and groups.
*/
class CalendarSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, result: SyncResult, calendar: LocalCalendar, private val remote: HttpUrl) : SyncManager(context, account, settings, extras, authority, result, calendar.name, CollectionInfo.Type.CALENDAR, account.name) {
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, result: SyncResult, calendar: LocalCalendar, private val remote: HttpUrl) : SyncManager<LocalEvent>(context, account, settings, extras, authority, result, calendar.name!!, CollectionInfo.Type.CALENDAR, account.name) {
protected override val syncErrorTitle: String
override val syncErrorTitle: String
get() = context.getString(R.string.sync_error_calendar, account.name)
protected override val syncSuccessfullyTitle: String
override val syncSuccessfullyTitle: String
get() = context.getString(R.string.sync_successfully_calendar, info.displayName,
account.name)
init {
localCollection = calendar as LocalCollection<LocalResource>
localCollection = calendar
}
override fun notificationId(): Int {
@ -81,7 +78,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
if (!super.prepare())
return false
journal = JournalEntryManager(httpClient, remote, localCalendar().name)
journal = JournalEntryManager(httpClient, remote, localCalendar().name!!)
return true
}
@ -101,9 +98,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
override fun processSyncEntry(cEntry: SyncEntry) {
val `is` = ByteArrayInputStream(cEntry.content.toByteArray(Charsets.UTF_8))
val inputReader = StringReader(cEntry.content)
val events = Event.fromStream(`is`, Charsets.UTF_8)
val events = Event.fromReader(inputReader)
if (events.size == 0) {
App.log.warning("Received VCard without data, ignoring")
return
@ -112,7 +109,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
val event = events[0]
val local = localCollection!!.getByUid(event.uid) as LocalEvent?
val local = localCollection!!.findByUid(event.uid!!)
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processEvent(event, local)
@ -140,7 +137,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
for (local in localDirty) {
val event = (local as LocalEvent).event
if (event.attendees.isEmpty()) {
if (event?.attendees?.isEmpty()!!) {
return
}
createInviteAttendeesNotification(event, local.content)
@ -148,7 +145,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
private fun createInviteAttendeesNotification(event: Event, icsContent: String) {
val notificationHelper = NotificationHelper(context, event.uid, event.uid.hashCode())
val notificationHelper = NotificationHelper(context, event.uid!!, event.uid!!.hashCode())
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_EMAIL, getEmailAddresses(event.attendees, false))
@ -156,14 +153,14 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
intent.putExtra(Intent.EXTRA_SUBJECT,
context.getString(R.string.sync_calendar_attendees_email_subject,
event.summary,
dateFormatDate.format(event.dtStart.date)))
dateFormatDate.format(event.dtStart?.date)))
intent.putExtra(Intent.EXTRA_TEXT,
context.getString(R.string.sync_calendar_attendees_email_content,
event.summary,
formatEventDates(event),
if (event.location != null) event.location else "",
formatAttendees(event.attendees)))
val uri = createAttachmentFromString(context, event.uid, icsContent)
val uri = createAttachmentFromString(context, event.uid!!, icsContent)
if (uri == null) {
App.log.severe("Unable to create attachment from calendar event")
return
@ -179,7 +176,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
private fun processEvent(newData: Event, localEvent: LocalEvent?): LocalResource {
private fun processEvent(newData: Event, localEvent: LocalEvent?): LocalEvent {
var localEvent = localEvent
// delete local event, if it exists
if (localEvent != null) {
@ -221,23 +218,23 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
private fun formatEventDates(event: Event): String {
val locale = Locale.getDefault()
val timezone = if (event.dtStart.timeZone != null) event.dtStart.timeZone else TimeZone.getTimeZone("UTC")
val dateFormatString = if (event.isAllDay) "EEEE, MMM dd" else "EEEE, MMM dd @ hh:mm a"
val timezone = if (event.dtStart?.timeZone != null) event.dtStart?.timeZone else TimeZone.getTimeZone("UTC")
val dateFormatString = if (event.isAllDay()) "EEEE, MMM dd" else "EEEE, MMM dd @ hh:mm a"
val longDateFormat = SimpleDateFormat(dateFormatString, locale)
longDateFormat.timeZone = timezone
val shortDateFormat = SimpleDateFormat("hh:mm a", locale)
shortDateFormat.timeZone = timezone
val startDate = event.dtStart.date
val startDate = event.dtStart?.date
val endDate = event.getEndDate(true)!!.date
val tzName = timezone.getDisplayName(timezone.inDaylightTime(startDate), TimeZone.SHORT)
val tzName = timezone?.getDisplayName(timezone?.inDaylightTime(startDate)!!, TimeZone.SHORT)
val cal1 = Calendar.getInstance()
val cal2 = Calendar.getInstance()
cal1.time = startDate
cal2.time = endDate
val sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
if (sameDay && event.isAllDay) {
if (sameDay && event.isAllDay()) {
return longDateFormat.format(startDate)
}
return if (sameDay)

@ -17,6 +17,7 @@ import android.content.SyncResult
import android.database.sqlite.SQLiteException
import android.os.Bundle
import android.provider.CalendarContract
import at.bitfire.ical4android.AndroidCalendar
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
@ -67,7 +68,7 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
val principal = HttpUrl.get(settings.uri!!)!!
for (calendar in LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null) as Array<LocalCalendar>) {
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
App.log.info("Synchronizing calendar #" + calendar.id + ", URL: " + calendar.name)
val syncManager = CalendarSyncManager(context, account, settings, extras, authority, syncResult, calendar, principal)
syncManager.performSync()
@ -95,10 +96,6 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
notificationManager.notify(title, context.getString(syncPhase))
} catch (e: OutOfMemoryError) {
if (e is CalendarStorageException || e is SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e)
syncResult.databaseError = true
}
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_calendar, account.name)
notificationManager.setThrowable(e)
@ -121,7 +118,7 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
remote[journalEntity.uid] = journalEntity
}
val local = LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null) as Array<LocalCalendar>
val local = AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null)
val updateColors = settings.manageCalendarColors

@ -29,37 +29,30 @@ import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.SyncEntry
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalContact
import com.etesync.syncadapter.resource.LocalGroup
import com.etesync.syncadapter.resource.LocalResource
import org.apache.commons.codec.Charsets
import org.apache.commons.collections4.SetUtils
import org.apache.commons.io.IOUtils
import java.io.ByteArrayInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.util.logging.Level
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.resource.*
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import java.io.StringReader
/**
*
* Synchronization manager for CardDAV collections; handles contacts and groups.
*/
class ContactsSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class, ContactsStorageException::class)
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, private val provider: ContentProviderClient, result: SyncResult, localAddressBook: LocalAddressBook, private val remote: HttpUrl) : SyncManager(context, account, settings, extras, authority, result, localAddressBook.url!!, CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.mainAccount.name) {
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, private val provider: ContentProviderClient, result: SyncResult, localAddressBook: LocalAddressBook, private val remote: HttpUrl) : SyncManager<LocalAddress>(context, account, settings, extras, authority, result, localAddressBook.url!!, CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.mainAccount.name) {
protected override val syncErrorTitle: String
get() = context.getString(R.string.sync_error_contacts, account.name)
@ -85,7 +78,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val reallyDirty = localAddressBook.verifyDirty()
val deleted = localAddressBook.deleted.size
val deleted = localAddressBook.findDeleted().size
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
App.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
return false
@ -96,7 +89,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
localAddressBook.updateSettings(values)
localAddressBook.settings.putAll(values)
journal = JournalEntryManager(httpClient, remote, localAddressBook.url!!)
@ -114,12 +107,12 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
/* groups as separate VCards: there are group contacts and individual contacts */
// mark groups with changed members as dirty
val batch = BatchOperation(addressBook.provider)
for (contact in addressBook.dirtyContacts) {
val batch = BatchOperation(addressBook.provider!!)
for (contact in addressBook.findDirtyContacts()) {
try {
App.log.fine("Looking for changed group memberships of contact " + contact.fileName)
val cachedGroups = contact.cachedGroupMemberships
val currentGroups = contact.groupMemberships
val cachedGroups = contact.getCachedGroupMemberships()
val currentGroups = contact.getGroupMemberships()
for (groupID in SetUtils.disjunction(cachedGroups, currentGroups)) {
App.log.fine("Marking group as dirty: " + groupID!!)
batch.enqueue(BatchOperation.Operation(
@ -152,10 +145,10 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
override fun processSyncEntry(cEntry: SyncEntry) {
val `is` = ByteArrayInputStream(cEntry.content.toByteArray(Charsets.UTF_8))
val inputReader = StringReader(cEntry.content)
val downloader = ResourceDownloader(context)
val contacts = Contact.fromStream(`is`, Charsets.UTF_8, downloader)
val contacts = Contact.fromReader(inputReader, downloader)
if (contacts.size == 0) {
App.log.warning("Received VCard without data, ignoring")
return
@ -163,14 +156,13 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
App.log.warning("Received multiple VCards, using first one")
val contact = contacts[0]
val local = localCollection!!.getByUid(contact.uid) as LocalResource?
val local = localCollection!!.findByUid(contact.uid!!)
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processContact(contact, local)
} else {
if (local != null) {
App.log.info("Removing local record #" + local.id + " which has been deleted on the server")
App.log.info("Removing local record which has been deleted on the server")
local.delete()
} else {
App.log.warning("Tried deleting a non-existent record: " + contact.uid)
@ -179,7 +171,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
@Throws(IOException::class, ContactsStorageException::class)
private fun processContact(newData: Contact, local: LocalResource?): LocalResource {
private fun processContact(newData: Contact, local: LocalAddress?): LocalAddress {
var local = local
val uuid = newData.uid
// update local contact, if it exists
@ -188,14 +180,14 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
if (local is LocalGroup && newData.group) {
// update group
val group = local as LocalGroup?
val group: LocalGroup = local
group!!.eTag = uuid
group.updateFromServer(newData)
group.update(newData)
syncResult.stats.numUpdates++
} else if (local is LocalContact && !newData.group) {
// update contact
val contact = local as LocalContact?
val contact: LocalContact = local
contact!!.eTag = uuid
contact.update(newData)
syncResult.stats.numUpdates++
@ -216,13 +208,13 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
if (newData.group) {
App.log.log(Level.INFO, "Creating local group", newData.uid)
val group = LocalGroup(localAddressBook(), newData, uuid, uuid)
group.create()
group.add()
local = group
} else {
App.log.log(Level.INFO, "Creating local contact", newData.uid)
val contact = LocalContact(localAddressBook(), newData, uuid, uuid)
contact.create()
contact.add()
local = contact
}
@ -272,15 +264,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
val body = response.body()
if (body != null) {
val stream = body.byteStream()
try {
if (response.isSuccessful && stream != null) {
return IOUtils.toByteArray(stream)
} else
App.log.severe("Couldn't download external resource")
} finally {
stream?.close()
}
return body.bytes()
}
} catch (e: IOException) {
App.log.log(Level.SEVERE, "Couldn't download external resource", e)

@ -55,12 +55,12 @@ import okhttp3.OkHttpClient
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.model.SyncEntry.Actions.ADD
abstract class SyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
abstract class SyncManager<T: LocalResource<*>> @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
constructor(protected val context: Context, protected val account: Account, protected val settings: AccountSettings, protected val extras: Bundle, protected val authority: String, protected val syncResult: SyncResult, journalUid: String, protected val serviceType: CollectionInfo.Type, accountName: String) {
protected val notificationManager: NotificationHelper
protected val info: CollectionInfo
protected var localCollection: LocalCollection<LocalResource>? = null
protected var localCollection: LocalCollection<T>? = null
protected var httpClient: OkHttpClient
@ -89,8 +89,8 @@ constructor(protected val context: Context, protected val account: Account, prot
/**
* Dirty and deleted resources. We need to save them so we safely ignore ones that were added after we started.
*/
private var localDeleted: List<LocalResource>? = null
protected var localDirty: Array<LocalResource> = arrayOf()
private var localDeleted: List<T>? = null
protected var localDirty: List<T> = LinkedList()
protected abstract val syncErrorTitle: String
@ -227,8 +227,6 @@ constructor(protected val context: Context, protected val account: Account, prot
} catch (e: OutOfMemoryError) {
if (e is Exceptions.HttpException) {
syncResult.stats.numParseExceptions++
} else if (e is CalendarStorageException || e is ContactsStorageException) {
syncResult.databaseError = true
} else {
syncResult.stats.numParseExceptions++
}
@ -400,7 +398,7 @@ constructor(protected val context: Context, protected val account: Account, prot
local.delete()
}
if (left > 0) {
localDeleted?.drop(left)
localDeleted = localDeleted?.drop(left)
}
left = pushed
@ -412,7 +410,7 @@ constructor(protected val context: Context, protected val account: Account, prot
local.clearDirty(local.uuid!!)
}
if (left > 0) {
localDirty = Arrays.copyOfRange(localDirty, left, localDirty.size)
localDirty = localDirty.drop(left)
}
if (pushed > 0) {
@ -450,7 +448,7 @@ constructor(protected val context: Context, protected val account: Account, prot
val entry = SyncEntry(local.content, action)
val tmp = JournalEntryManager.Entry()
tmp.update(crypto, entry.toJson(), previousEntry!!)
tmp.update(crypto, entry.toJson(), previousEntry)
previousEntry = tmp
localEntries!!.add(previousEntry)
@ -467,7 +465,7 @@ constructor(protected val context: Context, protected val account: Account, prot
remoteCTag = journalEntity.getLastUid(data)
localDeleted = processLocallyDeleted()
localDirty = localCollection!!.dirty
localDirty = localCollection!!.findDirty()
// This is done after fetching the local dirty so all the ones we are using will be prepared
prepareDirty()
}
@ -478,9 +476,9 @@ constructor(protected val context: Context, protected val account: Account, prot
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
*/
@Throws(CalendarStorageException::class, ContactsStorageException::class)
private fun processLocallyDeleted(): List<LocalResource> {
val localList = localCollection!!.deleted
val ret = ArrayList<LocalResource>(localList.size)
private fun processLocallyDeleted(): List<T> {
val localList = localCollection!!.findDeleted()
val ret = ArrayList<T>(localList.size)
for (local in localList) {
if (Thread.interrupted())
@ -504,7 +502,7 @@ constructor(protected val context: Context, protected val account: Account, prot
continue
}
App.log.fine("Found local record #" + local.id + " without file name; generating file name/UID if necessary")
App.log.fine("Found local record without file name; generating file name/UID if necessary")
local.prepareForUpload()
}
}

@ -32,7 +32,6 @@ import com.etesync.syncadapter.BuildConfig
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import ezvcard.Ezvcard
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.time.DateFormatUtils
import java.io.IOException
import java.util.logging.Level
@ -143,9 +142,9 @@ class AboutActivity : BaseActivity() {
override fun loadInBackground(): Spanned? {
App.log.fine("Loading license file $fileName")
try {
val `is` = context.resources.assets.open(fileName)
val raw = IOUtils.toByteArray(`is`)
`is`.close()
val inputStream = context.resources.assets.open(fileName)
val raw = inputStream.readBytes()
inputStream.close()
content = Html.fromHtml(String(raw))
return content
} catch (e: IOException) {

@ -359,11 +359,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
try {
if (future.result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT))
finish()
} catch (e: OperationCanceledException) {
App.log.log(Level.SEVERE, "Couldn't remove account", e)
} catch (e: IOException) {
App.log.log(Level.SEVERE, "Couldn't remove account", e)
} catch (e: AuthenticatorException) {
} catch(e: Exception) {
App.log.log(Level.SEVERE, "Couldn't remove account", e)
}
}, null)
@ -372,16 +368,13 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
try {
if (future.result)
finish()
} catch (e: OperationCanceledException) {
App.log.log(Level.SEVERE, "Couldn't remove account", e)
} catch (e: IOException) {
App.log.log(Level.SEVERE, "Couldn't remove account", e)
} catch (e: AuthenticatorException) {
} catch (e: Exception) {
App.log.log(Level.SEVERE, "Couldn't remove account", e)
}
}, null)
}
private fun requestSync() {
requestSync(account)
Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()

@ -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")

@ -27,9 +27,8 @@ import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.SyncEntry
import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment.Companion.setJournalEntryView
import ezvcard.util.PartialDate
import org.apache.commons.codec.Charsets
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.StringReader
import java.text.SimpleDateFormat
import java.util.*
@ -154,10 +153,10 @@ class JournalItemActivity : BaseActivity(), Refreshable {
private inner class LoadEventTask internal constructor(internal var view: View) : AsyncTask<Void, Void, Event>() {
override fun doInBackground(vararg aVoids: Void): Event? {
val `is` = ByteArrayInputStream(syncEntry.content.toByteArray(Charsets.UTF_8))
val inputReader = StringReader(syncEntry.content)
try {
return Event.fromStream(`is`, Charsets.UTF_8, null)[0]
return Event.fromReader(inputReader, null)[0]
} catch (e: InvalidCalendarException) {
e.printStackTrace()
} catch (e: IOException) {
@ -175,16 +174,17 @@ class JournalItemActivity : BaseActivity(), Refreshable {
setTextViewText(view, R.id.title, event.summary)
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart.date.time, event.dtEnd.date.time, event.isAllDay, context))
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart?.date?.time!!, event.dtEnd?.date!!.time, event.isAllDay(), context))
setTextViewText(view, R.id.where, event.location)
if (event.organizer != null) {
val organizer = event.organizer
if (organizer != null) {
val tv = view.findViewById<View>(R.id.organizer) as TextView
tv.text = event.organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
} else {
val organizer = view.findViewById<View>(R.id.organizer_container)
organizer.visibility = View.GONE
val organizerView = view.findViewById<View>(R.id.organizer_container)
organizerView.visibility = View.GONE
}
setTextViewText(view, R.id.description, event.description)
@ -220,10 +220,10 @@ class JournalItemActivity : BaseActivity(), Refreshable {
private inner class LoadContactTask internal constructor(internal var view: View) : AsyncTask<Void, Void, Contact>() {
override fun doInBackground(vararg aVoids: Void): Contact? {
val `is` = ByteArrayInputStream(syncEntry.content.toByteArray(Charsets.UTF_8))
val reader = StringReader(syncEntry.content)
try {
return Contact.fromStream(`is`, Charsets.UTF_8, null)[0]
return Contact.fromReader(reader, null)[0]
} catch (e: IOException) {
e.printStackTrace()
}
@ -279,7 +279,7 @@ class JournalItemActivity : BaseActivity(), Refreshable {
// ORG, TITLE, ROLE
if (contact.organization != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization.values[0])
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization?.values!![0])
}
if (contact.jobDescription != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle)
@ -291,8 +291,8 @@ class JournalItemActivity : BaseActivity(), Refreshable {
}
// NICKNAME
if (contact.nickName != null && contact.nickName.values.size > 0) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName.values[0])
if (contact.nickName != null && !contact.nickName?.values?.isEmpty()!!) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName?.values!![0])
}
// ADR
@ -314,11 +314,11 @@ class JournalItemActivity : BaseActivity(), Refreshable {
// ANNIVERSARY
if (contact.anniversary != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary.date, contact.anniversary.partialDate))
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary?.date, contact.anniversary?.partialDate))
}
// BDAY
if (contact.birthDay != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay.date, contact.birthDay.partialDate))
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay?.date, contact.birthDay?.partialDate))
}
// RELATED
@ -333,17 +333,19 @@ class JournalItemActivity : BaseActivity(), Refreshable {
}
}
private fun getDisplayedDate(date: Date?, partialDate: PartialDate): String? {
private fun getDisplayedDate(date: Date?, partialDate: PartialDate?): String? {
if (date != null) {
val epochDate = date.time
return getDisplayedDatetime(epochDate, epochDate, true, context)
} else {
} else if (partialDate != null){
val formatter = SimpleDateFormat("d MMMM", Locale.getDefault())
val calendar = GregorianCalendar()
calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!)
calendar.set(Calendar.MONTH, partialDate.month!! - 1)
return formatter.format(calendar.time)
}
return null
}
companion object {

@ -171,7 +171,7 @@ class ViewCollectionActivity : BaseActivity(), Refreshable {
if (info.type == CollectionInfo.Type.CALENDAR) {
try {
val providerClient = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
val resource = LocalCalendar.findByName(account, providerClient, LocalCalendar.Factory.INSTANCE, info.uid!!)
val resource = LocalCalendar.findByName(account, providerClient, LocalCalendar.Factory, info.uid!!)
providerClient!!.release()
if (resource == null) {
return null

@ -77,7 +77,7 @@ class CalendarAccount protected constructor(val account: Account) {
try {
val localCalendar = LocalCalendar.findByName(calendarAccount.account,
contentProviderClient,
LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME)!!)
LocalCalendar.Factory, getString(cur, Calendars.NAME)!!)
if (localCalendar != null) calendarAccount.calendars.add(localCalendar)
} catch (ex: Exception) {
ex.printStackTrace()

@ -32,10 +32,7 @@ import com.etesync.syncadapter.syncadapter.ContactsSyncManager
import com.etesync.syncadapter.ui.Refreshable
import com.etesync.syncadapter.ui.importlocal.ResultFragment.ImportResult
import org.apache.commons.codec.Charsets
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.*
class ImportFragment : DialogFragment() {
@ -204,11 +201,11 @@ class ImportFragment : DialogFragment() {
val result = ImportResult()
try {
val importStream = FileInputStream(importFile!!)
val importReader = FileReader(importFile!!)
if (info!!.type == CollectionInfo.Type.CALENDAR) {
val events = Event.fromStream(importStream, Charsets.UTF_8)
importStream.close()
val events = Event.fromReader(importReader, null)
importReader.close()
if (events.size == 0) {
App.log.warning("Empty/invalid file.")
@ -223,7 +220,7 @@ class ImportFragment : DialogFragment() {
val provider = context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
val localCalendar: LocalCalendar?
try {
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info!!.uid!!)
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info!!.uid!!)
if (localCalendar == null) {
throw FileNotFoundException("Failed to load local resource.")
}
@ -251,7 +248,7 @@ class ImportFragment : DialogFragment() {
} else if (info!!.type == CollectionInfo.Type.ADDRESS_BOOK) {
// FIXME: Handle groups and download icon?
val downloader = ContactsSyncManager.ResourceDownloader(context!!)
val contacts = Contact.fromStream(importStream, Charsets.UTF_8, downloader)
val contacts = Contact.fromReader(importReader, downloader)
if (contacts.size == 0) {
App.log.warning("Empty/invalid file.")

@ -78,7 +78,7 @@ class LocalCalendarImportFragment : ListFragment() {
}
override fun getChild(groupPosition: Int, childPosititon: Int): Any {
return calendarAccounts[groupPosition].getCalendars()[childPosititon].displayName
return calendarAccounts[groupPosition].getCalendars()[childPosititon].displayName!!
}
override fun getChildId(groupPosition: Int, childPosition: Int): Long {
@ -198,9 +198,9 @@ class LocalCalendarImportFragment : ListFragment() {
val result = ResultFragment.ImportResult()
try {
val localCalendar = LocalCalendar.findByName(account,
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI),
LocalCalendar.Factory.INSTANCE, info!!.uid!!)
val localEvents = fromCalendar.all
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
LocalCalendar.Factory, info!!.uid!!)
val localEvents = fromCalendar.findAll()
val total = localEvents.size
progressDialog!!.max = total
result.total = total.toLong()
@ -208,7 +208,7 @@ class LocalCalendarImportFragment : ListFragment() {
for (currentLocalEvent in localEvents) {
val event = currentLocalEvent.event
try {
val localEvent = LocalEvent(localCalendar!!, event, null, null)
val localEvent = LocalEvent(localCalendar!!, event!!, null, null)
localEvent.addAsDirty()
result.added++
} catch (e: CalendarStorageException) {

@ -133,7 +133,7 @@ class LocalContactImportFragment : Fragment() {
val addressBook = LocalAddressBook.findByUid(context!!,
context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
account, info!!.uid!!)
val localContacts = localAddressBook.all
val localContacts = localAddressBook.findAll()
val total = localContacts.size
progressDialog!!.max = total
result.total = total.toLong()
@ -142,7 +142,7 @@ class LocalContactImportFragment : Fragment() {
val contact = currentLocalContact.contact
try {
val localContact = LocalContact(addressBook!!, contact, null, null)
val localContact = LocalContact(addressBook!!, contact!!, null, null)
localContact.createAsDirty()
result.added++
} catch (e: ContactsStorageException) {

@ -1 +1 @@
Subproject commit 268473341cb761a0676f1746ff4467e48973f972
Subproject commit 2437b0b7aedf4fa1907a88c72781cff4c8291e40

@ -1 +1 @@
Subproject commit 3974799d7790f47987f7ae95fe444ab4442e7786
Subproject commit 42d5cc3f8b16c628fa13a5a3b0f211e6660fb084
Loading…
Cancel
Save