mirror of
https://github.com/etesync/android
synced 2024-11-28 19:08:12 +00:00
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.
This commit is contained in:
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
202
app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt
Normal file
202
app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt
Normal file
@ -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];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
291
app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt
Normal file
291
app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt
Normal file
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
152
app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt
Normal file
152
app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt
Normal file
@ -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.Event
|
||||||
import at.bitfire.ical4android.InvalidCalendarException
|
import at.bitfire.ical4android.InvalidCalendarException
|
||||||
import at.bitfire.vcard4android.ContactsStorageException
|
import at.bitfire.vcard4android.ContactsStorageException
|
||||||
|
import com.etesync.syncadapter.resource.LocalCollection
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,7 +69,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
|||||||
account.name)
|
account.name)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
localCollection = calendar
|
localCollection = calendar as LocalCollection<LocalResource>
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun notificationId(): Int {
|
override fun notificationId(): Int {
|
||||||
@ -142,7 +143,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
|||||||
if (event.attendees.isEmpty()) {
|
if (event.attendees.isEmpty()) {
|
||||||
return
|
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.
|
* Synchronization manager for CardDAV collections; handles contacts and groups.
|
||||||
*/
|
*/
|
||||||
class ContactsSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class, ContactsStorageException::class)
|
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
|
protected override val syncErrorTitle: String
|
||||||
get() = context.getString(R.string.sync_error_contacts, account.name)
|
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)
|
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||||
localAddressBook.updateSettings(values)
|
localAddressBook.updateSettings(values)
|
||||||
|
|
||||||
journal = JournalEntryManager(httpClient, remote, localAddressBook.url)
|
journal = JournalEntryManager(httpClient, remote, localAddressBook.url!!)
|
||||||
|
|
||||||
localAddressBook.includeGroups = true
|
localAddressBook.includeGroups = true
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
|||||||
|
|
||||||
protected val notificationManager: NotificationHelper
|
protected val notificationManager: NotificationHelper
|
||||||
protected val info: CollectionInfo
|
protected val info: CollectionInfo
|
||||||
protected var localCollection: LocalCollection? = null
|
protected var localCollection: LocalCollection<LocalResource>? = null
|
||||||
|
|
||||||
protected var httpClient: OkHttpClient
|
protected var httpClient: OkHttpClient
|
||||||
|
|
||||||
@ -409,7 +409,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
App.log.info("Added/changed resource with UUID: " + local.uuid)
|
App.log.info("Added/changed resource with UUID: " + local.uuid)
|
||||||
local.clearDirty(local.uuid)
|
local.clearDirty(local.uuid!!)
|
||||||
}
|
}
|
||||||
if (left > 0) {
|
if (left > 0) {
|
||||||
localDirty = Arrays.copyOfRange(localDirty, left, localDirty.size)
|
localDirty = Arrays.copyOfRange(localDirty, left, localDirty.size)
|
||||||
|
@ -171,7 +171,7 @@ class ViewCollectionActivity : BaseActivity(), Refreshable {
|
|||||||
if (info.type == CollectionInfo.Type.CALENDAR) {
|
if (info.type == CollectionInfo.Type.CALENDAR) {
|
||||||
try {
|
try {
|
||||||
val providerClient = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
|
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()
|
providerClient!!.release()
|
||||||
if (resource == null) {
|
if (resource == null) {
|
||||||
return null
|
return null
|
||||||
@ -188,7 +188,7 @@ class ViewCollectionActivity : BaseActivity(), Refreshable {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
val providerClient = contentResolver.acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI)
|
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()
|
providerClient.release()
|
||||||
if (resource == null) {
|
if (resource == null) {
|
||||||
return null
|
return null
|
||||||
|
@ -77,7 +77,7 @@ class CalendarAccount protected constructor(val account: Account) {
|
|||||||
try {
|
try {
|
||||||
val localCalendar = LocalCalendar.findByName(calendarAccount.account,
|
val localCalendar = LocalCalendar.findByName(calendarAccount.account,
|
||||||
contentProviderClient,
|
contentProviderClient,
|
||||||
LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME))
|
LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME)!!)
|
||||||
if (localCalendar != null) calendarAccount.calendars.add(localCalendar)
|
if (localCalendar != null) calendarAccount.calendars.add(localCalendar)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
ex.printStackTrace()
|
ex.printStackTrace()
|
||||||
|
@ -223,7 +223,7 @@ class ImportFragment : DialogFragment() {
|
|||||||
val provider = context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
|
val provider = context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
|
||||||
val localCalendar: LocalCalendar?
|
val localCalendar: LocalCalendar?
|
||||||
try {
|
try {
|
||||||
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info!!.uid)
|
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info!!.uid!!)
|
||||||
if (localCalendar == null) {
|
if (localCalendar == null) {
|
||||||
throw FileNotFoundException("Failed to load local resource.")
|
throw FileNotFoundException("Failed to load local resource.")
|
||||||
}
|
}
|
||||||
@ -264,11 +264,11 @@ class ImportFragment : DialogFragment() {
|
|||||||
finishParsingFile(contacts.size)
|
finishParsingFile(contacts.size)
|
||||||
|
|
||||||
val provider = context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)
|
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) {
|
for (contact in contacts) {
|
||||||
try {
|
try {
|
||||||
val localContact = LocalContact(localAddressBook, contact, null, null)
|
val localContact = LocalContact(localAddressBook!!, contact, null, null)
|
||||||
localContact.createAsDirty()
|
localContact.createAsDirty()
|
||||||
result.added++
|
result.added++
|
||||||
} catch (e: ContactsStorageException) {
|
} catch (e: ContactsStorageException) {
|
||||||
|
@ -199,7 +199,7 @@ class LocalCalendarImportFragment : ListFragment() {
|
|||||||
try {
|
try {
|
||||||
val localCalendar = LocalCalendar.findByName(account,
|
val localCalendar = LocalCalendar.findByName(account,
|
||||||
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI),
|
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI),
|
||||||
LocalCalendar.Factory.INSTANCE, info!!.uid)
|
LocalCalendar.Factory.INSTANCE, info!!.uid!!)
|
||||||
val localEvents = fromCalendar.all
|
val localEvents = fromCalendar.all
|
||||||
val total = localEvents.size
|
val total = localEvents.size
|
||||||
progressDialog!!.max = total
|
progressDialog!!.max = total
|
||||||
|
@ -83,7 +83,7 @@ class LocalContactImportFragment : Fragment() {
|
|||||||
if (account == null || !(account.name == accountName && account.type == accountType)) {
|
if (account == null || !(account.name == accountName && account.type == accountType)) {
|
||||||
if (accountName != null && accountType != null) {
|
if (accountName != null && accountType != null) {
|
||||||
account = Account(accountName, accountType)
|
account = Account(accountName, accountType)
|
||||||
localAddressBooks.add(LocalAddressBook(context, account, provider))
|
localAddressBooks.add(LocalAddressBook(context!!, account, provider))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,7 +132,7 @@ class LocalContactImportFragment : Fragment() {
|
|||||||
try {
|
try {
|
||||||
val addressBook = LocalAddressBook.findByUid(context!!,
|
val addressBook = LocalAddressBook.findByUid(context!!,
|
||||||
context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
|
context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
|
||||||
account, info!!.uid)
|
account, info!!.uid!!)
|
||||||
val localContacts = localAddressBook.all
|
val localContacts = localAddressBook.all
|
||||||
val total = localContacts.size
|
val total = localContacts.size
|
||||||
progressDialog!!.max = total
|
progressDialog!!.max = total
|
||||||
@ -142,7 +142,7 @@ class LocalContactImportFragment : Fragment() {
|
|||||||
val contact = currentLocalContact.contact
|
val contact = currentLocalContact.contact
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val localContact = LocalContact(addressBook, contact, null, null)
|
val localContact = LocalContact(addressBook!!, contact, null, null)
|
||||||
localContact.createAsDirty()
|
localContact.createAsDirty()
|
||||||
result.added++
|
result.added++
|
||||||
} catch (e: ContactsStorageException) {
|
} catch (e: ContactsStorageException) {
|
||||||
|
Loading…
Reference in New Issue
Block a user