From c4daed9391974ea421441310a727f4fd494524bf Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sun, 6 Jan 2019 10:08:33 +0000 Subject: [PATCH] Upgrade vcard4android and ical4android. This is a monster commit because to be honest, it's a monster change. It was impossible to do it in smaller steps because things just wouldn't compile. We couldn't do the migration step by step because they moved to Kotlin which was causing a lot of troubles. Now we are all on Kotlin, so things should hopefully work just fine. --- app/build.gradle | 2 +- .../main/java/com/etesync/syncadapter/App.kt | 5 +- .../syncadapter/resource/LocalAddress.kt | 17 + .../syncadapter/resource/LocalAddressBook.kt | 473 ++++++++---------- .../syncadapter/resource/LocalCalendar.kt | 230 ++++----- .../syncadapter/resource/LocalCollection.kt | 19 +- .../syncadapter/resource/LocalContact.kt | 199 +++----- .../syncadapter/resource/LocalEvent.kt | 153 ++---- .../syncadapter/resource/LocalGroup.kt | 302 +++++------ .../syncadapter/resource/LocalResource.kt | 8 +- .../etesync/syncadapter/resource/LocalTask.kt | 147 ++---- .../syncadapter/resource/LocalTaskList.kt | 189 +++---- .../AddressBooksSyncAdapterService.kt | 4 - .../syncadapter/CalendarSyncManager.kt | 41 +- .../CalendarsSyncAdapterService.kt | 9 +- .../syncadapter/ContactsSyncManager.kt | 56 +-- .../syncadapter/syncadapter/SyncManager.kt | 26 +- .../etesync/syncadapter/ui/AboutActivity.kt | 7 +- .../etesync/syncadapter/ui/AccountActivity.kt | 13 +- .../syncadapter/ui/DebugInfoActivity.kt | 2 +- .../syncadapter/ui/JournalItemActivity.kt | 38 +- .../syncadapter/ui/ViewCollectionActivity.kt | 2 +- .../ui/importlocal/CalendarAccount.kt | 2 +- .../ui/importlocal/ImportFragment.kt | 15 +- .../LocalCalendarImportFragment.kt | 10 +- .../importlocal/LocalContactImportFragment.kt | 4 +- ical4android | 2 +- vcard4android | 2 +- 28 files changed, 793 insertions(+), 1184 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/resource/LocalAddress.kt 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