Kotlin: move resources to Kotlin (almost compiles).

It just needs a few tiny wrappers around some public statics. I'm not
doing that because it'll sort itself out the moment we update
vcard4android and ical4android.
tmp
Tom Hacohen 5 years ago
parent 6af90b38ae
commit 2372bdcd8b

@ -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<LocalAddressBook> 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<Account> future = accountManager.renameAccount(account, newAccountName, new AccountManagerCallback<Account>() {
@Override
public void run(AccountManagerFuture<Account> 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<LocalResource> 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<LocalResource> 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<LocalResource> 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<Long> 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);
}
}
}

@ -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<LocalResource> {
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<LocalResource>
@Throws(ContactsStorageException::class)
get() {
val deleted = LinkedList<LocalResource>()
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<LocalResource>
@Throws(ContactsStorageException::class)
get() {
val dirty = LinkedList<LocalResource>()
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<LocalResource>
@Throws(ContactsStorageException::class)
get() {
val nameless = LinkedList<LocalResource>()
Collections.addAll(nameless, *queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null) as Array<LocalContact>)
if (includeGroups)
Collections.addAll(nameless, *queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null) as Array<LocalGroup>)
return nameless.toTypedArray()
}
val deletedContacts: Array<LocalContact>
@Throws(ContactsStorageException::class)
get() = queryContacts(RawContacts.DELETED + "!= 0", null) as Array<LocalContact>
val dirtyContacts: Array<LocalContact>
@Throws(ContactsStorageException::class)
get() = queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null) as Array<LocalContact>
val all: Array<LocalContact>
@Throws(ContactsStorageException::class)
get() = queryContacts(RawContacts.DELETED + "== 0", null) as Array<LocalContact>
val deletedGroups: Array<LocalGroup>
@Throws(ContactsStorageException::class)
get() = queryGroups(Groups.DELETED + "!= 0", null) as Array<LocalGroup>
val dirtyGroups: Array<LocalGroup>
@Throws(ContactsStorageException::class)
get() = queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null) as Array<LocalGroup>
var mainAccount: Account
@Throws(ContactsStorageException::class)
get() {
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<LocalContact>
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<LocalContact>
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<LocalContact> {
try {
val cursor = provider.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()), null)
val ids = HashSet<Long>()
while (cursor != null && cursor.moveToNext())
ids.add(cursor.getLong(0))
cursor!!.close()
val contacts = arrayOfNulls<LocalContact>(ids.size)
var i = 0
for (id in ids)
contacts[i++] = LocalContact(this, id, null, null)
return contacts
} 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<LocalGroup>)
if (group.members.size == 0) {
App.log.log(Level.FINE, "Deleting group", group)
group.delete()
}
}
@Throws(ContactsStorageException::class)
fun removeGroups() {
try {
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't remove all groups", e)
}
}
/** 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<LocalAddressBook> {
val accountManager = AccountManager.get(context)
val result = LinkedList<LocalAddressBook>()
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, account, provider)
if (mainAccount == null || addressBook.mainAccount == mainAccount)
result.add(addressBook)
}
return result.toTypedArray()
}
@Throws(ContactsStorageException::class)
fun findByUid(context: Context, provider: ContentProviderClient, mainAccount: Account?, uid: String): LocalAddressBook? {
val accountManager = AccountManager.get(context)
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, account, provider)
if (addressBook.url == uid && (mainAccount == null || addressBook.mainAccount == mainAccount))
return addressBook
}
return null
}
@Throws(ContactsStorageException::class)
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, journalEntity: JournalEntity): LocalAddressBook {
val info = journalEntity.info
val accountManager = AccountManager.get(context)
val account = Account(accountName(mainAccount, info), App.addressBookAccountType)
if (!accountManager.addAccountExplicitly(account, null, null))
throw ContactsStorageException("Couldn't create address book account")
setUserData(accountManager, account, mainAccount, info.uid!!)
val addressBook = LocalAddressBook(context, account, provider)
addressBook.mainAccount = mainAccount
addressBook.url = info.uid
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
return addressBook
}
// SETTINGS
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
fun setUserData(accountManager: AccountManager, account: Account, mainAccount: Account, url: String) {
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
accountManager.setUserData(account, USER_DATA_URL, url)
}
// HELPERS
fun accountName(mainAccount: Account, info: CollectionInfo): String {
val displayName = if (info.displayName != null) info.displayName else info.uid
val sb = StringBuilder(displayName)
sb.append(" (")
.append(mainAccount.name)
.append(" ")
.append(info.uid!!.substring(0, 4))
.append(")")
return sb.toString()
}
}
}

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

@ -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<LocalEvent> {
override val deleted: Array<LocalEvent>
@Throws(CalendarStorageException::class)
get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
override val withoutFileName: Array<LocalEvent>
@Throws(CalendarStorageException::class)
get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
val all: Array<LocalEvent>
@Throws(CalendarStorageException::class)
get() = queryEvents(null, null) as Array<LocalEvent>
override// get dirty events which are required to have an increased SEQUENCE value
// sequence has not been assigned yet (i.e. this event was just locally created)
val dirty: Array<LocalEvent>
@Throws(CalendarStorageException::class, FileNotFoundException::class)
get() {
val dirty = LinkedList<LocalEvent>()
for (event in queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>) {
if (event.event.sequence == null)
event.event.sequence = 0
else if (event.weAreOrganizer)
event.event.sequence++
dirty.add(event)
}
return dirty.toTypedArray()
}
override fun eventBaseInfoColumns(): Array<String> {
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<LocalEvent>
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<AndroidCalendar?> {
return arrayOfNulls<LocalCalendar>(size) as Array<AndroidCalendar?>
}
companion object {
val INSTANCE = Factory()
}
}
/** Fix all of the etags of all of the non-dirty events to be non-null.
* Currently set to all ones.. */
@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
}
}
}

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

