diff --git a/app/build.gradle b/app/build.gradle index 04b5471c..e027625f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ android { defaultConfig { applicationId "com.etesync.syncadapter" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 26 versionCode 43 diff --git a/app/src/main/java/com/etesync/syncadapter/App.kt b/app/src/main/java/com/etesync/syncadapter/App.kt index 7cb880d6..2054bdba 100644 --- a/app/src/main/java/com/etesync/syncadapter/App.kt +++ b/app/src/main/java/com/etesync/syncadapter/App.kt @@ -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 + val calendars = AndroidCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!, + LocalCalendar.Factory, null, null) for (calendar in calendars) { calendar.fixEtags() } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt new file mode 100644 index 00000000..8fbdf95f --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt @@ -0,0 +1,17 @@ +/* + * Copyright © Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.resource + +import at.bitfire.vcard4android.Contact + +interface LocalAddress: LocalResource { + + fun resetDeleted() + +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index ed095812..90eb08b3 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -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,8 +48,78 @@ 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 { - private val syncState = Bundle() +class LocalAddressBook( + private val context: Context, + account: Account, + provider: ContentProviderClient? +): AndroidAddressBook(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection { + + 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" + + 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, initialUserData(mainAccount, info.uid!!))) + throw ContactsStorageException("Couldn't create address book account") + + val addressBook = LocalAddressBook(context, account, provider) + ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true) + + val values = ContentValues(2) + values.put(ContactsContract.Settings.SHOULD_SYNC, 1) + values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) + addressBook.settings = values + + return addressBook + } + + + 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() + + + 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 + } + + // 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 @@ -57,100 +128,44 @@ class LocalAddressBook(protected val context: Context, account: Account, provide */ var includeGroups = true - /** - * Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0). - */ - override val deleted: Array - @Throws(ContactsStorageException::class) - get() { - val deleted = LinkedList() - Collections.addAll(deleted, *deletedContacts) - if (includeGroups) - Collections.addAll(deleted, *deletedGroups) - return deleted.toTypedArray() - } - - /** - * Returns an array of local contacts/groups which have been changed locally (DIRTY != 0). - */ - override val dirty: Array - @Throws(ContactsStorageException::class) - get() { - val dirty = LinkedList() - Collections.addAll(dirty, *dirtyContacts) - if (includeGroups) - Collections.addAll(dirty, *dirtyGroups) - return dirty.toTypedArray() - } - - /** - * Returns an array of local contacts which don't have a file name yet. - */ - override val withoutFileName: Array - @Throws(ContactsStorageException::class) - get() { - val nameless = LinkedList() - Collections.addAll(nameless, *queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null) as Array) - if (includeGroups) - Collections.addAll(nameless, *queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null) as Array) - return nameless.toTypedArray() - } - - val deletedContacts: Array - @Throws(ContactsStorageException::class) - get() = queryContacts(RawContacts.DELETED + "!= 0", null) as Array - - val dirtyContacts: Array - @Throws(ContactsStorageException::class) - get() = queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null) as Array - - val all: Array - @Throws(ContactsStorageException::class) - get() = queryContacts(RawContacts.DELETED + "== 0", null) as Array - - val deletedGroups: Array - @Throws(ContactsStorageException::class) - get() = queryGroups(Groups.DELETED + "!= 0", null) as Array - - val dirtyGroups: Array - @Throws(ContactsStorageException::class) - get() = queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null) as Array - + private var _mainAccount: Account? = null var mainAccount: Account - @Throws(ContactsStorageException::class) get() { - 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") + _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(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) + 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 } - var url: String? - @Throws(ContactsStorageException::class) - get() { - val accountManager = AccountManager.get(context) - return accountManager.getUserData(account, USER_DATA_URL) - } - @Throws(ContactsStorageException::class) - set(url) { - val accountManager = AccountManager.get(context) - accountManager.setUserData(account, USER_DATA_URL, url) - } + 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) - @Throws(AuthenticatorException::class, OperationCanceledException::class, IOException::class, ContactsStorageException::class, android.accounts.OperationCanceledException::class) 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 - if (contacts.size == 0) - throw FileNotFoundException() - return contacts[0] - } + override fun findAll(): List = 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 - 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,37 +286,10 @@ class LocalAddressBook(protected val context: Context, account: Account, provide } - @Throws(ContactsStorageException::class) - internal fun getByGroupMembership(groupID: Long): Array { - 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() - while (cursor != null && cursor.moveToNext()) - ids.add(cursor.getLong(0)) - - cursor!!.close() - - val contacts = arrayOfNulls(ids.size) - var i = 0 - for (id in ids) - contacts[i++] = LocalContact(this, id, null, null) - return contacts - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't query contacts", e) - } - - } - - - @Throws(ContactsStorageException::class) fun deleteAll() { try { - provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null) - provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null) + 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) } @@ -283,60 +297,56 @@ class LocalAddressBook(protected val context: Context, account: Account, provide } + /* special group operations */ + fun getByGroupMembership(groupID: Long): List { + val ids = HashSet() + 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) - 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 { - val accountManager = AccountManager.get(context) - - val result = LinkedList() - 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() - } - } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 8fc6e64d..a54c6022 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -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 net.fortuna.ical4j.model.component.VTimeZone - import org.apache.commons.lang3.StringUtils - import java.io.FileNotFoundException -import java.util.LinkedList +import java.util.* +import java.util.logging.Level -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 private constructor( + account: Account, + provider: ContentProviderClient, + id: Long +): AndroidCalendar(account, provider, LocalEvent.Factory, id), LocalCollection { -class LocalCalendar protected constructor(account: Account, provider: ContentProviderClient, id: Long) : AndroidCalendar(account, provider, LocalEvent.Factory.INSTANCE, id), LocalCollection { + companion object { + val defaultColor = -0x743cb6 // light green 500 - override val deleted: Array - @Throws(CalendarStorageException::class) - get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array + val COLUMN_CTAG = Calendars.CAL_SYNC1 - override val withoutFileName: Array - @Throws(CalendarStorageException::class) - get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array + 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) - val all: Array - @Throws(CalendarStorageException::class) - get() = queryEvents(null, null) as Array + // 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) - 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 - @Throws(CalendarStorageException::class, FileNotFoundException::class) - get() { - val dirty = LinkedList() - for (event in queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array) { - if (event.event.sequence == null) - event.event.sequence = 0 - else if (event.weAreOrganizer) - event.event.sequence++ - dirty.add(event) - } - - return dirty.toTypedArray() + return AndroidCalendar.create(account, provider, values) } - override fun eventBaseInfoColumns(): Array { - 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() + + 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) - fun update(journalEntity: JournalEntity, updateColor: Boolean) { - update(valuesFromCollectionInfo(journalEntity, updateColor)) + 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 { + val dirty = LinkedList() + + // 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 getByUid(uid: String): LocalEvent? { - val ret = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)) as Array - return if (ret != null && ret.size > 0) { - ret[0] - } else null - } + override fun findWithoutFileName(): List + = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) + + override fun findAll(): List + = queryEvents(null, null) + + override fun findByUid(uid: String): LocalEvent? + = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() - @Throws(CalendarStorageException::class) 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 { - return arrayOfNulls(size) as Array - } - - 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 { + object Factory: AndroidCalendarFactory { - val defaultColor = -0x743cb6 // light green 500 - - val COLUMN_CTAG = Calendars.CAL_SYNC1 - - 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) } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt index 02d1672e..fc2c77b1 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt @@ -8,21 +8,14 @@ package com.etesync.syncadapter.resource -import java.io.FileNotFoundException +interface LocalCollection> { + fun findDeleted(): List + fun findDirty(): List + fun findWithoutFileName(): List + fun findAll(): List -import at.bitfire.ical4android.CalendarStorageException -import at.bitfire.vcard4android.ContactsStorageException + fun findByUid(uid: String): T? -interface LocalCollection { - val deleted: Array - val withoutFileName: Array - /** Dirty *non-deleted* entries */ - val dirty: Array - - @Throws(CalendarStorageException::class, ContactsStorageException::class) - fun getByUid(uid: String): T? - - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun count(): Long } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt index de184790..428add3e 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt @@ -42,7 +42,14 @@ import ezvcard.VCardVersion import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS -class LocalContact : AndroidContact, LocalResource { +class LocalContact : AndroidContact, LocalAddress { + companion object { + init { + Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION + } + + internal const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3 + } private var saveAsDirty = false // When true, the resource will be saved as dirty @@ -57,7 +64,6 @@ class LocalContact : AndroidContact, LocalResource { get() = TextUtils.isEmpty(eTag) override val content: String - @Throws(IOException::class, ContactsStorageException::class) get() { val contact: Contact contact = this.contact!! @@ -70,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") + constructor(addressBook: AndroidAddressBook, values: ContentValues) + : super(addressBook, values) {} - try { - val c = addressBook.provider.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null) - try { - return if (c == null || !c.moveToNext() || c.isNull(0)) 0 else c.getInt(0) - } finally { - c?.close() - } - } catch (e: RemoteException) { - throw ContactsStorageException("Could't read last hash code", e) - } + constructor(addressBook: AndroidAddressBook, contact: Contact, uuid: String?, eTag: String?) + : super(addressBook, contact, uuid, eTag) {} - } - - - constructor(addressBook: AndroidAddressBook, id: Long, uuid: String?, eTag: String?) : super(addressBook, id, uuid, eTag) {} - - constructor(addressBook: AndroidAddressBook, contact: Contact, uuid: String?, eTag: String?) : super(addressBook, contact, uuid, eTag) {} - - @Throws(ContactsStorageException::class) fun resetDirty() { val values = ContentValues(1) values.put(ContactsContract.RawContacts.DIRTY, 0) - try { - addressBook.provider.update(rawContactSyncURI(), values, null, null) - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't clear dirty flag", e) - } - + addressBook.provider?.update(rawContactSyncURI(), values, null, null) + } + + override fun resetDeleted() { + val values = ContentValues(1) + values.put(ContactsContract.Groups.DELETED, 0) + addressBook.provider?.update(rawContactSyncURI(), values, null, null) } - @Throws(ContactsStorageException::class) override fun clearDirty(eTag: String) { - try { - val values = ContentValues(3) - values.put(AndroidContact.COLUMN_ETAG, eTag) - values.put(ContactsContract.RawContacts.DIRTY, 0) + val values = ContentValues(3) + values.put(AndroidContact.COLUMN_ETAG, eTag) + values.put(ContactsContract.RawContacts.DIRTY, 0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // workaround for Android 7 which sets DIRTY flag when only meta-data is changed - val hashCode = dataHashCode() - values.put(COLUMN_HASHCODE, hashCode) - App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode") - } - - addressBook.provider.update(rawContactSyncURI(), values, null, null) - - this.eTag = eTag - } catch (e: FileNotFoundException) { - throw ContactsStorageException("Couldn't clear dirty flag", e) - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't clear dirty flag", e) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // workaround for Android 7 which sets DIRTY flag when only meta-data is changed + val hashCode = dataHashCode() + values.put(COLUMN_HASHCODE, hashCode) + App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode") } + addressBook.provider?.update(rawContactSyncURI(), values, null, null) + + this.eTag = eTag } - @Throws(ContactsStorageException::class) override fun prepareForUpload() { - try { - val uid = UUID.randomUUID().toString() + val uid = UUID.randomUUID().toString() - val values = ContentValues(2) - values.put(AndroidContact.COLUMN_FILENAME, uid) - values.put(AndroidContact.COLUMN_UID, uid) - addressBook.provider.update(rawContactSyncURI(), values, null, null) - - fileName = uid - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't update UID", e) - } + val values = ContentValues(2) + values.put(AndroidContact.COLUMN_FILENAME, uid) + values.put(AndroidContact.COLUMN_UID, uid) + addressBook.provider?.update(rawContactSyncURI(), values, null, null) + fileName = uid } override fun populateData(mimeType: String, row: ContentValues) { when (mimeType) { CachedGroupMembership.CONTENT_ITEM_TYPE -> cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID)) GroupMembership.CONTENT_ITEM_TYPE -> groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID)) - UnknownProperties.CONTENT_ITEM_TYPE -> contact.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES) + UnknownProperties.CONTENT_ITEM_TYPE -> contact?.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES) } } - @Throws(ContactsStorageException::class) override fun insertDataRows(batch: BatchOperation) { super.insertDataRows(batch) - 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) + val hashCode = dataHashCode() + App.log.fine("Storing contact hash = $hashCode") + values.put(COLUMN_HASHCODE, hashCode) - if (batch == null) - addressBook.provider.update(rawContactSyncURI(), values, null, null) - else { - val builder = ContentProviderOperation - .newUpdate(rawContactSyncURI()) - .withValues(values) - batch.enqueue(BatchOperation.Operation(builder)) - } - } catch (e: FileNotFoundException) { - throw ContactsStorageException("Couldn't store contact checksum", e) - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't store contact checksum", e) + if (batch == null) + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + else { + val builder = ContentProviderOperation + .newUpdate(rawContactSyncURI()) + .withValues(values) + batch.enqueue(BatchOperation.Operation(builder)) } + } + fun getLastHashCode(): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + throw IllegalStateException("getLastHashCode() should not be called on Android != 7") + + addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c -> + if (c.moveToNext() && !c.isNull(0)) + return c.getInt(0) + } + return 0 } - fun addToGroup(batch: BatchOperation, groupID: Long) { - assertID() + fun addToGroup(batch: BatchOperation, groupID: Long) { batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newInsert(dataSyncURI()) .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) @@ -263,7 +235,6 @@ class LocalContact : AndroidContact, LocalResource { } fun removeGroupMemberships(batch: BatchOperation) { - assertID() batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newDelete(dataSyncURI()) .withSelection( @@ -284,9 +255,8 @@ class LocalContact : AndroidContact, LocalResource { * @throws ContactsStorageException on contact provider errors * @throws FileNotFoundException if the current contact can't be found */ - @Throws(ContactsStorageException::class, FileNotFoundException::class) fun getCachedGroupMemberships(): Set { - getContact() + contact return cachedGroupMemberships } @@ -296,37 +266,16 @@ class LocalContact : AndroidContact, LocalResource { * @throws ContactsStorageException on contact provider errors * @throws FileNotFoundException if the current contact can't be found */ - @Throws(ContactsStorageException::class, FileNotFoundException::class) fun getGroupMemberships(): Set { - getContact() + contact return groupMemberships } // factory - internal class Factory : AndroidContactFactory() { - - override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalContact { - return LocalContact(addressBook, id, fileName, eTag) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - + object Factory: AndroidContactFactory { + override fun fromProvider(addressBook: AndroidAddressBook, values: ContentValues) = + LocalContact(addressBook, values) } - - companion object { - init { - Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION - } - - val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3 - } - } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt index dc90e2af..3a0c3056 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt @@ -8,36 +8,34 @@ package com.etesync.syncadapter.resource -import android.annotation.TargetApi import android.content.ContentProviderOperation import android.content.ContentValues -import android.database.Cursor import android.net.Uri -import android.os.Build import android.os.RemoteException import android.provider.CalendarContract import android.provider.CalendarContract.Events import android.text.TextUtils - +import at.bitfire.ical4android.* +import at.bitfire.ical4android.Constants.ical4jVersion +import at.bitfire.vcard4android.ContactsStorageException import com.etesync.syncadapter.App import com.etesync.syncadapter.Constants - import net.fortuna.ical4j.model.property.ProdId - import java.io.ByteArrayOutputStream import java.io.IOException -import java.util.UUID +import java.util.* import java.util.logging.Level -import at.bitfire.ical4android.AndroidCalendar -import at.bitfire.ical4android.AndroidEvent -import at.bitfire.ical4android.AndroidEventFactory -import at.bitfire.ical4android.CalendarStorageException -import at.bitfire.ical4android.Event -import at.bitfire.vcard4android.ContactsStorageException +class LocalEvent : AndroidEvent, LocalResource { + companion object { + init { + ICalendar.prodId = ProdId(Constants.PRODID_BASE + " ical4j/" + ical4jVersion) + } -@TargetApi(17) -class LocalEvent : AndroidEvent, LocalResource { + internal const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1 + internal const val COLUMN_UID = Events.UID_2445 + internal const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3 + } private var saveAsDirty = false // When true, the resource will be saved as dirty @@ -47,12 +45,11 @@ class LocalEvent : AndroidEvent, LocalResource { var weAreOrganizer = true override val content: String - @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class) get() { - App.log.log(Level.FINE, "Preparing upload of event " + fileName!!, getEvent()) + App.log.log(Level.FINE, "Preparing upload of event " + fileName!!, event) val os = ByteArrayOutputStream() - getEvent().write(os) + event?.write(os) return os.toString() } @@ -64,34 +61,27 @@ class LocalEvent : AndroidEvent, LocalResource { val uuid: String? get() = fileName - constructor(calendar: AndroidCalendar, event: Event, fileName: String?, eTag: String?) : super(calendar, event) { + constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) { this.fileName = fileName this.eTag = eTag } - protected constructor(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues?) : super(calendar, id, baseInfo) { - if (baseInfo != null) { - fileName = baseInfo.getAsString(Events._SYNC_ID) - eTag = baseInfo.getAsString(COLUMN_ETAG) - } + protected constructor(calendar: AndroidCalendar<*>, baseInfo: ContentValues) : super(calendar, baseInfo) { + fileName = baseInfo.getAsString(Events._SYNC_ID) + eTag = baseInfo.getAsString(COLUMN_ETAG) } /* process LocalEvent-specific fields */ - override fun populateEvent(values: ContentValues) { - super.populateEvent(values) - fileName = values.getAsString(Events._SYNC_ID) - eTag = values.getAsString(COLUMN_ETAG) - event.uid = values.getAsString(COLUMN_UID) + override fun populateEvent(row: ContentValues) { + super.populateEvent(row) + fileName = row.getAsString(Events._SYNC_ID) + eTag = row.getAsString(COLUMN_ETAG) + event?.uid = row.getAsString(COLUMN_UID) - event.sequence = values.getAsInteger(COLUMN_SEQUENCE) - if (Build.VERSION.SDK_INT >= 17) { - val isOrganizer = values.getAsInteger(Events.IS_ORGANIZER) - weAreOrganizer = isOrganizer != null && isOrganizer != 0 - } else { - val organizer = values.getAsString(Events.ORGANIZER) - weAreOrganizer = organizer == null || organizer == calendar.account.name - } + event?.sequence = row.getAsInteger(COLUMN_SEQUENCE) + val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER) + weAreOrganizer = isOrganizer != null && isOrganizer != 0 } override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) { @@ -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() + var uid: String? = null + val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) + if (c.moveToNext()) + uid = c.getString(0) + if (uid == null) + uid = UUID.randomUUID().toString() - c.close() - val newFileName = uid + c.close() + val newFileName = uid - val values = ContentValues(2) - values.put(Events._SYNC_ID, newFileName) - values.put(COLUMN_UID, uid) - calendar.provider.update(eventSyncURI(), values, null, null) + val values = ContentValues(2) + values.put(Events._SYNC_ID, newFileName) + values.put(COLUMN_UID, uid) + calendar.provider.update(eventSyncURI(), values, null, null) - fileName = newFileName - if (event != null) - event.uid = uid - - } catch (e: RemoteException) { - throw CalendarStorageException("Couldn't update UID", e) - } + fileName = newFileName + val event = this.event + if (event != null) + event.uid = uid } - @Throws(CalendarStorageException::class) override fun clearDirty(eTag: String) { - try { - val values = ContentValues(2) - values.put(CalendarContract.Events.DIRTY, 0) - values.put(COLUMN_ETAG, eTag) - if (event != null) - values.put(COLUMN_SEQUENCE, event.sequence) - calendar.provider.update(eventSyncURI(), values, null, null) - - this.eTag = eTag - } catch (e: RemoteException) { - throw CalendarStorageException("Couldn't update UID", e) - } + val values = ContentValues(2) + values.put(CalendarContract.Events.DIRTY, 0) + values.put(COLUMN_ETAG, eTag) + if (event != null) + values.put(COLUMN_SEQUENCE, event?.sequence) + calendar.provider.update(eventSyncURI(), values, null, null) + this.eTag = eTag } - internal class Factory : AndroidEventFactory { - - override fun newInstance(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues): AndroidEvent { - return LocalEvent(calendar, id, baseInfo) - } - - override fun newInstance(calendar: AndroidCalendar, event: Event): AndroidEvent { - return LocalEvent(calendar, event, null, null) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - } - - companion object { - init { - Event.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x") - } - - internal val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1 - internal val COLUMN_UID = if (Build.VERSION.SDK_INT >= 17) Events.UID_2445 else Events.SYNC_DATA2 - internal val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3 + object Factory: AndroidEventFactory { + override fun fromProvider(calendar: AndroidCalendar, values: ContentValues): LocalEvent = + LocalEvent(calendar, values) } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index bc2c30a5..85625a0d 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -12,6 +12,7 @@ import android.content.ContentProviderOperation import android.content.ContentUris import android.content.ContentValues import android.database.Cursor +import android.net.Uri import android.os.Build import android.os.Parcel import android.os.RemoteException @@ -21,6 +22,7 @@ import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import android.provider.ContactsContract.RawContacts.Data import android.text.TextUtils +import at.bitfire.vcard4android.* import com.etesync.syncadapter.App @@ -34,27 +36,88 @@ import java.util.LinkedList import java.util.UUID import java.util.logging.Level -import at.bitfire.vcard4android.AndroidAddressBook -import at.bitfire.vcard4android.AndroidGroup -import at.bitfire.vcard4android.AndroidGroupFactory -import at.bitfire.vcard4android.BatchOperation -import at.bitfire.vcard4android.CachedGroupMembership -import at.bitfire.vcard4android.Contact -import at.bitfire.vcard4android.ContactsStorageException import ezvcard.VCardVersion import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS -class LocalGroup : AndroidGroup, LocalResource { +class LocalGroup : AndroidGroup, LocalAddress { + companion object { + /** marshalled list of member UIDs, as sent by server */ + val COLUMN_PENDING_MEMBERS = Groups.SYNC3 - override val uuid: String - get() = getFileName() + /** + * Processes all groups with non-null {@link #COLUMN_PENDING_MEMBERS}: the pending memberships + * are (if possible) applied, keeping cached memberships in sync. + * @param addressBook address book to take groups from + */ + fun applyPendingMemberships(addressBook: LocalAddressBook) { + addressBook.provider!!.query( + addressBook.groupsSyncUri(), + arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS), + "$COLUMN_PENDING_MEMBERS IS NOT NULL", null, + null + )?.use { cursor -> + val batch = BatchOperation(addressBook.provider) + while (cursor.moveToNext()) { + val id = cursor.getLong(0) + Constants.log.fine("Assigning members to group $id") + + // required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed + val changeContactIDs = HashSet() + + // delete all memberships and cached memberships for this group + for (contact in addressBook.getByGroupMembership(id)) { + contact.removeGroupMemberships(batch) + changeContactIDs += contact.id!! + } + + // extract list of member UIDs + val members = LinkedList() + val raw = cursor.getBlob(1) + val parcel = Parcel.obtain() + try { + parcel.unmarshall(raw, 0, raw.size) + parcel.setDataPosition(0) + parcel.readStringList(members) + } finally { + parcel.recycle() + } + + // insert memberships + for (uid in members) { + Constants.log.fine("Assigning member: $uid") + addressBook.findContactByUID(uid)?.let { member -> + member.addToGroup(batch, id) + changeContactIDs += member.id!! + } ?: Constants.log.warning("Group member not found: $uid") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + // workaround for Android 7 which sets DIRTY flag when only meta-data is changed + changeContactIDs + .map { addressBook.findContactByID(it) } + .forEach { it.updateHashCode(batch) } + + // remove pending memberships + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id))) + .withValue(COLUMN_PENDING_MEMBERS, null) + .withYieldAllowed(true) + )) + + batch.commit() + } + } + } + } + + override val uuid: String? + get() = fileName override val content: String - @Throws(IOException::class, ContactsStorageException::class) get() { val contact: Contact - contact = getContact() + contact = this.contact!! App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact) @@ -65,42 +128,29 @@ class LocalGroup : AndroidGroup, LocalResource { } override val isLocalOnly: Boolean - get() = TextUtils.isEmpty(getETag()) + get() = TextUtils.isEmpty(eTag) - /** - * Lists all members of this group. - * @return list of all members' raw contact IDs - * @throws ContactsStorageException on contact provider errors - */ - val members: LongArray - @Throws(ContactsStorageException::class) - get() { - assertID() - val members = LinkedList() - try { - val cursor = addressBook.provider.query( - addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), - arrayOf(Data.RAW_CONTACT_ID), - GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", - arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), null - ) - while (cursor != null && cursor.moveToNext()) - members.add(cursor.getLong(0)) - cursor!!.close() - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't list group members", e) - } + constructor(addressBook: AndroidAddressBook, values: ContentValues) + : super(addressBook, values) {} - return ArrayUtils.toPrimitive(members.toTypedArray()) + constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?) + : super(addressBook, contact, fileName, eTag) {} + + override fun contentValues(): ContentValues { + val values = super.contentValues() + + val members = Parcel.obtain() + try { + members.writeStringList(contact?.members) + values.put(COLUMN_PENDING_MEMBERS, members.marshall()) + } finally { + members.recycle() } + return values + } - constructor(addressBook: AndroidAddressBook, id: Long, fileName: String?, eTag: String?) : super(addressBook, id, fileName, eTag) {} - - constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?) : super(addressBook, contact, fileName, eTag) {} - - @Throws(ContactsStorageException::class) override fun clearDirty(eTag: String) { - assertID() + val id = requireNotNull(id) val values = ContentValues(2) values.put(Groups.DIRTY, 0) @@ -109,7 +159,7 @@ class LocalGroup : AndroidGroup, LocalResource { update(values) // update cached group memberships - val batch = BatchOperation(addressBook.provider) + val batch = BatchOperation(addressBook.provider!!) // delete cached group memberships batch.enqueue(BatchOperation.Operation( @@ -121,7 +171,7 @@ class LocalGroup : AndroidGroup, LocalResource { )) // insert updated cached group memberships - for (member in members) + for (member in getMembers()) batch.enqueue(BatchOperation.Operation( ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) @@ -133,7 +183,6 @@ class LocalGroup : AndroidGroup, LocalResource { batch.commit() } - @Throws(ContactsStorageException::class) override fun prepareForUpload() { val uid = UUID.randomUUID().toString() @@ -145,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 + ")" + /** + * Lists all members of this group. + * @return list of all members' raw contact IDs + * @throws RemoteException on contact provider errors + */ + internal fun getMembers(): List { + val id = requireNotNull(id) + val members = LinkedList() + addressBook.provider!!.query( + addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), + arrayOf(Data.RAW_CONTACT_ID), + "${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?", + arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) + members += cursor.getLong(0) + } + return members } + // factory - internal class Factory : AndroidGroupFactory() { - - override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalGroup { - return LocalGroup(addressBook, id, fileName, eTag) - } - - override fun newInstance(addressBook: AndroidAddressBook, contact: Contact, fileName: String, eTag: String): LocalGroup { - return LocalGroup(addressBook, contact, fileName, eTag) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - + object Factory: AndroidGroupFactory { + override fun fromProvider(addressBook: AndroidAddressBook, values: ContentValues) = + LocalGroup(addressBook, values) } - - companion object { - /** marshalled list of member UIDs, as sent by server */ - val COLUMN_PENDING_MEMBERS = Groups.SYNC3 - - /** - * Processes all groups with non-null [.COLUMN_PENDING_MEMBERS]: the pending memberships - * are (if possible) applied, keeping cached memberships in sync. - * @param addressBook address book to take groups from - * @throws ContactsStorageException on contact provider errors - */ - @Throws(ContactsStorageException::class) - fun applyPendingMemberships(addressBook: LocalAddressBook) { - try { - val cursor = addressBook.provider.query( - addressBook.syncAdapterURI(Groups.CONTENT_URI), - arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS), - "$COLUMN_PENDING_MEMBERS IS NOT NULL", arrayOf(), null - ) - - val batch = BatchOperation(addressBook.provider) - while (cursor != null && cursor.moveToNext()) { - val id = cursor.getLong(0) - App.log.fine("Assigning members to group $id") - - // required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed - val changeContactIDs = HashSet() - - // delete all memberships and cached memberships for this group - for (contact in addressBook.getByGroupMembership(id)) { - contact.removeGroupMemberships(batch) - changeContactIDs.add(contact.id) - } - - // extract list of member UIDs - val members = LinkedList() - val raw = cursor.getBlob(1) - val parcel = Parcel.obtain() - parcel.unmarshall(raw, 0, raw.size) - parcel.setDataPosition(0) - parcel.readStringList(members) - parcel.recycle() - - // insert memberships - for (uid in members) { - App.log.fine("Assigning member: $uid") - try { - val member = addressBook.findContactByUID(uid) - member.addToGroup(batch, id) - changeContactIDs.add(member.id) - } catch (e: FileNotFoundException) { - App.log.log(Level.WARNING, "Group member not found: $uid", e) - } - - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - // workaround for Android 7 which sets DIRTY flag when only meta-data is changed - for (contactID in changeContactIDs) { - val contact = LocalContact(addressBook, contactID, null, null) - contact.updateHashCode(batch) - } - - // remove pending memberships - batch.enqueue(BatchOperation.Operation( - ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id))) - .withValue(COLUMN_PENDING_MEMBERS, null) - .withYieldAllowed(true) - )) - - batch.commit() - } - cursor!!.close() - } catch (e: RemoteException) { - throw ContactsStorageException("Couldn't get pending memberships", e) - } - - } - } - } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt index 56aafebf..d4413fac 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt @@ -8,12 +8,10 @@ package com.etesync.syncadapter.resource -import java.io.IOException - import at.bitfire.ical4android.CalendarStorageException import at.bitfire.vcard4android.ContactsStorageException -interface LocalResource { +interface LocalResource { val uuid: String? /** True if doesn't exist on server yet, false otherwise. */ @@ -22,13 +20,9 @@ interface LocalResource { /** Returns a string of how this should be represented for example: vCard. */ val content: String - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun delete(): Int - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun prepareForUpload() - @Throws(CalendarStorageException::class, ContactsStorageException::class) fun clearDirty(eTag: String) - } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt index 10b42062..9ddd0b7a 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -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 { + companion object { + internal const val COLUMN_ETAG = TaskContract.Tasks.SYNC1 + internal const val COLUMN_UID = TaskContract.Tasks.SYNC2 + internal const val COLUMN_SEQUENCE = TaskContract.Tasks.SYNC3 + } -class LocalTask : AndroidTask, LocalResource { private var fileName: String? = null var eTag: String? = null override val content: String - @Throws(IOException::class, ContactsStorageException::class) - get() = "" + get() { + App.log.log(Level.FINE, "Preparing upload of task " + fileName!!, task) + + val os = ByteArrayOutputStream() + task?.write(os) + + return os.toString() + } override val isLocalOnly: Boolean - get() = false + get() = TextUtils.isEmpty(eTag) override// Now the same val uuid: String? get() = fileName - constructor(taskList: AndroidTaskList, task: Task, fileName: String?, eTag: String?) : super(taskList, task) { + constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int) + : super(taskList, task) { this.fileName = fileName this.eTag = eTag } - protected constructor(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues?) : super(taskList, id) { - if (baseInfo != null) { - fileName = baseInfo.getAsString(Events._SYNC_ID) - eTag = baseInfo.getAsString(COLUMN_ETAG) - } + private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) { + id = values.getAsLong(TaskContract.Tasks._ID) + fileName = values.getAsString(TaskContract.Tasks._SYNC_ID) + eTag = values.getAsString(COLUMN_ETAG) } + /* process LocalTask-specific fields */ - @Throws(FileNotFoundException::class, RemoteException::class, ParseException::class) - override fun populateTask(values: ContentValues) { - super.populateTask(values) - - fileName = values.getAsString(Events._SYNC_ID) - eTag = values.getAsString(COLUMN_ETAG) - task.uid = values.getAsString(COLUMN_UID) - - task.sequence = values.getAsInteger(COLUMN_SEQUENCE) - } - override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) { super.buildTask(builder, update) - builder.withValue(Tasks._SYNC_ID, fileName) - .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) - } + val values = ContentValues(2) + values.put(TaskContract.Tasks._DIRTY, 0) + values.put(COLUMN_ETAG, eTag) + if (task != null) + values.put(COLUMN_SEQUENCE, task?.sequence) + taskList.provider.client.update(taskSyncURI(), values, null, null) + this.eTag = eTag } - internal class Factory : AndroidTaskFactory { - - override fun newInstance(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues): LocalTask { - return LocalTask(taskList, id, baseInfo) - } - - override fun newInstance(taskList: AndroidTaskList, task: Task): LocalTask { - return LocalTask(taskList, task, null, null) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - - companion object { - val INSTANCE = Factory() - } - } - - companion object { - init { - Task.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x") - } - - internal val COLUMN_ETAG = Tasks.SYNC1 - internal val COLUMN_UID = Tasks.SYNC2 - internal val COLUMN_SEQUENCE = Tasks.SYNC3 + object Factory: AndroidTaskFactory { + override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) = + LocalTask(taskList, values) } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index 4c0910bf..d9ef05e4 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -9,82 +9,95 @@ 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 +import com.etesync.syncadapter.model.JournalEntity +import org.dmfs.tasks.contract.TaskContract.TaskLists +import org.dmfs.tasks.contract.TaskContract.Tasks -class LocalTaskList protected constructor(account: Account, provider: TaskProvider, id: Long) : AndroidTaskList(account, provider, LocalTask.Factory.INSTANCE, id), LocalCollection { +class LocalTaskList private constructor( + account: Account, + provider: TaskProvider, + id: Long +): AndroidTaskList(account, provider, LocalTask.Factory, id), LocalCollection { + companion object { + val defaultColor = -0x3c1592 // "DAVdroid green" - override val deleted: Array - @Throws(CalendarStorageException::class) - get() = queryTasks(Tasks._DELETED + "!=0", null) as Array - - override val withoutFileName: Array - @Throws(CalendarStorageException::class) - get() = queryTasks(Tasks._SYNC_ID + " IS NULL", null) as Array - - override// sequence has not been assigned yet (i.e. this task was just locally created) - val dirty: Array - @Throws(CalendarStorageException::class, FileNotFoundException::class) - get() { - val tasks = queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null) as Array - for (task in tasks) { - if (task.task.sequence == null) - task.task.sequence = 0 - else - task.task.sequence++ + 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 } - return tasks } + 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 create(account, provider, values) + } + + 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, if (info.displayName.isNullOrBlank()) info.uid else info.displayName) + + if (withColor) + values.put(TaskLists.LIST_COLOR, info.color ?: defaultColor) + + return values + } - override fun taskBaseInfoColumns(): Array { - return BASE_INFO_COLUMNS } - @Throws(CalendarStorageException::class) - fun update(info: CollectionInfo, updateColor: Boolean) { - update(valuesFromCollectionInfo(info, updateColor)) + fun update(journalEntity: JournalEntity, updateColor: Boolean) = + update(valuesFromCollectionInfo(journalEntity, updateColor)) + + override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null) + + override fun findDirty(): List { + 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 } - @Throws(CalendarStorageException::class) - override fun getByUid(uid: String): LocalTask? { - val ret = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)) as Array - return if (ret != null && ret.size > 0) { - ret[0] - } else null - } + override fun findAll(): List + = queryTasks(null, null) + + override fun findWithoutFileName(): List + = queryTasks(Tasks._SYNC_ID + " IS NULL", null) + + override fun findByUid(uid: String): LocalTask? + = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() - @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) + TaskProvider.syncAdapterUri(provider.tasksUri(), account), null, + Tasks.LIST_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) @@ -92,78 +105,10 @@ class LocalTaskList protected constructor(account: Account, provider: TaskProvid } + object Factory: AndroidTaskListFactory { - class Factory : AndroidTaskListFactory { + override fun newInstance(account: Account, provider: TaskProvider, id: Long) = + LocalTaskList(account, provider, id) - override fun newInstance(account: Account, provider: TaskProvider, id: Long): AndroidTaskList { - return LocalTaskList(account, provider, id) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) as Array - } - - companion object { - val INSTANCE = Factory() - } } - - 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) - - @Throws(CalendarStorageException::class) - fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri { - val values = valuesFromCollectionInfo(info, true) - values.put(TaskLists.OWNER, account.name) - values.put(TaskLists.SYNC_ENABLED, 1) - values.put(TaskLists.VISIBLE, 1) - return AndroidTaskList.create(account, provider, values) - } - - private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues { - val values = ContentValues() - values.put(TaskLists._SYNC_ID, info.uid) - values.put(TaskLists.LIST_NAME, info.displayName) - - if (withColor) - values.put(TaskLists.LIST_COLOR, if (info.color != null) info.color else 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() - } - } - } - - - // HELPERS - - @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() - } - } - } - } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt index fece6f4b..9f6e7a9d 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt @@ -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) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt index 5244c01a..7b2e0e64 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt @@ -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(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 + 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) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt index fe069285..0d44ada6 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt @@ -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) { + 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 + val local = AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null) val updateColors = settings.manageCalendarColors diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt index 91082c36..1b16eb18 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt @@ -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(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) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index bc613fc1..2fce6e9f 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -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> @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? = null + protected var localCollection: LocalCollection? = 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? = null - protected var localDirty: Array = arrayOf() + private var localDeleted: List? = null + protected var localDirty: List = 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 { - val localList = localCollection!!.deleted - val ret = ArrayList(localList.size) + private fun processLocallyDeleted(): List { + val localList = localCollection!!.findDeleted() + val ret = ArrayList(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() } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt index 5da8fc31..55edaa48 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt @@ -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) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 2a857d77..089e0616 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -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() diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt index d9a41cca..0cb9f211 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt @@ -149,7 +149,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks report.append("CONFIGURATION\n") // power saving - val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager? if (powerManager != null && Build.VERSION.SDK_INT >= 23) report.append("Power saving disabled: ") .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") diff --git a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt index 300dde4c..70eebf04 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt @@ -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() { 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(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(R.id.organizer_container) - organizer.visibility = View.GONE + val organizerView = view.findViewById(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() { 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 { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt index b7165a4e..c9ffc801 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt @@ -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 diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt index 89c90c8b..1c351c9a 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt @@ -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() diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt index f93ea582..2acc50af 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt @@ -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.") diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt index 8484bfb9..db3b9750 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt @@ -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) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt index 0d829d84..55d3ad51 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt @@ -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) { diff --git a/ical4android b/ical4android index 26847334..2437b0b7 160000 --- a/ical4android +++ b/ical4android @@ -1 +1 @@ -Subproject commit 268473341cb761a0676f1746ff4467e48973f972 +Subproject commit 2437b0b7aedf4fa1907a88c72781cff4c8291e40 diff --git a/vcard4android b/vcard4android index 3974799d..42d5cc3f 160000 --- a/vcard4android +++ b/vcard4android @@ -1 +1 @@ -Subproject commit 3974799d7790f47987f7ae95fe444ab4442e7786 +Subproject commit 42d5cc3f8b16c628fa13a5a3b0f211e6660fb084