diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java deleted file mode 100644 index 7c4532a0..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.RemoteException; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.GroupMembership; -import android.provider.ContactsContract.Groups; -import android.provider.ContactsContract.RawContacts; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.os.OperationCanceledException; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.utils.AndroidCompat; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.logging.Level; - -import at.bitfire.vcard4android.AndroidAddressBook; -import at.bitfire.vcard4android.AndroidContact; -import at.bitfire.vcard4android.AndroidGroup; -import at.bitfire.vcard4android.CachedGroupMembership; -import at.bitfire.vcard4android.ContactsStorageException; - - -public class LocalAddressBook extends AndroidAddressBook implements LocalCollection { - - protected static final String - USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type", - USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name", - USER_DATA_URL = "url"; - - protected final Context context; - private final Bundle syncState = new Bundle(); - - /** - * Whether contact groups (LocalGroup resources) are included in query results for - * {@link #getDeleted()}, {@link #getDirty()} and - * {@link #getWithoutFileName()}. - */ - public boolean includeGroups = true; - - - public static LocalAddressBook[] find(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount) throws ContactsStorageException { - AccountManager accountManager = AccountManager.get(context); - - List result = new LinkedList<>(); - for (Account account : accountManager.getAccountsByType(App.Companion.getAddressBookAccountType())) { - LocalAddressBook addressBook = new LocalAddressBook(context, account, provider); - if (mainAccount == null || addressBook.getMainAccount().equals(mainAccount)) - result.add(addressBook); - } - - return result.toArray(new LocalAddressBook[result.size()]); - } - - public static LocalAddressBook findByUid(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount, String uid) throws ContactsStorageException { - AccountManager accountManager = AccountManager.get(context); - - for (Account account : accountManager.getAccountsByType(App.Companion.getAddressBookAccountType())) { - LocalAddressBook addressBook = new LocalAddressBook(context, account, provider); - if (addressBook.getURL().equals(uid) && (mainAccount == null || addressBook.getMainAccount().equals(mainAccount))) - return addressBook; - } - - return null; - } - - public static LocalAddressBook create(@NonNull Context context, @NonNull ContentProviderClient provider, @NonNull Account mainAccount, @NonNull JournalEntity journalEntity) throws ContactsStorageException { - CollectionInfo info = journalEntity.getInfo(); - AccountManager accountManager = AccountManager.get(context); - - Account account = new Account(accountName(mainAccount, info), App.Companion.getAddressBookAccountType()); - if (!accountManager.addAccountExplicitly(account, null, null)) - throw new ContactsStorageException("Couldn't create address book account"); - - setUserData(accountManager, account, mainAccount, info.getUid()); - LocalAddressBook addressBook = new LocalAddressBook(context, account, provider); - addressBook.setMainAccount(mainAccount); - addressBook.setURL(info.getUid()); - - ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); - - return addressBook; - } - - public void update(@NonNull JournalEntity journalEntity) throws AuthenticatorException, OperationCanceledException, IOException, ContactsStorageException, android.accounts.OperationCanceledException { - CollectionInfo info = journalEntity.getInfo(); - final String newAccountName = accountName(getMainAccount(), info); - if (!account.name.equals(newAccountName) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - final AccountManager accountManager = AccountManager.get(context); - AccountManagerFuture future = accountManager.renameAccount(account, newAccountName, new AccountManagerCallback() { - @Override - public void run(AccountManagerFuture future) { - try { - // update raw contacts to new account name - if (provider != null) { - ContentValues values = new ContentValues(1); - values.put(RawContacts.ACCOUNT_NAME, newAccountName); - provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?", - new String[] { account.name, account.type }); - } - } catch(RemoteException e) { - App.Companion.getLog().log(Level.WARNING, "Couldn't re-assign contacts to new account name", e); - } - } - }, null); - account = future.getResult(); - } - - // make sure it will still be synchronized when contacts are updated - ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); - } - - public void delete() { - AccountManager accountManager = AccountManager.get(context); - AndroidCompat.INSTANCE.removeAccount(accountManager, account); - } - - public LocalAddressBook(Context context, Account account, ContentProviderClient provider) { - super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE); - this.context = context; - } - - @NonNull - public LocalContact findContactByUID(String uid) throws ContactsStorageException, FileNotFoundException { - LocalContact[] contacts = (LocalContact[]) queryContacts(LocalContact.COLUMN_UID + "=?", new String[]{uid}); - if (contacts.length == 0) - throw new FileNotFoundException(); - return contacts[0]; - } - - /** - * Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0). - */ - @Override - @NonNull - public LocalResource[] getDeleted() throws ContactsStorageException { - List deleted = new LinkedList<>(); - Collections.addAll(deleted, getDeletedContacts()); - if (includeGroups) - Collections.addAll(deleted, getDeletedGroups()); - return deleted.toArray(new LocalResource[deleted.size()]); - } - - /** - * Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e. - * if they're "really dirty" (= data has changed, not only metadata, which is not hashed). - * The DIRTY flag is removed from contacts which are not "really dirty", i.e. from contacts - * whose contact data checksum has not changed. - * @return number of "really dirty" contacts - */ - public int verifyDirty() throws ContactsStorageException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.Companion.getLog().severe("verifyDirty() should not be called on Android <7"); - - int reallyDirty = 0; - for (LocalContact contact : getDirtyContacts()) { - try { - int lastHash = contact.getLastHashCode(), - currentHash = contact.dataHashCode(); - if (lastHash == currentHash) { - // hash is code still the same, contact is not "really dirty" (only metadata been have changed) - App.Companion.getLog().log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact); - contact.resetDirty(); - } else { - App.Companion.getLog().log(Level.FINE, "Contact data has changed from hash " + lastHash + " to " + currentHash, contact); - reallyDirty++; - } - } catch(FileNotFoundException e) { - throw new ContactsStorageException("Couldn't calculate hash code", e); - } - } - - if (includeGroups) - reallyDirty += getDirtyGroups().length; - - return reallyDirty; - } - - /** - * Returns an array of local contacts/groups which have been changed locally (DIRTY != 0). - */ - @Override - @NonNull - public LocalResource[] getDirty() throws ContactsStorageException { - List dirty = new LinkedList<>(); - Collections.addAll(dirty, getDirtyContacts()); - if (includeGroups) - Collections.addAll(dirty, getDirtyGroups()); - return dirty.toArray(new LocalResource[dirty.size()]); - } - - /** - * Returns an array of local contacts which don't have a file name yet. - */ - @Override - @NonNull - public LocalResource[] getWithoutFileName() throws ContactsStorageException { - List nameless = new LinkedList<>(); - Collections.addAll(nameless, (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null)); - if (includeGroups) - Collections.addAll(nameless, (LocalGroup[])queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null)); - return nameless.toArray(new LocalResource[nameless.size()]); - } - - @Override - public LocalResource getByUid(String uid) throws ContactsStorageException { - LocalContact[] ret = (LocalContact[]) queryContacts(AndroidContact.COLUMN_FILENAME + " =? ", new String[]{uid}); - if (ret != null && ret.length > 0) { - return ret[0]; - } - return null; - } - - @Override - public long count() throws ContactsStorageException { - try { - Cursor cursor = provider.query(syncAdapterURI(RawContacts.CONTENT_URI), - null, - null, null, null); - try { - return cursor.getCount(); - } finally { - cursor.close(); - } - } catch (RemoteException e) { - throw new ContactsStorageException("Couldn't query contacts", e); - } - } - - @NonNull - public LocalContact[] getDeletedContacts() throws ContactsStorageException { - return (LocalContact[])queryContacts(RawContacts.DELETED + "!= 0", null); - } - - @NonNull - public LocalContact[] getDirtyContacts() throws ContactsStorageException { - return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null); - } - - @NonNull - public LocalContact[] getAll() throws ContactsStorageException { - return (LocalContact[])queryContacts(RawContacts.DELETED + "== 0", null); - } - - @NonNull - public LocalGroup[] getDeletedGroups() throws ContactsStorageException { - return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null); - } - - @NonNull - public LocalGroup[] getDirtyGroups() throws ContactsStorageException { - return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null); - } - - @NonNull LocalContact[] getByGroupMembership(long groupID) throws ContactsStorageException { - try { - Cursor cursor = provider.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI), - new String[] { RawContacts.Data.RAW_CONTACT_ID }, - "(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)", - new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupID), CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupID) }, - null); - - Set ids = new HashSet<>(); - while (cursor != null && cursor.moveToNext()) - ids.add(cursor.getLong(0)); - - cursor.close(); - - LocalContact[] contacts = new LocalContact[ids.size()]; - int i = 0; - for (Long id : ids) - contacts[i++] = new LocalContact(this, id, null, null); - return contacts; - } catch (RemoteException e) { - throw new ContactsStorageException("Couldn't query contacts", e); - } - } - - - public void deleteAll() throws ContactsStorageException { - try { - provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null); - provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null); - } catch(RemoteException e) { - throw new ContactsStorageException("Couldn't delete all local contacts and groups", e); - } - } - - - /** - * 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 - */ - public long findOrCreateGroup(@NonNull String title) throws ContactsStorageException { - try { - Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI), - new String[] { Groups._ID }, - Groups.TITLE + "=?", new String[] { title }, - null); - try { - if (cursor != null && cursor.moveToNext()) - return cursor.getLong(0); - } finally { - cursor.close(); - } - - ContentValues values = new ContentValues(); - values.put(Groups.TITLE, title); - Uri uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values); - return ContentUris.parseId(uri); - } catch(RemoteException e) { - throw new ContactsStorageException("Couldn't find local contact group", e); - } - } - - public void removeEmptyGroups() throws ContactsStorageException { - // find groups without members - /** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */ - for (LocalGroup group : (LocalGroup[])queryGroups(null, null)) - if (group.getMembers().length == 0) { - App.Companion.getLog().log(Level.FINE, "Deleting group", group); - group.delete(); - } - } - - public void removeGroups() throws ContactsStorageException { - try { - provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null); - } catch(RemoteException e) { - throw new ContactsStorageException("Couldn't remove all groups", e); - } - } - - - // SETTINGS - - // XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work. - public static void setUserData(@NonNull AccountManager accountManager, @NonNull Account account, @NonNull Account mainAccount, @NonNull String url) { - 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); - } - - public Account getMainAccount() throws ContactsStorageException { - AccountManager accountManager = AccountManager.get(context); - String name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME), - type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE); - if (name != null && type != null) - return new Account(name, type); - else - throw new ContactsStorageException("Address book doesn't exist anymore"); - } - - public void setMainAccount(@NonNull Account mainAccount) throws ContactsStorageException { - AccountManager accountManager = AccountManager.get(context); - accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name); - accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type); - } - - public String getURL() throws ContactsStorageException { - AccountManager accountManager = AccountManager.get(context); - return accountManager.getUserData(account, USER_DATA_URL); - } - - public void setURL(String url) throws ContactsStorageException { - AccountManager accountManager = AccountManager.get(context); - accountManager.setUserData(account, USER_DATA_URL, url); - } - - // HELPERS - - public static String accountName(@NonNull Account mainAccount, @NonNull CollectionInfo info) { - String displayName = (info.getDisplayName() != null) ? info.getDisplayName() : info.getUid(); - StringBuilder sb = new StringBuilder(displayName); - sb .append(" (") - .append(mainAccount.name) - .append(" ") - .append(info.getUid().substring(0, 4)) - .append(")"); - return sb.toString(); - } - - /** Fix all of the etags of all of the non-dirty contacts to be non-null. - * Currently set to all ones. */ - public void fixEtags() throws ContactsStorageException { - String newEtag = "1111111111111111111111111111111111111111111111111111111111111111"; - String where = ContactsContract.RawContacts.DIRTY + "=0 AND " + AndroidContact.COLUMN_ETAG + " IS NULL"; - - ContentValues values = new ContentValues(1); - values.put(AndroidContact.COLUMN_ETAG, newEtag); - try { - int fixed = provider.update(syncAdapterURI(RawContacts.CONTENT_URI), - values, where, null); - App.Companion.getLog().info("Fixed entries: " + String.valueOf(fixed)); - } catch (RemoteException e) { - throw new ContactsStorageException("Couldn't query contacts", e); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt new file mode 100644 index 00000000..ed095812 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -0,0 +1,432 @@ +/* + * Copyright © 2013 – 2015 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 android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AccountManagerCallback +import android.accounts.AccountManagerFuture +import android.accounts.AuthenticatorException +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.RemoteException +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import android.provider.ContactsContract.Groups +import android.provider.ContactsContract.RawContacts +import android.support.v4.os.OperationCanceledException + +import com.etesync.syncadapter.App +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.utils.AndroidCompat + +import java.io.FileNotFoundException +import java.io.IOException +import java.util.Collections +import java.util.HashSet +import java.util.LinkedList +import java.util.logging.Level + +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidGroup +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() + + /** + * Whether contact groups (LocalGroup resources) are included in query results for + * [.getDeleted], [.getDirty] and + * [.getWithoutFileName]. + */ + 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 + + 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") + } + @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) + } + + 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) + } + + @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) { + val accountManager = AccountManager.get(context) + val future = accountManager.renameAccount(account, newAccountName, { + try { + // update raw contacts to new account name + if (provider != null) { + val values = ContentValues(1) + values.put(RawContacts.ACCOUNT_NAME, newAccountName) + provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?", + arrayOf(account.name, account.type)) + } + } catch (e: RemoteException) { + App.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e) + } + }, null) + account = future.result + } + + // 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) + } + + @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] + } + + /** + * Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e. + * if they're "really dirty" (= data has changed, not only metadata, which is not hashed). + * The DIRTY flag is removed from contacts which are not "really dirty", i.e. from contacts + * 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") + + 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) + } + + } + + if (includeGroups) + reallyDirty += dirtyGroups.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 + } + + @Throws(ContactsStorageException::class) + override fun count(): Long { + try { + val cursor = provider.query(syncAdapterURI(RawContacts.CONTENT_URI), null, null, null, null) + try { + return cursor.count.toLong() + } finally { + cursor.close() + } + } catch (e: RemoteException) { + throw ContactsStorageException("Couldn't query contacts", e) + } + + } + + @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) + } catch (e: RemoteException) { + throw ContactsStorageException("Couldn't delete all local contacts and groups", e) + } + + } + + + /** + * 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 + */ + @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) + } + + } + + @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) + } + + } + + /** 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" + + val values = ContentValues(1) + values.put(AndroidContact.COLUMN_ETAG, newEtag) + try { + val fixed = provider.update(syncAdapterURI(RawContacts.CONTENT_URI), + values, where, null) + App.log.info("Fixed entries: " + fixed.toString()) + } catch (e: RemoteException) { + throw ContactsStorageException("Couldn't query contacts", e) + } + + } + + 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.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.java deleted file mode 100644 index d74429b5..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 android.accounts.Account; -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.support.annotation.NonNull; -import android.text.TextUtils; - -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.List; - -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; - -public class LocalCalendar extends AndroidCalendar implements LocalCollection { - - public static final int defaultColor = 0xFF8bc34a; // light green 500 - - public static final String COLUMN_CTAG = Calendars.CAL_SYNC1; - - static String[] BASE_INFO_COLUMNS = new String[] { - Events._ID, - Events._SYNC_ID, - LocalEvent.COLUMN_ETAG - }; - - @Override - protected String[] eventBaseInfoColumns() { - return BASE_INFO_COLUMNS; - } - - - protected LocalCalendar(Account account, ContentProviderClient provider, long id) { - super(account, provider, LocalEvent.Factory.INSTANCE, id); - } - - public static Uri create(@NonNull Account account, @NonNull ContentProviderClient provider, @NonNull JournalEntity journalEntity) throws CalendarStorageException { - ContentValues 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 create(account, provider, values); - } - - public void update(JournalEntity journalEntity, boolean updateColor) throws CalendarStorageException { - update(valuesFromCollectionInfo(journalEntity, updateColor)); - } - - public static LocalCalendar findByName(Account account, ContentProviderClient provider, AndroidCalendarFactory factory, String name) throws FileNotFoundException, CalendarStorageException { - AndroidCalendar ret[] = LocalCalendar.find(account, provider, factory, Calendars.NAME + "==?", new String[]{name}); - if (ret.length == 1) { - return (LocalCalendar) ret[0]; - } else { - App.Companion.getLog().severe("No calendar found for name " + name); - return null; - } - } - - private static ContentValues valuesFromCollectionInfo(JournalEntity journalEntity, boolean withColor) { - CollectionInfo info = journalEntity.getInfo(); - ContentValues values = new ContentValues(); - values.put(Calendars.NAME, info.getUid()); - values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getDisplayName()); - - if (withColor) - values.put(Calendars.CALENDAR_COLOR, info.getColor() != null ? info.getColor() : 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.getTimeZone())) { - VTimeZone timeZone = DateUtils.parseVTimeZone(info.getTimeZone()); - if (timeZone != null && timeZone.getTimeZoneId() != null) - values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.getTimeZoneId().getValue())); - } - values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT); - values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(new int[] { Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY }, ",")); - values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(new int[] { CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE }, ", ")); - return values; - } - - @Override - public LocalEvent[] getDeleted() throws CalendarStorageException { - return (LocalEvent[])queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null); - } - - @Override - public LocalEvent[] getWithoutFileName() throws CalendarStorageException { - return (LocalEvent[])queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null); - } - - - public LocalEvent[] getAll() throws CalendarStorageException { - return (LocalEvent[])queryEvents(null, null); - } - - @Override - public LocalEvent getByUid(String uid) throws CalendarStorageException { - LocalEvent[] ret = (LocalEvent[]) queryEvents(Events._SYNC_ID + " =? ", new String[]{uid}); - if (ret != null && ret.length > 0) { - return ret[0]; - } - return null; - } - - @Override - public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException { - List dirty = new LinkedList<>(); - - // get dirty events which are required to have an increased SEQUENCE value - for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null)) { - if (event.getEvent().sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created) - event.getEvent().sequence = 0; - else if (event.weAreOrganizer) - event.getEvent().sequence++; - dirty.add(event); - } - - return dirty.toArray(new LocalResource[dirty.size()]); - } - - @SuppressWarnings("Recycle") - public void processDirtyExceptions() throws CalendarStorageException { - // process deleted exceptions - App.Companion.getLog().info("Processing deleted exceptions"); - try { - Cursor cursor = provider.query( - syncAdapterURI(Events.CONTENT_URI), - new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE }, - Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null); - while (cursor != null && cursor.moveToNext()) { - App.Companion.getLog().fine("Found deleted exception, removing; then re-schuling original event"); - long id = cursor.getLong(0), // can't be null (by definition) - originalID = cursor.getLong(1); // can't be null (by query) - - // get original event's SEQUENCE - Cursor cursor2 = provider.query( - syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)), - new String[] { LocalEvent.COLUMN_SEQUENCE }, - null, null, null); - int originalSequence = (cursor2 == null || cursor2.isNull(0)) ? 0 : cursor2.getInt(0); - - cursor2.close(); - BatchOperation batch = new BatchOperation(provider); - // re-schedule original event and set it to DIRTY - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID))) - .withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1) - .withValue(Events.DIRTY, 1) - )); - // remove exception - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))) - )); - batch.commit(); - } - cursor.close(); - } catch (RemoteException e) { - throw new CalendarStorageException("Couldn't process locally modified exception", e); - } - - // process dirty exceptions - App.Companion.getLog().info("Processing dirty exceptions"); - try { - Cursor cursor = provider.query( - syncAdapterURI(Events.CONTENT_URI), - new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE }, - Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null); - while (cursor != null && cursor.moveToNext()) { - App.Companion.getLog().fine("Found dirty exception, increasing SEQUENCE to re-schedule"); - long id = cursor.getLong(0), // can't be null (by definition) - originalID = cursor.getLong(1); // can't be null (by query) - int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2); - - BatchOperation batch = new BatchOperation(provider); - // original event to DIRTY - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID))) - .withValue(Events.DIRTY, 1) - )); - // increase SEQUENCE and set DIRTY to 0 - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))) - .withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1) - .withValue(Events.DIRTY, 0) - )); - batch.commit(); - } - cursor.close(); - } catch (RemoteException e) { - throw new CalendarStorageException("Couldn't process locally modified exception", e); - } - } - - @Override - public long count() throws CalendarStorageException { - String where = Events.CALENDAR_ID + "=?"; - String whereArgs[] = {String.valueOf(id)}; - - try { - Cursor cursor = provider.query( - syncAdapterURI(Events.CONTENT_URI), - null, - where, whereArgs, null); - try { - return cursor.getCount(); - } finally { - cursor.close(); - } - } catch (RemoteException e) { - throw new CalendarStorageException("Couldn't query calendar events", e); - } - } - - public static class Factory implements AndroidCalendarFactory { - public static final Factory INSTANCE = new Factory(); - - @Override - public AndroidCalendar newInstance(Account account, ContentProviderClient provider, long id) { - return new LocalCalendar(account, provider, id); - } - - @Override - public AndroidCalendar[] newArray(int size) { - return new LocalCalendar[size]; - } - } - - /** Fix all of the etags of all of the non-dirty events to be non-null. - * Currently set to all ones.. */ - public void fixEtags() throws CalendarStorageException { - String newEtag = "1111111111111111111111111111111111111111111111111111111111111111"; - String where = Events.CALENDAR_ID + "=? AND " + Events.DIRTY + "=0 AND " + LocalEvent.COLUMN_ETAG + " IS NULL"; - String whereArgs[] = {String.valueOf(id)}; - - ContentValues values = new ContentValues(1); - values.put(LocalEvent.COLUMN_ETAG, newEtag); - try { - int fixed = provider.update(syncAdapterURI(Events.CONTENT_URI), - values, where, whereArgs); - App.Companion.getLog().info("Fixed entries: " + String.valueOf(fixed)); - } catch (RemoteException e) { - throw new CalendarStorageException("Couldn't fix etags", e); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt new file mode 100644 index 00000000..8fc6e64d --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -0,0 +1,284 @@ +/* + * Copyright © 2013 – 2015 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 android.accounts.Account +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 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 at.bitfire.ical4android.AndroidCalendar +import at.bitfire.ical4android.AndroidCalendarFactory +import at.bitfire.ical4android.BatchOperation +import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.ical4android.DateUtils + +class LocalCalendar protected constructor(account: Account, provider: ContentProviderClient, id: Long) : AndroidCalendar(account, provider, LocalEvent.Factory.INSTANCE, id), LocalCollection { + + override val deleted: Array + @Throws(CalendarStorageException::class) + get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array + + override val withoutFileName: Array + @Throws(CalendarStorageException::class) + get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array + + + val all: Array + @Throws(CalendarStorageException::class) + get() = queryEvents(null, null) as Array + + 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() + } + + override fun eventBaseInfoColumns(): Array { + return BASE_INFO_COLUMNS + } + + @Throws(CalendarStorageException::class) + fun update(journalEntity: JournalEntity, updateColor: Boolean) { + update(valuesFromCollectionInfo(journalEntity, updateColor)) + } + + @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 + } + + @Throws(CalendarStorageException::class) + fun processDirtyExceptions() { + // process deleted exceptions + App.log.info("Processing deleted exceptions") + try { + val cursor = provider.query( + syncAdapterURI(Events.CONTENT_URI), + arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE), + Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null) + while (cursor != null && cursor.moveToNext()) { + App.log.fine("Found deleted exception, removing; then re-schuling original event") + val id = cursor.getLong(0) + // can't be null (by definition) + val originalID = cursor.getLong(1) // can't be null (by query) + + // get original event's SEQUENCE + val cursor2 = provider.query( + syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)), + arrayOf(LocalEvent.COLUMN_SEQUENCE), null, null, null) + val originalSequence = if (cursor2 == null || cursor2.isNull(0)) 0 else cursor2.getInt(0) + + cursor2!!.close() + val batch = BatchOperation(provider) + // re-schedule original event and set it to DIRTY + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID))) + .withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1) + .withValue(Events.DIRTY, 1) + )) + // remove exception + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))) + )) + batch.commit() + } + cursor!!.close() + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't process locally modified exception", e) + } + + // process dirty exceptions + App.log.info("Processing dirty exceptions") + try { + val cursor = provider.query( + syncAdapterURI(Events.CONTENT_URI), + arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE), + Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null) + while (cursor != null && cursor.moveToNext()) { + App.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule") + val id = cursor.getLong(0) + // can't be null (by definition) + val originalID = cursor.getLong(1) // can't be null (by query) + val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2) + + val batch = BatchOperation(provider) + // original event to DIRTY + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID))) + .withValue(Events.DIRTY, 1) + )) + // increase SEQUENCE and set DIRTY to 0 + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))) + .withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1) + .withValue(Events.DIRTY, 0) + )) + batch.commit() + } + cursor!!.close() + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't process locally modified exception", e) + } + + } + + @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) + try { + return cursor.count.toLong() + } finally { + cursor.close() + } + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't query calendar events", e) + } + + } + + 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.. */ + @Throws(CalendarStorageException::class) + fun fixEtags() { + val newEtag = "1111111111111111111111111111111111111111111111111111111111111111" + val where = Events.CALENDAR_ID + "=? AND " + Events.DIRTY + "=0 AND " + LocalEvent.COLUMN_ETAG + " IS NULL" + val whereArgs = arrayOf(id.toString()) + + val values = ContentValues(1) + values.put(LocalEvent.COLUMN_ETAG, newEtag) + try { + val fixed = provider.update(syncAdapterURI(Events.CONTENT_URI), + values, where, whereArgs) + App.log.info("Fixed entries: " + fixed.toString()) + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't fix etags", e) + } + + } + + companion object { + + 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 + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.java deleted file mode 100644 index d5c7c88f..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 java.io.FileNotFoundException; - -import at.bitfire.ical4android.CalendarStorageException; -import at.bitfire.vcard4android.ContactsStorageException; - -public interface LocalCollection { - - LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException; - LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException; - /** Dirty *non-deleted* entries */ - LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException; - - LocalResource getByUid(String uid) throws CalendarStorageException, ContactsStorageException; - - long count() throws CalendarStorageException, ContactsStorageException; -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt new file mode 100644 index 00000000..02d1672e --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2013 – 2015 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 java.io.FileNotFoundException + +import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.vcard4android.ContactsStorageException + +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.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.java deleted file mode 100644 index e50c05e7..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 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.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.GroupMembership; -import android.provider.ContactsContract.RawContacts.Data; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.model.UnknownProperties; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; -import java.util.logging.Level; - -import at.bitfire.vcard4android.AndroidAddressBook; -import at.bitfire.vcard4android.AndroidContact; -import at.bitfire.vcard4android.AndroidContactFactory; -import at.bitfire.vcard4android.BatchOperation; -import at.bitfire.vcard4android.CachedGroupMembership; -import at.bitfire.vcard4android.Contact; -import at.bitfire.vcard4android.ContactsStorageException; -import ezvcard.Ezvcard; -import ezvcard.VCardVersion; - -import static at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS; - -public class LocalContact extends AndroidContact implements LocalResource { - static { - Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION; - } - public static final String COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3; - - private boolean saveAsDirty = false; // When true, the resource will be saved as dirty - - protected final Set - cachedGroupMemberships = new HashSet<>(), - groupMemberships = new HashSet<>(); - - - protected LocalContact(AndroidAddressBook addressBook, long id, String uuid, String eTag) { - super(addressBook, id, uuid, eTag); - } - - public LocalContact(AndroidAddressBook addressBook, Contact contact, String uuid, String eTag) { - super(addressBook, contact, uuid, eTag); - } - - public String getUuid() { - // The same now - return getFileName(); - } - - @Override - public boolean isLocalOnly() { - return TextUtils.isEmpty(getETag()); - } - - public void resetDirty() throws ContactsStorageException { - ContentValues values = new ContentValues(1); - values.put(ContactsContract.RawContacts.DIRTY, 0); - try { - addressBook.provider.update(rawContactSyncURI(), values, null, null); - } catch(RemoteException e) { - throw new ContactsStorageException("Couldn't clear dirty flag", e); - } - } - - public void clearDirty(String eTag) throws ContactsStorageException { - try { - ContentValues values = new ContentValues(3); - values.put(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 - int hashCode = dataHashCode(); - values.put(COLUMN_HASHCODE, hashCode); - App.Companion.getLog().finer("Clearing dirty flag with eTag = " + eTag + ", contact hash = " + hashCode); - } - - addressBook.provider.update(rawContactSyncURI(), values, null, null); - - this.eTag = eTag; - } catch (FileNotFoundException|RemoteException e) { - throw new ContactsStorageException("Couldn't clear dirty flag", e); - } - } - - public void prepareForUpload() throws ContactsStorageException { - try { - final String uid = UUID.randomUUID().toString(); - final String newFileName = uid; - - ContentValues values = new ContentValues(2); - values.put(COLUMN_FILENAME, newFileName); - values.put(COLUMN_UID, uid); - addressBook.provider.update(rawContactSyncURI(), values, null, null); - - fileName = newFileName; - } catch (RemoteException e) { - throw new ContactsStorageException("Couldn't update UID", e); - } - } - - @Override - public String getContent() throws IOException, ContactsStorageException { - final Contact contact; - contact = getContact(); - - App.Companion.getLog().log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - contact.write(VCardVersion.V4_0, GROUP_VCARDS, os); - - return os.toString(); - } - - @Override - protected void populateData(String mimeType, ContentValues row) { - switch (mimeType) { - case CachedGroupMembership.CONTENT_ITEM_TYPE: - cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID)); - break; - case GroupMembership.CONTENT_ITEM_TYPE: - groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID)); - break; - case UnknownProperties.CONTENT_ITEM_TYPE: - contact.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES); - break; - } - } - - @Override - protected void insertDataRows(BatchOperation batch) throws ContactsStorageException { - super.insertDataRows(batch); - - if (contact.unknownProperties != null) { - final BatchOperation.Operation op; - final ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI()); - if (id == null) { - op = new BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0); - } else { - op = new BatchOperation.Operation(builder); - builder.withValue(UnknownProperties.RAW_CONTACT_ID, id); - } - builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE) - .withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties); - batch.enqueue(op); - } - - } - - public int updateAsDirty(Contact contact) throws ContactsStorageException { - saveAsDirty = true; - return this.update(contact); - } - - public Uri createAsDirty() throws ContactsStorageException { - saveAsDirty = true; - return this.create(); - } - - @Override - protected void buildContact(ContentProviderOperation.Builder builder, boolean update) { - super.buildContact(builder, update); - builder.withValue(ContactsContract.RawContacts.DIRTY, saveAsDirty ? 1 : 0); - } - - /** - * Calculates a hash code from the contact's data (VCard) and group memberships. - * Attention: re-reads {@link #contact} from the database, discarding all changes in memory - * @return hash code of contact data (including group memberships) - */ - protected int dataHashCode() throws FileNotFoundException, ContactsStorageException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.Companion.getLog().severe("dataHashCode() should not be called on Android <7"); - - // reset contact so that getContact() reads from database - contact = null; - - // groupMemberships is filled by getContact() - int dataHash = getContact().hashCode(), - groupHash = groupMemberships.hashCode(); - App.Companion.getLog().finest("Calculated data hash = " + dataHash + ", group memberships hash = " + groupHash); - return dataHash ^ groupHash; - } - - public void updateHashCode(@Nullable BatchOperation batch) throws ContactsStorageException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.Companion.getLog().severe("updateHashCode() should not be called on Android <7"); - - ContentValues values = new ContentValues(1); - try { - int hashCode = dataHashCode(); - App.Companion.getLog().fine("Storing contact hash = " + hashCode); - values.put(COLUMN_HASHCODE, hashCode); - - if (batch == null) - addressBook.provider.update(rawContactSyncURI(), values, null, null); - else { - ContentProviderOperation.Builder builder = ContentProviderOperation - .newUpdate(rawContactSyncURI()) - .withValues(values); - batch.enqueue(new BatchOperation.Operation(builder)); - } - } catch(FileNotFoundException|RemoteException e) { - throw new ContactsStorageException("Couldn't store contact checksum", e); - } - } - - public int getLastHashCode() throws ContactsStorageException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - App.Companion.getLog().severe("getLastHashCode() should not be called on Android <7"); - - try { - Cursor c = addressBook.provider.query(rawContactSyncURI(), new String[] { COLUMN_HASHCODE }, null, null, null); - try { - if (c == null || !c.moveToNext() || c.isNull(0)) - return 0; - return c.getInt(0); - } finally { - if (c != null) - c.close(); - } - } catch(RemoteException e) { - throw new ContactsStorageException("Could't read last hash code", e); - } - } - - - public void addToGroup(BatchOperation batch, long groupID) { - assertID(); - - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newInsert(dataSyncURI()) - .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) - .withValue(GroupMembership.RAW_CONTACT_ID, id) - .withValue(GroupMembership.GROUP_ROW_ID, groupID) - )); - groupMemberships.add(groupID); - - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newInsert(dataSyncURI()) - .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) - .withValue(CachedGroupMembership.RAW_CONTACT_ID, id) - .withValue(CachedGroupMembership.GROUP_ID, groupID) - .withYieldAllowed(true) - )); - cachedGroupMemberships.add(groupID); - } - - public void removeGroupMemberships(BatchOperation batch) { - assertID(); - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newDelete(dataSyncURI()) - .withSelection( - Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)", - new String[] { String.valueOf(id), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE } - ) - .withYieldAllowed(true) - )); - groupMemberships.clear(); - cachedGroupMemberships.clear(); - } - - /** - * Returns the IDs of all groups the contact was member of (cached memberships). - * Cached memberships are kept in sync with memberships by DAVdroid and are used to determine - * whether a membership has been deleted/added when a raw contact is dirty. - * @return set of {@link GroupMembership#GROUP_ROW_ID} (may be empty) - * @throws ContactsStorageException on contact provider errors - * @throws FileNotFoundException if the current contact can't be found - */ - @NonNull - public Set getCachedGroupMemberships() throws ContactsStorageException, FileNotFoundException { - getContact(); - return cachedGroupMemberships; - } - - /** - * Returns the IDs of all groups the contact is member of. - * @return set of {@link GroupMembership#GROUP_ROW_ID}s (may be empty) - * @throws ContactsStorageException on contact provider errors - * @throws FileNotFoundException if the current contact can't be found - */ - @NonNull - public Set getGroupMemberships() throws ContactsStorageException, FileNotFoundException { - getContact(); - return groupMemberships; - } - - - // factory - - static class Factory extends AndroidContactFactory { - static final Factory INSTANCE = new Factory(); - - @Override - public LocalContact newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) { - return new LocalContact(addressBook, id, fileName, eTag); - } - - @Override - public LocalContact[] newArray(int size) { - return new LocalContact[size]; - } - - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt new file mode 100644 index 00000000..de184790 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt @@ -0,0 +1,332 @@ +/* + * Copyright © 2013 – 2015 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 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.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import android.provider.ContactsContract.RawContacts.Data +import android.text.TextUtils + +import com.etesync.syncadapter.App +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.model.UnknownProperties + +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.util.HashSet +import java.util.UUID +import java.util.logging.Level + +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidContactFactory +import at.bitfire.vcard4android.BatchOperation +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.ContactsStorageException +import ezvcard.Ezvcard +import ezvcard.VCardVersion + +import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS + +class LocalContact : AndroidContact, LocalResource { + + private var saveAsDirty = false // When true, the resource will be saved as dirty + + internal val cachedGroupMemberships: MutableSet = HashSet() + internal val groupMemberships: MutableSet = HashSet() + + override// The same now + val uuid: String? + get() = fileName + + override val isLocalOnly: Boolean + get() = TextUtils.isEmpty(eTag) + + override val content: String + @Throws(IOException::class, ContactsStorageException::class) + get() { + val contact: Contact + contact = this.contact!! + + App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact) + + val os = ByteArrayOutputStream() + contact.write(VCardVersion.V4_0, GROUP_VCARDS, os) + + return os.toString() + } + + val lastHashCode: Int + @Throws(ContactsStorageException::class) + get() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + App.log.severe("getLastHashCode() should not be called on Android <7") + + try { + val c = addressBook.provider.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null) + try { + return if (c == null || !c.moveToNext() || c.isNull(0)) 0 else c.getInt(0) + } finally { + c?.close() + } + } catch (e: RemoteException) { + throw ContactsStorageException("Could't read last hash code", e) + } + + } + + + constructor(addressBook: AndroidAddressBook, id: Long, uuid: String?, eTag: String?) : super(addressBook, id, uuid, eTag) {} + + constructor(addressBook: AndroidAddressBook, 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) + } + + } + + @Throws(ContactsStorageException::class) + override fun clearDirty(eTag: String) { + try { + val values = ContentValues(3) + values.put(AndroidContact.COLUMN_ETAG, eTag) + values.put(ContactsContract.RawContacts.DIRTY, 0) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // workaround for Android 7 which sets DIRTY flag when only meta-data is changed + val hashCode = dataHashCode() + values.put(COLUMN_HASHCODE, hashCode) + App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode") + } + + addressBook.provider.update(rawContactSyncURI(), values, null, null) + + this.eTag = eTag + } catch (e: FileNotFoundException) { + throw ContactsStorageException("Couldn't clear dirty flag", e) + } catch (e: RemoteException) { + throw ContactsStorageException("Couldn't clear dirty flag", e) + } + + } + + @Throws(ContactsStorageException::class) + override fun prepareForUpload() { + try { + val uid = UUID.randomUUID().toString() + + val values = ContentValues(2) + 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) + } + + } + + 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) + } + } + + @Throws(ContactsStorageException::class) + override fun insertDataRows(batch: BatchOperation) { + super.insertDataRows(batch) + + if (contact.unknownProperties != null) { + val op: BatchOperation.Operation + val builder = ContentProviderOperation.newInsert(dataSyncURI()) + if (id == null) { + op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0) + } else { + op = BatchOperation.Operation(builder) + builder.withValue(UnknownProperties.RAW_CONTACT_ID, id) + } + builder.withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE) + .withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties) + batch.enqueue(op) + } + + } + + @Throws(ContactsStorageException::class) + fun updateAsDirty(contact: Contact): Int { + saveAsDirty = true + return this.update(contact) + } + + @Throws(ContactsStorageException::class) + fun createAsDirty(): Uri { + saveAsDirty = true + return this.create() + } + + override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) { + super.buildContact(builder, update) + builder.withValue(ContactsContract.RawContacts.DIRTY, if (saveAsDirty) 1 else 0) + } + + /** + * 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 + * @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") + + // reset contact so that getContact() reads from database + contact = null + + // groupMemberships is filled by getContact() + val dataHash = getContact().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") + + val values = ContentValues(1) + try { + val hashCode = dataHashCode() + App.log.fine("Storing contact hash = $hashCode") + values.put(COLUMN_HASHCODE, hashCode) + + if (batch == null) + addressBook.provider.update(rawContactSyncURI(), values, null, null) + else { + val builder = ContentProviderOperation + .newUpdate(rawContactSyncURI()) + .withValues(values) + batch.enqueue(BatchOperation.Operation(builder)) + } + } catch (e: FileNotFoundException) { + throw ContactsStorageException("Couldn't store contact checksum", e) + } catch (e: RemoteException) { + throw ContactsStorageException("Couldn't store contact checksum", e) + } + + } + + + fun addToGroup(batch: BatchOperation, groupID: Long) { + assertID() + + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newInsert(dataSyncURI()) + .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) + .withValue(GroupMembership.RAW_CONTACT_ID, id) + .withValue(GroupMembership.GROUP_ROW_ID, groupID) + )) + groupMemberships.add(groupID) + + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newInsert(dataSyncURI()) + .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + .withValue(CachedGroupMembership.RAW_CONTACT_ID, id) + .withValue(CachedGroupMembership.GROUP_ID, groupID) + .withYieldAllowed(true) + )) + cachedGroupMemberships.add(groupID) + } + + fun removeGroupMemberships(batch: BatchOperation) { + assertID() + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newDelete(dataSyncURI()) + .withSelection( + Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)", + arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + ) + .withYieldAllowed(true) + )) + groupMemberships.clear() + cachedGroupMemberships.clear() + } + + /** + * Returns the IDs of all groups the contact was member of (cached memberships). + * Cached memberships are kept in sync with memberships by DAVdroid and are used to determine + * whether a membership has been deleted/added when a raw contact is dirty. + * @return set of [GroupMembership.GROUP_ROW_ID] (may be empty) + * @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() + return cachedGroupMemberships + } + + /** + * Returns the IDs of all groups the contact is member of. + * @return set of [GroupMembership.GROUP_ROW_ID]s (may be empty) + * @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() + 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() + } + + } + + 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.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.java deleted file mode 100644 index 2c7c4f7e..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 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.support.annotation.NonNull; -import android.text.TextUtils; - -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.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; - -@TargetApi(17) -public class LocalEvent extends AndroidEvent implements LocalResource { - static { - Event.prodId = new ProdId(Constants.PRODID_BASE + " ical4j/2.x"); - } - static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1, - COLUMN_UID = Build.VERSION.SDK_INT >= 17 ? Events.UID_2445 : Events.SYNC_DATA2, - COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3; - - private boolean saveAsDirty = false; // When true, the resource will be saved as dirty - - private String fileName; - protected String eTag; - - private String getFileName() { - return fileName; - } - - public String getETag() { - return eTag; - } - - public void setETag(String eTag) { - this.eTag = eTag; - } - - public boolean weAreOrganizer = true; - - public LocalEvent(@NonNull AndroidCalendar calendar, Event event, String fileName, String eTag) { - super(calendar, event); - this.fileName = fileName; - this.eTag = eTag; - } - - protected LocalEvent(@NonNull AndroidCalendar calendar, long id, ContentValues baseInfo) { - super(calendar, id, baseInfo); - if (baseInfo != null) { - fileName = baseInfo.getAsString(Events._SYNC_ID); - eTag = baseInfo.getAsString(COLUMN_ETAG); - } - } - - @Override - public String getContent() throws IOException, ContactsStorageException, CalendarStorageException { - App.Companion.getLog().log(Level.FINE, "Preparing upload of event " + getFileName(), getEvent()); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - getEvent().write(os); - - return os.toString(); - } - - @Override - public boolean isLocalOnly() { - return TextUtils.isEmpty(getETag()); - } - - @Override - public String getUuid() { - // Now the same - return getFileName(); - } - - /* process LocalEvent-specific fields */ - - @Override - protected void populateEvent(ContentValues values) { - super.populateEvent(values); - fileName = values.getAsString(Events._SYNC_ID); - eTag = values.getAsString(COLUMN_ETAG); - event.uid = values.getAsString(COLUMN_UID); - - event.sequence = values.getAsInteger(COLUMN_SEQUENCE); - if (Build.VERSION.SDK_INT >= 17) { - Integer isOrganizer = values.getAsInteger(Events.IS_ORGANIZER); - weAreOrganizer = isOrganizer != null && isOrganizer != 0; - } else { - String organizer = values.getAsString(Events.ORGANIZER); - weAreOrganizer = organizer == null || organizer.equals(calendar.account.name); - } - } - - @Override - protected void buildEvent(Event recurrence, ContentProviderOperation.Builder builder) { - super.buildEvent(recurrence, builder); - - boolean buildException = recurrence != null; - Event eventToBuild = buildException ? recurrence : event; - - builder.withValue(COLUMN_UID, event.uid) - .withValue(COLUMN_SEQUENCE, eventToBuild.sequence) - .withValue(CalendarContract.Events.DIRTY, saveAsDirty ? 1 : 0) - .withValue(CalendarContract.Events.DELETED, 0); - - if (buildException) - builder.withValue(Events.ORIGINAL_SYNC_ID, fileName); - else - builder.withValue(Events._SYNC_ID, fileName) - .withValue(COLUMN_ETAG, eTag); - } - - public Uri addAsDirty() throws CalendarStorageException { - saveAsDirty = true; - return this.add(); - } - - public Uri updateAsDirty(Event event) throws CalendarStorageException { - saveAsDirty = true; - return this.update(event); - } - - /* custom queries */ - - public void prepareForUpload() throws CalendarStorageException { - try { - String uid = null; - Cursor c = calendar.provider.query(eventSyncURI(), new String[] { COLUMN_UID }, null, null, null); - if (c.moveToNext()) - uid = c.getString(0); - if (uid == null) - uid = UUID.randomUUID().toString(); - - c.close(); - final String newFileName = uid; - - ContentValues values = new 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 (RemoteException e) { - throw new CalendarStorageException("Couldn't update UID", e); - } - } - - @Override - public void clearDirty(String eTag) throws CalendarStorageException { - try { - ContentValues values = new 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 (RemoteException e) { - throw new CalendarStorageException("Couldn't update UID", e); - } - } - - static class Factory implements AndroidEventFactory { - static final Factory INSTANCE = new Factory(); - - @Override - public AndroidEvent newInstance(AndroidCalendar calendar, long id, ContentValues baseInfo) { - return new LocalEvent(calendar, id, baseInfo); - } - - @Override - public AndroidEvent newInstance(AndroidCalendar calendar, Event event) { - return new LocalEvent(calendar, event, null, null); - } - - @Override - public AndroidEvent[] newArray(int size) { - return new LocalEvent[size]; - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt new file mode 100644 index 00000000..dc90e2af --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt @@ -0,0 +1,202 @@ +/* + * Copyright © 2013 – 2015 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 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 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.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 + +@TargetApi(17) +class LocalEvent : AndroidEvent, LocalResource { + + private var saveAsDirty = false // When true, the resource will be saved as dirty + + private var fileName: String? = null + var eTag: String? = null + + 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()) + + val os = ByteArrayOutputStream() + getEvent().write(os) + + return os.toString() + } + + override val isLocalOnly: Boolean + get() = TextUtils.isEmpty(eTag) + + override// Now the same + val uuid: String? + get() = fileName + + 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) + } + } + + /* process LocalEvent-specific fields */ + + override fun populateEvent(values: ContentValues) { + super.populateEvent(values) + fileName = values.getAsString(Events._SYNC_ID) + eTag = values.getAsString(COLUMN_ETAG) + event.uid = values.getAsString(COLUMN_UID) + + event.sequence = values.getAsInteger(COLUMN_SEQUENCE) + if (Build.VERSION.SDK_INT >= 17) { + val isOrganizer = values.getAsInteger(Events.IS_ORGANIZER) + weAreOrganizer = isOrganizer != null && isOrganizer != 0 + } else { + val organizer = values.getAsString(Events.ORGANIZER) + weAreOrganizer = organizer == null || organizer == calendar.account.name + } + } + + override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) { + super.buildEvent(recurrence, builder) + + val buildException = recurrence != null + val eventToBuild = if (buildException) recurrence else event + + 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) + + if (buildException) + builder.withValue(Events.ORIGINAL_SYNC_ID, fileName) + else + builder.withValue(Events._SYNC_ID, fileName) + .withValue(COLUMN_ETAG, eTag) + } + + @Throws(CalendarStorageException::class) + fun addAsDirty(): Uri { + saveAsDirty = true + return this.add() + } + + @Throws(CalendarStorageException::class) + fun updateAsDirty(event: Event): Uri { + saveAsDirty = true + return this.update(event) + } + + /* custom queries */ + + @Throws(CalendarStorageException::class) + override fun prepareForUpload() { + try { + var uid: String? = null + val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) + if (c.moveToNext()) + uid = c.getString(0) + if (uid == null) + uid = UUID.randomUUID().toString() + + c.close() + val newFileName = uid + + val values = ContentValues(2) + values.put(Events._SYNC_ID, newFileName) + values.put(COLUMN_UID, uid) + calendar.provider.update(eventSyncURI(), values, null, null) + + fileName = newFileName + if (event != null) + event.uid = uid + + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't update UID", e) + } + + } + + @Throws(CalendarStorageException::class) + override fun clearDirty(eTag: String) { + try { + val values = ContentValues(2) + values.put(CalendarContract.Events.DIRTY, 0) + values.put(COLUMN_ETAG, eTag) + if (event != null) + values.put(COLUMN_SEQUENCE, event.sequence) + calendar.provider.update(eventSyncURI(), values, null, null) + + this.eTag = eTag + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't update UID", e) + } + + } + + internal class Factory : AndroidEventFactory { + + override fun newInstance(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues): AndroidEvent { + return LocalEvent(calendar, id, baseInfo) + } + + override fun newInstance(calendar: AndroidCalendar, event: Event): AndroidEvent { + return LocalEvent(calendar, event, null, null) + } + + 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 + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.java deleted file mode 100644 index 9277d068..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 android.content.ContentProviderOperation; -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.Cursor; -import android.os.Build; -import android.os.Parcel; -import android.os.RemoteException; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.GroupMembership; -import android.provider.ContactsContract.Groups; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.RawContacts.Data; -import android.text.TextUtils; - -import com.etesync.syncadapter.App; - -import org.apache.commons.lang3.ArrayUtils; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -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 static at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS; - -public class LocalGroup extends AndroidGroup implements LocalResource { - /** marshalled list of member UIDs, as sent by server */ - public static final String COLUMN_PENDING_MEMBERS = Groups.SYNC3; - - public String getUuid() { - return getFileName(); - } - - public LocalGroup(AndroidAddressBook addressBook, long id, String fileName, String eTag) { - super(addressBook, id, fileName, eTag); - } - - public LocalGroup(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) { - super(addressBook, contact, fileName, eTag); - } - - @Override - public String getContent() throws IOException, ContactsStorageException { - final Contact contact; - contact = getContact(); - - App.Companion.getLog().log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - contact.write(VCardVersion.V4_0, GROUP_VCARDS, os); - - return os.toString(); - } - - @Override - public boolean isLocalOnly() { - return TextUtils.isEmpty(getETag()); - } - - @Override - public void clearDirty(String eTag) throws ContactsStorageException { - assertID(); - - ContentValues values = new ContentValues(2); - values.put(Groups.DIRTY, 0); - values.put(COLUMN_ETAG, this.eTag = eTag); - update(values); - - // update cached group memberships - BatchOperation batch = new BatchOperation(addressBook.provider); - - // delete cached group memberships - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) - .withSelection( - CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?", - new String[] { CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) } - ) - )); - - // insert updated cached group memberships - for (long member : getMembers()) - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) - .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) - .withValue(CachedGroupMembership.RAW_CONTACT_ID, member) - .withValue(CachedGroupMembership.GROUP_ID, id) - .withYieldAllowed(true) - )); - - batch.commit(); - } - - @Override - public void prepareForUpload() throws ContactsStorageException { - final String uid = UUID.randomUUID().toString(); - final String newFileName = uid; - - ContentValues values = new ContentValues(2); - values.put(COLUMN_FILENAME, newFileName); - values.put(COLUMN_UID, uid); - update(values); - - fileName = newFileName; - } - - @Override - protected ContentValues contentValues() { - ContentValues values = super.contentValues(); - - Parcel 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. - */ - public void markMembersDirty() throws ContactsStorageException { - assertID(); - BatchOperation batch = new BatchOperation(addressBook.provider); - - for (long member : getMembers()) - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member))) - .withValue(RawContacts.DIRTY, 1) - .withYieldAllowed(true) - )); - - batch.commit(); - } - - /** - * 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 - * @throws ContactsStorageException on contact provider errors - */ - public static void applyPendingMemberships(LocalAddressBook addressBook) throws ContactsStorageException { - try { - Cursor cursor = addressBook.provider.query( - addressBook.syncAdapterURI(Groups.CONTENT_URI), - new String[] { Groups._ID, COLUMN_PENDING_MEMBERS }, - COLUMN_PENDING_MEMBERS + " IS NOT NULL", new String[] {}, - null - ); - - BatchOperation batch = new BatchOperation(addressBook.provider); - while (cursor != null && cursor.moveToNext()) { - long id = cursor.getLong(0); - App.Companion.getLog().fine("Assigning members to group " + id); - - // required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed - Set changeContactIDs = new HashSet<>(); - - // delete all memberships and cached memberships for this group - for (LocalContact contact : addressBook.getByGroupMembership(id)) { - contact.removeGroupMemberships(batch); - changeContactIDs.add(contact.getId()); - } - - // extract list of member UIDs - List members = new LinkedList<>(); - byte[] raw = cursor.getBlob(1); - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(raw, 0, raw.length); - parcel.setDataPosition(0); - parcel.readStringList(members); - parcel.recycle(); - - // insert memberships - for (String uid : members) { - App.Companion.getLog().fine("Assigning member: " + uid); - try { - LocalContact member = addressBook.findContactByUID(uid); - member.addToGroup(batch, id); - changeContactIDs.add(member.getId()); - } catch(FileNotFoundException e) { - App.Companion.getLog().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 (Long contactID : changeContactIDs) { - LocalContact contact = new LocalContact(addressBook, contactID, null, null); - contact.updateHashCode(batch); - } - - // remove pending memberships - batch.enqueue(new BatchOperation.Operation( - ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id))) - .withValue(COLUMN_PENDING_MEMBERS, null) - .withYieldAllowed(true) - )); - - batch.commit(); - } - cursor.close(); - } catch(RemoteException e) { - throw new ContactsStorageException("Couldn't get pending memberships", e); - } - } - - - // helpers - - private void assertID() { - if (id == null) - throw new IllegalStateException("Group has not been saved yet"); - } - - /** - * Lists all members of this group. - * @return list of all members' raw contact IDs - * @throws ContactsStorageException on contact provider errors - */ - protected long[] getMembers() throws ContactsStorageException { - assertID(); - List members = new LinkedList<>(); - try { - Cursor cursor = addressBook.provider.query( - addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), - new String[] { Data.RAW_CONTACT_ID }, - GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", - new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) }, - null - ); - while (cursor != null && cursor.moveToNext()) - members.add(cursor.getLong(0)); - cursor.close(); - } catch(RemoteException e) { - throw new ContactsStorageException("Couldn't list group members", e); - } - return ArrayUtils.toPrimitive(members.toArray(new Long[members.size()])); - } - - @java.lang.Override - @java.lang.SuppressWarnings("all") - public java.lang.String toString() { - return "LocalGroup(super=" + super.toString() + ", uuid=" + this.getUuid() + ")"; - } - - // factory - - static class Factory extends AndroidGroupFactory { - static final Factory INSTANCE = new Factory(); - - @Override - public LocalGroup newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) { - return new LocalGroup(addressBook, id, fileName, eTag); - } - - @Override - public LocalGroup newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) { - return new LocalGroup(addressBook, contact, fileName, eTag); - } - - @Override - public LocalGroup[] newArray(int size) { - return new LocalGroup[size]; - } - - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt new file mode 100644 index 00000000..bc2c30a5 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -0,0 +1,291 @@ +/* + * Copyright © 2013 – 2015 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 android.content.ContentProviderOperation +import android.content.ContentUris +import android.content.ContentValues +import android.database.Cursor +import android.os.Build +import android.os.Parcel +import android.os.RemoteException +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import android.provider.ContactsContract.Groups +import android.provider.ContactsContract.RawContacts +import android.provider.ContactsContract.RawContacts.Data +import android.text.TextUtils + +import com.etesync.syncadapter.App + +import org.apache.commons.lang3.ArrayUtils + +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.util.HashSet +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 { + + override val uuid: String + get() = getFileName() + + override val content: String + @Throws(IOException::class, ContactsStorageException::class) + get() { + val contact: Contact + contact = getContact() + + App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact) + + val os = ByteArrayOutputStream() + contact.write(VCardVersion.V4_0, GROUP_VCARDS, os) + + return os.toString() + } + + override val isLocalOnly: Boolean + get() = TextUtils.isEmpty(getETag()) + + /** + * 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) + } + + return ArrayUtils.toPrimitive(members.toTypedArray()) + } + + constructor(addressBook: AndroidAddressBook, id: Long, fileName: String?, eTag: String?) : super(addressBook, id, fileName, eTag) {} + + constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?) : super(addressBook, contact, fileName, eTag) {} + + @Throws(ContactsStorageException::class) + override fun clearDirty(eTag: String) { + assertID() + + val values = ContentValues(2) + values.put(Groups.DIRTY, 0) + this.eTag = eTag + values.put(AndroidGroup.COLUMN_ETAG, eTag) + update(values) + + // update cached group memberships + val batch = BatchOperation(addressBook.provider) + + // delete cached group memberships + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) + .withSelection( + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?", + arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE, id.toString()) + ) + )) + + // insert updated cached group memberships + for (member in members) + batch.enqueue(BatchOperation.Operation( + ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) + .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + .withValue(CachedGroupMembership.RAW_CONTACT_ID, member) + .withValue(CachedGroupMembership.GROUP_ID, id) + .withYieldAllowed(true) + )) + + batch.commit() + } + + @Throws(ContactsStorageException::class) + override fun prepareForUpload() { + val uid = UUID.randomUUID().toString() + + val values = ContentValues(2) + values.put(AndroidGroup.COLUMN_FILENAME, uid) + values.put(AndroidGroup.COLUMN_UID, uid) + update(values) + + 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() + } + + + // helpers + + private fun assertID() { + if (id == null) + throw IllegalStateException("Group has not been saved yet") + } + + override fun toString(): String { + return "LocalGroup(super=" + super.toString() + ", uuid=" + this.uuid + ")" + } + + // factory + + internal class Factory : AndroidGroupFactory() { + + override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalGroup { + return LocalGroup(addressBook, id, fileName, eTag) + } + + override fun newInstance(addressBook: AndroidAddressBook, contact: Contact, fileName: String, eTag: String): LocalGroup { + return LocalGroup(addressBook, contact, fileName, eTag) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + companion object { + val INSTANCE = Factory() + } + + } + + companion object { + /** marshalled list of member UIDs, as sent by server */ + val COLUMN_PENDING_MEMBERS = Groups.SYNC3 + + /** + * Processes all groups with non-null [.COLUMN_PENDING_MEMBERS]: the pending memberships + * are (if possible) applied, keeping cached memberships in sync. + * @param addressBook address book to take groups from + * @throws ContactsStorageException on contact provider errors + */ + @Throws(ContactsStorageException::class) + fun applyPendingMemberships(addressBook: LocalAddressBook) { + try { + val cursor = addressBook.provider.query( + addressBook.syncAdapterURI(Groups.CONTENT_URI), + arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS), + "$COLUMN_PENDING_MEMBERS IS NOT NULL", arrayOf(), null + ) + + val batch = BatchOperation(addressBook.provider) + while (cursor != null && cursor.moveToNext()) { + val id = cursor.getLong(0) + App.log.fine("Assigning members to group $id") + + // required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed + val changeContactIDs = HashSet() + + // 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.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.java deleted file mode 100644 index 8bc8f284..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 java.io.IOException; - -import at.bitfire.ical4android.CalendarStorageException; -import at.bitfire.vcard4android.ContactsStorageException; - -public interface LocalResource { - String getUuid(); - Long getId(); - - /** True if doesn't exist on server yet, false otherwise. */ - boolean isLocalOnly(); - - /** Returns a string of how this should be represented for example: vCard. */ - String getContent() throws IOException, ContactsStorageException, CalendarStorageException; - - int delete() throws CalendarStorageException, ContactsStorageException; - - void prepareForUpload() throws CalendarStorageException, ContactsStorageException; - void clearDirty(String eTag) throws CalendarStorageException, ContactsStorageException; - -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt new file mode 100644 index 00000000..56aafebf --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt @@ -0,0 +1,34 @@ +/* + * Copyright © 2013 – 2015 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 java.io.IOException + +import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.vcard4android.ContactsStorageException + +interface LocalResource { + val uuid: String? + + /** True if doesn't exist on server yet, false otherwise. */ + val isLocalOnly: Boolean + + /** 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.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.java deleted file mode 100644 index f4bf50d6..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 android.content.ContentProviderOperation; -import android.content.ContentValues; -import android.os.RemoteException; -import android.provider.CalendarContract.Events; -import android.support.annotation.NonNull; - -import com.etesync.syncadapter.Constants; - -import net.fortuna.ical4j.model.property.ProdId; - -import org.dmfs.provider.tasks.TaskContract.Tasks; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.text.ParseException; -import java.util.UUID; - -import at.bitfire.ical4android.AndroidTask; -import at.bitfire.ical4android.AndroidTaskFactory; -import at.bitfire.ical4android.AndroidTaskList; -import at.bitfire.ical4android.CalendarStorageException; -import at.bitfire.ical4android.Task; -import at.bitfire.vcard4android.ContactsStorageException; - -public class LocalTask extends AndroidTask implements LocalResource { - static { - Task.prodId = new ProdId(Constants.PRODID_BASE + " ical4j/2.x"); - } - - protected String uuid; - - static final String COLUMN_ETAG = Tasks.SYNC1, - COLUMN_UID = Tasks.SYNC2, - COLUMN_SEQUENCE = Tasks.SYNC3; - - protected String fileName; - protected String eTag; - - public String getUuid() { - return uuid; - } - - private String getFileName() { - return fileName; - } - - public String getETag() { - return eTag; - } - - public void setETag(String eTag) { - this.eTag = eTag; - } - - public LocalTask(@NonNull AndroidTaskList taskList, Task task, String fileName, String eTag) { - super(taskList, task); - this.fileName = fileName; - this.eTag = eTag; - } - - protected LocalTask(@NonNull AndroidTaskList taskList, long id, ContentValues baseInfo) { - super(taskList, id); - if (baseInfo != null) { - fileName = baseInfo.getAsString(Events._SYNC_ID); - eTag = baseInfo.getAsString(COLUMN_ETAG); - } - } - - @Override - public String getContent() throws IOException, ContactsStorageException { - return null; - } - - @Override - public boolean isLocalOnly() { - return false; - } - - /* process LocalTask-specific fields */ - - @Override - protected void populateTask(ContentValues values) throws FileNotFoundException, RemoteException, ParseException { - 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 - protected void buildTask(ContentProviderOperation.Builder builder, boolean update) { - super.buildTask(builder, update); - builder .withValue(Tasks._SYNC_ID, fileName) - .withValue(COLUMN_UID, task.uid) - .withValue(COLUMN_SEQUENCE, task.sequence) - .withValue(COLUMN_ETAG, eTag); - } - - - /* custom queries */ - - public void prepareForUpload() throws CalendarStorageException { - try { - final String uid = UUID.randomUUID().toString(); - final String newFileName = uid + ".ics"; - - ContentValues values = new 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 (RemoteException e) { - throw new CalendarStorageException("Couldn't update UID", e); - } - } - - @Override - public void clearDirty(String eTag) throws CalendarStorageException { - try { - ContentValues values = new 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 (RemoteException e) { - throw new CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e); - } - } - - - static class Factory implements AndroidTaskFactory { - static final Factory INSTANCE = new Factory(); - - @Override - public LocalTask newInstance(AndroidTaskList taskList, long id, ContentValues baseInfo) { - return new LocalTask(taskList, id, baseInfo); - } - - @Override - public LocalTask newInstance(AndroidTaskList taskList, Task task) { - return new LocalTask(taskList, task, null, null); - } - - @Override - public LocalTask[] newArray(int size) { - return new LocalTask[size]; - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt new file mode 100644 index 00000000..10b42062 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -0,0 +1,152 @@ +/* + * Copyright © 2013 – 2015 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 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 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 + +class LocalTask : AndroidTask, LocalResource { + private var fileName: String? = null + var eTag: String? = null + + override val content: String + @Throws(IOException::class, ContactsStorageException::class) + get() = "" + + override val isLocalOnly: Boolean + get() = false + + override// Now the same + val uuid: String? + get() = fileName + + constructor(taskList: AndroidTaskList, task: Task, fileName: String?, eTag: String?) : 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) + } + } + + /* 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) + .withValue(COLUMN_ETAG, eTag) + } + + + /* custom queries */ + + @Throws(CalendarStorageException::class) + override fun prepareForUpload() { + try { + val uid = UUID.randomUUID().toString() + val newFileName = "$uid.ics" + + val values = ContentValues(2) + values.put(Tasks._SYNC_ID, newFileName) + values.put(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) + } + + } + + @Throws(CalendarStorageException::class) + override fun clearDirty(eTag: String) { + try { + val values = ContentValues(2) + values.put(Tasks._DIRTY, 0) + values.put(COLUMN_ETAG, eTag) + if (task != null) + values.put(COLUMN_SEQUENCE, task.sequence) + taskList.provider.client.update(taskSyncURI(), values, null, null) + + this.eTag = eTag + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e) + } + + } + + + internal class Factory : AndroidTaskFactory { + + override fun newInstance(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues): LocalTask { + return LocalTask(taskList, id, baseInfo) + } + + override fun newInstance(taskList: AndroidTaskList, task: Task): LocalTask { + return LocalTask(taskList, task, null, null) + } + + override fun newArray(size: Int): Array { + 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 + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.java deleted file mode 100644 index 5a328c8b..00000000 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright © 2013 – 2015 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 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 android.support.annotation.NonNull; - -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; - -public class LocalTaskList extends AndroidTaskList implements LocalCollection { - - public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green" - - public static final String COLUMN_CTAG = TaskLists.SYNC_VERSION; - - static String[] BASE_INFO_COLUMNS = new String[] { - Tasks._ID, - Tasks._SYNC_ID, - LocalTask.COLUMN_ETAG - }; - - - @Override - protected String[] taskBaseInfoColumns() { - return BASE_INFO_COLUMNS; - } - - - protected LocalTaskList(Account account, TaskProvider provider, long id) { - super(account, provider, LocalTask.Factory.INSTANCE, id); - } - - public static Uri create(Account account, TaskProvider provider, CollectionInfo info) throws CalendarStorageException { - ContentValues values = valuesFromCollectionInfo(info, true); - values.put(TaskLists.OWNER, account.name); - values.put(TaskLists.SYNC_ENABLED, 1); - values.put(TaskLists.VISIBLE, 1); - return create(account, provider, values); - } - - public void update(CollectionInfo info, boolean updateColor) throws CalendarStorageException { - update(valuesFromCollectionInfo(info, updateColor)); - } - - private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) { - ContentValues values = new ContentValues(); - values.put(TaskLists._SYNC_ID, info.getUid()); - values.put(TaskLists.LIST_NAME, info.getDisplayName()); - - if (withColor) - values.put(TaskLists.LIST_COLOR, info.getColor() != null ? info.getColor() : defaultColor); - - return values; - } - - @Override - public LocalTask[] getDeleted() throws CalendarStorageException { - return (LocalTask[])queryTasks(Tasks._DELETED + "!=0", null); - } - - @Override - public LocalTask[] getWithoutFileName() throws CalendarStorageException { - return (LocalTask[])queryTasks(Tasks._SYNC_ID + " IS NULL", null); - } - - @Override - public LocalTask getByUid(String uid) throws CalendarStorageException { - LocalTask[] ret = (LocalTask[]) queryTasks(Tasks._SYNC_ID + " =? ", new String[]{uid}); - if (ret != null && ret.length > 0) { - return ret[0]; - } - return null; - } - - @Override - public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException { - LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null); - if (tasks != null) - for (LocalTask task : tasks) { - if (task.getTask().sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created) - task.getTask().sequence = 0; - else - task.getTask().sequence++; - } - return tasks; - } - - @Override - public long count() throws CalendarStorageException { - String where = Tasks.LIST_ID + "=?"; - String whereArgs[] = {String.valueOf(getId())}; - - try { - Cursor cursor = provider.client.query( - syncAdapterURI(provider.tasksUri()), - null, - where, whereArgs, null); - try { - return cursor.getCount(); - } finally { - cursor.close(); - } - } catch (RemoteException e) { - throw new CalendarStorageException("Couldn't query calendar events", e); - } - } - - // helpers - - public static boolean tasksProviderAvailable(@NonNull Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - return context.getPackageManager().resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null; - else { - TaskProvider provider = TaskProvider.acquire(context.getContentResolver(), TaskProvider.ProviderName.OpenTasks); - try { - return provider != null; - } finally { - if (provider != null) - provider.close(); - } - } - } - - - public static class Factory implements AndroidTaskListFactory { - public static final Factory INSTANCE = new Factory(); - - @Override - public AndroidTaskList newInstance(Account account, TaskProvider provider, long id) { - return new LocalTaskList(account, provider, id); - } - - @Override - public AndroidTaskList[] newArray(int size) { - return new LocalTaskList[size]; - } - } - - - // HELPERS - - public static void onRenameAccount(@NonNull ContentResolver resolver, @NonNull String oldName, @NonNull String newName) throws RemoteException { - ContentProviderClient client = resolver.acquireContentProviderClient(TaskProvider.ProviderName.OpenTasks.authority); - if (client != null) { - ContentValues values = new ContentValues(1); - values.put(Tasks.ACCOUNT_NAME, newName); - client.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), values, Tasks.ACCOUNT_NAME + "=?", new String[]{oldName}); - client.release(); - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt new file mode 100644 index 00000000..4c0910bf --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -0,0 +1,169 @@ +/* + * Copyright © 2013 – 2015 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 android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.RemoteException + +import com.etesync.syncadapter.model.CollectionInfo + +import org.dmfs.provider.tasks.TaskContract.TaskLists +import org.dmfs.provider.tasks.TaskContract.Tasks + +import java.io.FileNotFoundException + +import at.bitfire.ical4android.AndroidTaskList +import at.bitfire.ical4android.AndroidTaskListFactory +import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.ical4android.TaskProvider + +class LocalTaskList protected constructor(account: Account, provider: TaskProvider, id: Long) : AndroidTaskList(account, provider, LocalTask.Factory.INSTANCE, id), LocalCollection { + + 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++ + } + return tasks + } + + + override fun taskBaseInfoColumns(): Array { + return BASE_INFO_COLUMNS + } + + @Throws(CalendarStorageException::class) + fun update(info: CollectionInfo, updateColor: Boolean) { + update(valuesFromCollectionInfo(info, updateColor)) + } + + @Throws(CalendarStorageException::class) + override fun getByUid(uid: String): LocalTask? { + val ret = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)) as Array + return if (ret != null && ret.size > 0) { + ret[0] + } else null + } + + @Throws(CalendarStorageException::class) + override fun count(): Long { + val where = Tasks.LIST_ID + "=?" + val whereArgs = arrayOf(id.toString()) + + try { + val cursor = provider.client.query( + syncAdapterURI(provider.tasksUri()), null, + where, whereArgs, null) + try { + return cursor.count.toLong() + } finally { + cursor.close() + } + } catch (e: RemoteException) { + throw CalendarStorageException("Couldn't query calendar events", e) + } + + } + + + class Factory : AndroidTaskListFactory { + + override fun newInstance(account: Account, provider: TaskProvider, id: Long): AndroidTaskList { + return LocalTaskList(account, provider, id) + } + + override fun newArray(size: Int): Array { + 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/CalendarSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt index 17c7d194..5244c01a 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt @@ -51,6 +51,7 @@ 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 /** @@ -68,7 +69,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra account.name) init { - localCollection = calendar + localCollection = calendar as LocalCollection } override fun notificationId(): Int { @@ -142,7 +143,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra if (event.attendees.isEmpty()) { return } - createInviteAttendeesNotification(event, local.getContent()) + createInviteAttendeesNotification(event, local.content) } } 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 5b2f7e7c..91082c36 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt @@ -59,7 +59,7 @@ import okhttp3.ResponseBody * 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) @@ -98,7 +98,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) localAddressBook.updateSettings(values) - journal = JournalEntryManager(httpClient, remote, localAddressBook.url) + journal = JournalEntryManager(httpClient, remote, localAddressBook.url!!) localAddressBook.includeGroups = true 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 d515bc1b..bc613fc1 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -60,7 +60,7 @@ constructor(protected val context: Context, protected val account: Account, prot protected val notificationManager: NotificationHelper protected val info: CollectionInfo - protected var localCollection: LocalCollection? = null + protected var localCollection: LocalCollection? = null protected var httpClient: OkHttpClient @@ -409,7 +409,7 @@ constructor(protected val context: Context, protected val account: Account, prot break } App.log.info("Added/changed resource with UUID: " + local.uuid) - local.clearDirty(local.uuid) + local.clearDirty(local.uuid!!) } if (left > 0) { localDirty = Arrays.copyOfRange(localDirty, left, localDirty.size) 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 d26914a4..b7165a4e 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.INSTANCE, info.uid!!) providerClient!!.release() if (resource == null) { return null @@ -188,7 +188,7 @@ class ViewCollectionActivity : BaseActivity(), Refreshable { } else { try { val providerClient = contentResolver.acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI) - val resource = LocalAddressBook.findByUid(this@ViewCollectionActivity, providerClient!!, account, info.uid) + val resource = LocalAddressBook.findByUid(this@ViewCollectionActivity, providerClient!!, account, 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 454fc13e..89c90c8b 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.INSTANCE, 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 372a490d..f93ea582 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 @@ -223,7 +223,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.INSTANCE, info!!.uid!!) if (localCalendar == null) { throw FileNotFoundException("Failed to load local resource.") } @@ -264,11 +264,11 @@ class ImportFragment : DialogFragment() { finishParsingFile(contacts.size) val provider = context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI) - val localAddressBook = LocalAddressBook.findByUid(context!!, provider!!, account, info!!.uid) + val localAddressBook = LocalAddressBook.findByUid(context!!, provider!!, account, info!!.uid!!) for (contact in contacts) { try { - val localContact = LocalContact(localAddressBook, contact, null, null) + val localContact = LocalContact(localAddressBook!!, contact, null, null) localContact.createAsDirty() result.added++ } catch (e: ContactsStorageException) { 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 2aa9c0dd..8484bfb9 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 @@ -199,7 +199,7 @@ class LocalCalendarImportFragment : ListFragment() { try { val localCalendar = LocalCalendar.findByName(account, context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI), - LocalCalendar.Factory.INSTANCE, info!!.uid) + LocalCalendar.Factory.INSTANCE, info!!.uid!!) val localEvents = fromCalendar.all val total = localEvents.size progressDialog!!.max = total 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 805828f0..0d829d84 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 @@ -83,7 +83,7 @@ class LocalContactImportFragment : Fragment() { if (account == null || !(account.name == accountName && account.type == accountType)) { if (accountName != null && accountType != null) { account = Account(accountName, accountType) - localAddressBooks.add(LocalAddressBook(context, account, provider)) + localAddressBooks.add(LocalAddressBook(context!!, account, provider)) } } } @@ -132,7 +132,7 @@ class LocalContactImportFragment : Fragment() { try { val addressBook = LocalAddressBook.findByUid(context!!, context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!, - account, info!!.uid) + account, info!!.uid!!) val localContacts = localAddressBook.all val total = localContacts.size progressDialog!!.max = total @@ -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) {