@ -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<T> {
val deleted: Array<T>
val withoutFileName: Array<T>
/** Dirty *non-deleted* entries */
val dirty: Array<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getByUid(uid: String): T?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun count(): Long
}

@ -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<Long>
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<Long> 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<Long> 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];
}
}
}

@ -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<Long> = HashSet()
internal val groupMemberships: MutableSet<Long> = 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<Long> {
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<Long> {
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<LocalContact?> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
}
companion object {
init {
Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION
}
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
}
}

@ -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];
}
}
}

@ -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<AndroidEvent?> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
}
companion object {
init {
Event.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x")
}
internal val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
internal val COLUMN_UID = if (Build.VERSION.SDK_INT >= 17) Events.UID_2445 else Events.SYNC_DATA2
internal val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
}
}

@ -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<Long> 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<String> 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<Long> 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];
}
}
}

@ -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<Long>()
try {
val cursor = addressBook.provider.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(Data.RAW_CONTACT_ID),
GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), null
)
while (cursor != null && cursor.moveToNext())
members.add(cursor.getLong(0))
cursor!!.close()
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't list group members", e)
}
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<LocalGroup> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
}
companion object {
/** marshalled list of member UIDs, as sent by server */
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
/**
* Processes all groups with non-null [.COLUMN_PENDING_MEMBERS]: the pending memberships
* are (if possible) applied, keeping cached memberships in sync.
* @param addressBook address book to take groups from
* @throws ContactsStorageException on contact provider errors
*/
@Throws(ContactsStorageException::class)
fun applyPendingMemberships(addressBook: LocalAddressBook) {
try {
val cursor = addressBook.provider.query(
addressBook.syncAdapterURI(Groups.CONTENT_URI),
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
"$COLUMN_PENDING_MEMBERS IS NOT NULL", arrayOf(), null
)
val batch = BatchOperation(addressBook.provider)
while (cursor != null && cursor.moveToNext()) {
val id = cursor.getLong(0)
App.log.fine("Assigning members to group $id")
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val changeContactIDs = HashSet<Long>()
// delete all memberships and cached memberships for this group
for (contact in addressBook.getByGroupMembership(id)) {
contact.removeGroupMemberships(batch)
changeContactIDs.add(contact.id)
}
// extract list of member UIDs
val members = LinkedList<String>()
val raw = cursor.getBlob(1)
val parcel = Parcel.obtain()
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
parcel.readStringList(members)
parcel.recycle()
// insert memberships
for (uid in members) {
App.log.fine("Assigning member: $uid")
try {
val member = addressBook.findContactByUID(uid)
member.addToGroup(batch, id)
changeContactIDs.add(member.id)
} catch (e: FileNotFoundException) {
App.log.log(Level.WARNING, "Group member not found: $uid", e)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
for (contactID in changeContactIDs) {
val contact = LocalContact(addressBook, contactID, null, null)
contact.updateHashCode(batch)
}
// remove pending memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
))
batch.commit()
}
cursor!!.close()
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't get pending memberships", e)
}
}
}
}

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

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

@ -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];
}
}
}

@ -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<LocalTask?> {
return arrayOfNulls(size)
}
companion object {
val INSTANCE = Factory()
}
}
companion object {
init {
Task.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x")
}
internal val COLUMN_ETAG = Tasks.SYNC1
internal val COLUMN_UID = Tasks.SYNC2
internal val COLUMN_SEQUENCE = Tasks.SYNC3
}
}

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

@ -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<LocalTask> {
override val deleted: Array<LocalTask>
@Throws(CalendarStorageException::class)
get() = queryTasks(Tasks._DELETED + "!=0", null) as Array<LocalTask>
override val withoutFileName: Array<LocalTask>
@Throws(CalendarStorageException::class)
get() = queryTasks(Tasks._SYNC_ID + " IS NULL", null) as Array<LocalTask>
override// sequence has not been assigned yet (i.e. this task was just locally created)
val dirty: Array<LocalTask>
@Throws(CalendarStorageException::class, FileNotFoundException::class)
get() {
val tasks = queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null) as Array<LocalTask>
for (task in tasks) {
if (task.task.sequence == null)
task.task.sequence = 0
else
task.task.sequence++
}
return tasks
}
override fun taskBaseInfoColumns(): Array<String> {
return BASE_INFO_COLUMNS
}
@Throws(CalendarStorageException::class)
fun update(info: CollectionInfo, updateColor: Boolean) {
update(valuesFromCollectionInfo(info, updateColor))
}
@Throws(CalendarStorageException::class)
override fun getByUid(uid: String): LocalTask? {
val ret = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)) as Array<LocalTask>
return if (ret != null && ret.size > 0) {
ret[0]
} else null
}
@Throws(CalendarStorageException::class)
override fun count(): Long {
val where = Tasks.LIST_ID + "=?"
val whereArgs = arrayOf(id.toString())
try {
val cursor = provider.client.query(
syncAdapterURI(provider.tasksUri()), null,
where, whereArgs, null)
try {
return cursor.count.toLong()
} finally {
cursor.close()
}
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't query calendar events", e)
}
}
class Factory : AndroidTaskListFactory {
override fun newInstance(account: Account, provider: TaskProvider, id: Long): AndroidTaskList {
return LocalTaskList(account, provider, id)
}
override fun newArray(size: Int): Array<AndroidTaskList?> {
return arrayOfNulls<LocalTaskList>(size) as Array<AndroidTaskList?>
}
companion object {
val INSTANCE = Factory()
}
}
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()
}
}
}
}

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

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

@ -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<LocalResource>? = 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)

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

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

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

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

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

Loading…
Cancel
Save