mirror of
https://github.com/etesync/android
synced 2024-11-23 00:18:19 +00:00
Basic support for VCard4-style groups
* rewritten contact group support to support VCard3 CATEGORIES and VCard4-style KIND/MEMBER groups * new account setting: contact group method (VCard3/VCard4/Apple "VCard4-as-VCard3") * keep unknown properties when saving/generating VCards
This commit is contained in:
parent
91234a688f
commit
b30733c64b
@ -45,6 +45,7 @@ import at.bitfire.davdroid.resource.LocalTaskList;
|
|||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.ical4android.TaskProvider;
|
import at.bitfire.ical4android.TaskProvider;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
|
import at.bitfire.vcard4android.GroupMethod;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
@ -72,6 +73,14 @@ public class AccountSettings {
|
|||||||
"0" false */
|
"0" false */
|
||||||
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
|
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
|
||||||
|
|
||||||
|
/** Contact group method:
|
||||||
|
automatic VCard4 if server supports VCard 4, VCard3 otherwise (default value)
|
||||||
|
VCard3 adds a contact's groups to its CATEGORIES / interprets a contact's CATEGORIES as groups
|
||||||
|
VCard4 uses groups as defined in VCard 4 (KIND/MEMBER properties)
|
||||||
|
Apple uses Apple-proprietary X-ADDRESSBOOK-KIND/-MEMBER properties
|
||||||
|
*/
|
||||||
|
private final static String KEY_CONTACT_GROUP_METHOD = "contact_group_method";
|
||||||
|
|
||||||
public final static long SYNC_INTERVAL_MANUALLY = -1;
|
public final static long SYNC_INTERVAL_MANUALLY = -1;
|
||||||
|
|
||||||
final Context context;
|
final Context context;
|
||||||
@ -184,6 +193,7 @@ public class AccountSettings {
|
|||||||
|
|
||||||
// CalDAV settings
|
// CalDAV settings
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public Integer getTimeRangePastDays() {
|
public Integer getTimeRangePastDays() {
|
||||||
String strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS);
|
String strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS);
|
||||||
if (strDays != null) {
|
if (strDays != null) {
|
||||||
@ -193,7 +203,7 @@ public class AccountSettings {
|
|||||||
return DEFAULT_TIME_RANGE_PAST_DAYS;
|
return DEFAULT_TIME_RANGE_PAST_DAYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTimeRangePastDays(Integer days) {
|
public void setTimeRangePastDays(@Nullable Integer days) {
|
||||||
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, String.valueOf(days == null ? -1 : days));
|
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, String.valueOf(days == null ? -1 : days));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,6 +216,22 @@ public class AccountSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CardDAV settings
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public GroupMethod getGroupMethod() {
|
||||||
|
final String name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD);
|
||||||
|
return name != null ?
|
||||||
|
GroupMethod.valueOf(name) :
|
||||||
|
GroupMethod.AUTOMATIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupMethod(@NonNull GroupMethod method) {
|
||||||
|
final String name = GroupMethod.AUTOMATIC.equals(method) ? null : method.name();
|
||||||
|
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// update from previous account settings
|
// update from previous account settings
|
||||||
|
|
||||||
private void update(int fromVersion) {
|
private void update(int fromVersion) {
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2013 – 2016 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 at.bitfire.davdroid.model;
|
||||||
|
|
||||||
|
import android.provider.ContactsContract.RawContacts;
|
||||||
|
|
||||||
|
public class UnknownProperties {
|
||||||
|
|
||||||
|
public static final String CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties";
|
||||||
|
|
||||||
|
public static final String
|
||||||
|
MIMETYPE = RawContacts.Data.MIMETYPE,
|
||||||
|
RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID,
|
||||||
|
UNKNOWN_PROPERTIES = RawContacts.Data.DATA1;
|
||||||
|
|
||||||
|
}
|
@ -12,22 +12,22 @@ import android.content.ContentProviderClient;
|
|||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
|
||||||
import android.provider.ContactsContract.Data;
|
|
||||||
import android.provider.ContactsContract.Groups;
|
import android.provider.ContactsContract.Groups;
|
||||||
import android.provider.ContactsContract.RawContacts;
|
import android.provider.ContactsContract.RawContacts;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import at.bitfire.davdroid.App;
|
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidContact;
|
import at.bitfire.vcard4android.AndroidContact;
|
||||||
import at.bitfire.vcard4android.AndroidGroupFactory;
|
import at.bitfire.vcard4android.AndroidGroup;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
|
||||||
@ -40,122 +40,128 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
|
|
||||||
private final Bundle syncState = new Bundle();
|
private final Bundle syncState = new Bundle();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether contact groups (LocalGroup resources) are included in query results for
|
||||||
|
* {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
|
||||||
|
* {@link #getWithoutFileName()}.
|
||||||
|
*/
|
||||||
|
public boolean includeGroups = true;
|
||||||
|
|
||||||
|
|
||||||
public LocalAddressBook(Account account, ContentProviderClient provider) {
|
public LocalAddressBook(Account account, ContentProviderClient provider) {
|
||||||
super(account, provider, AndroidGroupFactory.INSTANCE, LocalContact.Factory.INSTANCE);
|
super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, excluding those which have been modified locally (and not uploaded yet).
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public LocalContact[] getAll() throws ContactsStorageException {
|
public LocalResource[] getAll() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(null, null);
|
List<LocalResource> all = new LinkedList<>();
|
||||||
|
Collections.addAll(all, (LocalResource[])queryContacts(null, null));
|
||||||
|
if (includeGroups)
|
||||||
|
Collections.addAll(all, (LocalResource[])queryGroups(null, null));
|
||||||
|
return all.toArray(new LocalResource[all.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of local contacts which have been deleted locally. (DELETED != 0).
|
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LocalContact[] getDeleted() throws ContactsStorageException {
|
public LocalResource[] getDeleted() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(RawContacts.DELETED + "!=0", null);
|
List<LocalResource> deleted = new LinkedList<>();
|
||||||
|
Collections.addAll(deleted, getDeletedContacts());
|
||||||
|
if (includeGroups)
|
||||||
|
Collections.addAll(deleted, getDeletedGroups());
|
||||||
|
return deleted.toArray(new LocalResource[deleted.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of local contacts which have been changed locally (DIRTY != 0).
|
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LocalContact[] getDirty() throws ContactsStorageException {
|
public LocalResource[] getDirty() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!=0", null);
|
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.
|
* Returns an array of local contacts which don't have a file name yet.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LocalContact[] getWithoutFileName() throws ContactsStorageException {
|
public LocalResource[] getWithoutFileName() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null);
|
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()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteAll() throws ContactsStorageException {
|
public void deleteAll() throws ContactsStorageException {
|
||||||
try {
|
try {
|
||||||
provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null);
|
provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null);
|
||||||
} catch (RemoteException e) {
|
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null);
|
||||||
throw new ContactsStorageException("Couldn't delete all local contacts", e);
|
} catch(RemoteException e) {
|
||||||
|
throw new ContactsStorageException("Couldn't delete all local contacts and groups", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GROUPS
|
public LocalContact[] getDeletedContacts() throws ContactsStorageException {
|
||||||
|
return (LocalContact[])queryContacts(RawContacts.DELETED + "!= 0", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalContact[] getDirtyContacts() throws ContactsStorageException {
|
||||||
|
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
|
||||||
|
return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
|
||||||
|
return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the first group with the given title.
|
* Finds the first group with the given title. If there is no group with this
|
||||||
* @param displayName title of the group to look for
|
* title, a new group is created.
|
||||||
* @return group with given title, or null if none
|
* @param title title of the group to look for
|
||||||
|
* @return id of the group with given title
|
||||||
|
* @throws ContactsStorageException on contact provider errors
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("Recycle")
|
public long findOrCreateGroup(@NonNull String title) throws ContactsStorageException {
|
||||||
public LocalGroup findGroupByTitle(String displayName) throws ContactsStorageException {
|
|
||||||
try {
|
try {
|
||||||
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
|
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
|
||||||
new String[] { Groups._ID },
|
new String[] { Groups._ID },
|
||||||
ContactsContract.Groups.TITLE + "=?", new String[] { displayName }, null);
|
Groups.TITLE + "=?", new String[] { title },
|
||||||
|
null);
|
||||||
if (cursor != null && cursor.moveToNext())
|
if (cursor != null && cursor.moveToNext())
|
||||||
return new LocalGroup(this, cursor.getLong(0));
|
return cursor.getLong(0);
|
||||||
} catch (RemoteException e) {
|
|
||||||
|
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);
|
throw new ContactsStorageException("Couldn't find local contact group", e);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("Recycle")
|
public void removeEmptyGroups() throws ContactsStorageException {
|
||||||
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
|
// find groups without members
|
||||||
List<LocalGroup> groups = new LinkedList<>();
|
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
|
||||||
try {
|
for (LocalGroup group : (LocalGroup[])queryGroups(null, null))
|
||||||
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
|
if (group.getMembers().length == 0)
|
||||||
new String[] { Groups._ID },
|
group.delete();
|
||||||
Groups.DELETED + "!=0", null, null);
|
|
||||||
while (cursor != null && cursor.moveToNext())
|
|
||||||
groups.add(new LocalGroup(this, cursor.getLong(0)));
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ContactsStorageException("Couldn't query deleted groups", e);
|
|
||||||
}
|
|
||||||
return groups.toArray(new LocalGroup[groups.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("Recycle")
|
|
||||||
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
|
|
||||||
List<LocalGroup> groups = new LinkedList<>();
|
|
||||||
try {
|
|
||||||
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
|
|
||||||
new String[] { Groups._ID },
|
|
||||||
Groups.DIRTY + "!=0", null, null);
|
|
||||||
while (cursor != null && cursor.moveToNext())
|
|
||||||
groups.add(new LocalGroup(this, cursor.getLong(0)));
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ContactsStorageException("Couldn't query dirty groups", e);
|
|
||||||
}
|
|
||||||
return groups.toArray(new LocalGroup[groups.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("Recycle")
|
|
||||||
public void markMembersDirty(long groupId) throws ContactsStorageException {
|
|
||||||
ContentValues dirty = new ContentValues(1);
|
|
||||||
dirty.put(RawContacts.DIRTY, 1);
|
|
||||||
try {
|
|
||||||
// query all GroupMemberships of this groupId, mark every corresponding raw contact as DIRTY
|
|
||||||
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Data.CONTENT_URI),
|
|
||||||
new String[] { GroupMembership.RAW_CONTACT_ID },
|
|
||||||
Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
|
|
||||||
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
long id = cursor.getLong(0);
|
|
||||||
App.log.fine("Marking raw contact #" + id + " as dirty");
|
|
||||||
provider.update(syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)), dirty, null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ContactsStorageException("Couldn't query dirty groups", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,16 +13,20 @@ import android.content.ContentValues;
|
|||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||||
|
import android.provider.ContactsContract.RawContacts.Data;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.util.logging.Level;
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import at.bitfire.davdroid.App;
|
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
import at.bitfire.davdroid.BuildConfig;
|
||||||
|
import at.bitfire.davdroid.model.UnknownProperties;
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidContact;
|
import at.bitfire.vcard4android.AndroidContact;
|
||||||
import at.bitfire.vcard4android.AndroidContactFactory;
|
import at.bitfire.vcard4android.AndroidContactFactory;
|
||||||
import at.bitfire.vcard4android.BatchOperation;
|
import at.bitfire.vcard4android.BatchOperation;
|
||||||
|
import at.bitfire.vcard4android.CachedGroupMembership;
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import ezvcard.Ezvcard;
|
import ezvcard.Ezvcard;
|
||||||
@ -32,6 +36,11 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " vcard4android ez-vcard/" + Ezvcard.VERSION;
|
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " vcard4android ez-vcard/" + Ezvcard.VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected final Set<Long>
|
||||||
|
cachedGroupMemberships = new HashSet<>(),
|
||||||
|
groupMemberships = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
||||||
super(addressBook, id, fileName, eTag);
|
super(addressBook, id, fileName, eTag);
|
||||||
}
|
}
|
||||||
@ -69,53 +78,94 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// group support
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void populateGroupMembership(ContentValues row) {
|
protected void populateData(String mimeType, ContentValues row) {
|
||||||
if (row.containsKey(GroupMembership.GROUP_ROW_ID)) {
|
switch (mimeType) {
|
||||||
long groupId = row.getAsLong(GroupMembership.GROUP_ROW_ID);
|
case CachedGroupMembership.CONTENT_ITEM_TYPE:
|
||||||
|
cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID));
|
||||||
// fetch group
|
break;
|
||||||
LocalGroup group = new LocalGroup(addressBook, groupId);
|
case GroupMembership.CONTENT_ITEM_TYPE:
|
||||||
try {
|
groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID));
|
||||||
Contact groupInfo = group.getContact();
|
break;
|
||||||
|
case UnknownProperties.CONTENT_ITEM_TYPE:
|
||||||
// add to CATEGORIES
|
contact.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES);
|
||||||
contact.categories.add(groupInfo.displayName);
|
break;
|
||||||
} catch (FileNotFoundException|ContactsStorageException e) {
|
|
||||||
App.log.log(Level.WARNING, "Couldn't find assigned group #" + groupId + ", ignoring membership", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void insertGroupMemberships(BatchOperation batch) throws ContactsStorageException {
|
protected void insertDataRows(BatchOperation batch) throws ContactsStorageException {
|
||||||
for (String category : contact.categories) {
|
super.insertDataRows(batch);
|
||||||
// Is there already a category with this display name?
|
|
||||||
LocalGroup group = ((LocalAddressBook)addressBook).findGroupByTitle(category);
|
|
||||||
|
|
||||||
if (group == null) {
|
if (contact.unknownProperties != null) {
|
||||||
// no, we have to create the group before inserting the membership
|
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
|
||||||
|
if (id == null)
|
||||||
Contact groupInfo = new Contact();
|
builder.withValueBackReference(UnknownProperties.RAW_CONTACT_ID, 0);
|
||||||
groupInfo.displayName = category;
|
else
|
||||||
group = new LocalGroup(addressBook, groupInfo);
|
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id);
|
||||||
group.create();
|
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
|
||||||
}
|
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties);
|
||||||
|
batch.enqueue(builder.build());
|
||||||
Long groupId = group.getId();
|
|
||||||
if (groupId != null) {
|
|
||||||
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
|
|
||||||
if (id == null)
|
|
||||||
builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
|
|
||||||
else
|
|
||||||
builder.withValue(GroupMembership.RAW_CONTACT_ID, id);
|
|
||||||
builder .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
|
||||||
.withValue(GroupMembership.GROUP_ROW_ID, groupId);
|
|
||||||
batch.enqueue(builder.build());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addToGroup(BatchOperation batch, long groupID) {
|
||||||
|
assertID();
|
||||||
|
batch.enqueue(ContentProviderOperation
|
||||||
|
.newInsert(dataSyncURI())
|
||||||
|
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||||
|
.withValue(GroupMembership.RAW_CONTACT_ID, id)
|
||||||
|
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.enqueue(ContentProviderOperation
|
||||||
|
.newInsert(dataSyncURI())
|
||||||
|
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||||
|
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
|
||||||
|
.withValue(CachedGroupMembership.GROUP_ID, groupID)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeGroupMemberships(BatchOperation batch) {
|
||||||
|
assertID();
|
||||||
|
batch.enqueue(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 }
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -134,6 +184,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
return new LocalContact(addressBook, contact, fileName, eTag);
|
return new LocalContact(addressBook, contact, fileName, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public LocalContact[] newArray(int size) {
|
public LocalContact[] newArray(int size) {
|
||||||
return new LocalContact[size];
|
return new LocalContact[size];
|
||||||
}
|
}
|
||||||
|
@ -8,28 +8,241 @@
|
|||||||
|
|
||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
import android.content.ContentProviderOperation;
|
||||||
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
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 org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import at.bitfire.dav4android.Constants;
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidGroup;
|
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.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
|
import lombok.Cleanup;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
public class LocalGroup extends AndroidGroup {
|
@ToString(callSuper=true)
|
||||||
|
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 LocalGroup(AndroidAddressBook addressBook, long id) {
|
public LocalGroup(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
||||||
super(addressBook, id);
|
super(addressBook, id, fileName, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalGroup(AndroidAddressBook addressBook, Contact contact) {
|
public LocalGroup(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
||||||
super(addressBook, contact);
|
super(addressBook, contact, fileName, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearDirty() throws ContactsStorageException {
|
|
||||||
ContentValues values = new ContentValues(1);
|
@Override
|
||||||
values.put(ContactsContract.Groups.DIRTY, 0);
|
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(values);
|
||||||
|
|
||||||
|
// update cached group memberships
|
||||||
|
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||||
|
|
||||||
|
// delete cached group memberships
|
||||||
|
batch.enqueue(ContentProviderOperation
|
||||||
|
.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||||
|
.withSelection(
|
||||||
|
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
|
||||||
|
new String[] { CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) }
|
||||||
|
).build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// insert updated cached group memberships
|
||||||
|
for (long member : getMembers())
|
||||||
|
batch.enqueue(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)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
|
||||||
|
String newFileName = uid + ".vcf";
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
@Cleanup("recycle") Parcel members = Parcel.obtain();
|
||||||
|
members.writeStringList(contact.members);
|
||||||
|
values.put(COLUMN_PENDING_MEMBERS, members.marshall());
|
||||||
|
|
||||||
|
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(ContentProviderOperation
|
||||||
|
.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
|
||||||
|
.withValue(RawContacts.DIRTY, 1)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@Cleanup 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);
|
||||||
|
Constants.log.fine("Assigning members to group " + id);
|
||||||
|
|
||||||
|
// delete all memberships and cached memberships for this group
|
||||||
|
batch.enqueue(ContentProviderOperation
|
||||||
|
.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||||
|
.withSelection(
|
||||||
|
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" +
|
||||||
|
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
|
||||||
|
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id), CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) })
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// extract list of member UIDs
|
||||||
|
List<String> members = new LinkedList<>();
|
||||||
|
byte[] raw = cursor.getBlob(1);
|
||||||
|
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
|
||||||
|
parcel.unmarshall(raw, 0, raw.length);
|
||||||
|
parcel.setDataPosition(0);
|
||||||
|
parcel.readStringList(members);
|
||||||
|
|
||||||
|
// insert memberships
|
||||||
|
for (String uid : members) {
|
||||||
|
Constants.log.fine("Assigning member: " + uid);
|
||||||
|
try {
|
||||||
|
LocalContact member = addressBook.findContactByUID(uid);
|
||||||
|
member.addToGroup(batch, id);
|
||||||
|
} catch(FileNotFoundException e) {
|
||||||
|
Constants.log.log(Level.WARNING, "Group member not found: " + uid, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove pending memberships
|
||||||
|
batch.enqueue(ContentProviderOperation
|
||||||
|
.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||||
|
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.commit();
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
@Cleanup 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));
|
||||||
|
} catch(RemoteException e) {
|
||||||
|
throw new ContactsStorageException("Couldn't list group members", e);
|
||||||
|
}
|
||||||
|
return ArrayUtils.toPrimitive(members.toArray(new Long[members.size()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ import okhttp3.MediaType;
|
|||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
|
||||||
|
*/
|
||||||
public class CalendarSyncManager extends SyncManager {
|
public class CalendarSyncManager extends SyncManager {
|
||||||
|
|
||||||
protected static final int MAX_MULTIGET = 20;
|
protected static final int MAX_MULTIGET = 20;
|
||||||
|
@ -10,26 +10,33 @@ package at.bitfire.davdroid.syncadapter;
|
|||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentProviderOperation;
|
||||||
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
|
import android.provider.ContactsContract.Groups;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.apache.commons.codec.Charsets;
|
||||||
|
import org.apache.commons.collections4.SetUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavAddressBook;
|
import at.bitfire.dav4android.DavAddressBook;
|
||||||
@ -54,8 +61,10 @@ import at.bitfire.davdroid.resource.LocalContact;
|
|||||||
import at.bitfire.davdroid.resource.LocalGroup;
|
import at.bitfire.davdroid.resource.LocalGroup;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
|
import at.bitfire.vcard4android.BatchOperation;
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
|
import at.bitfire.vcard4android.GroupMethod;
|
||||||
import ezvcard.VCardVersion;
|
import ezvcard.VCardVersion;
|
||||||
import ezvcard.util.IOUtils;
|
import ezvcard.util.IOUtils;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
@ -68,12 +77,49 @@ import okhttp3.RequestBody;
|
|||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
|
||||||
|
*
|
||||||
|
* <p></p>Group handling differs according to the {@link #groupMethod}. There are two basic methods to
|
||||||
|
* handle/manage groups:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>VCard3 {@code CATEGORIES}: groups memberships are attached to each contact and represented as
|
||||||
|
* "category". When a group is dirty or has been deleted, all its members have to be set to
|
||||||
|
* dirty, too (because they have to be uploaded without the respective category). This
|
||||||
|
* is done in {@link #prepareDirty()}. Empty groups can be deleted without further processing,
|
||||||
|
* which is done in {@link #postProcess()} because groups may become empty after downloading
|
||||||
|
* updated remoted contacts.</li>
|
||||||
|
* <li>VCard4-style: individual and group contacts (with a list of member UIDs) are
|
||||||
|
* distinguished. When a local group is dirty, its members don't need to be set to dirty.
|
||||||
|
* <ol>
|
||||||
|
* <li>However, when a contact is dirty, it has
|
||||||
|
* to be checked whether its group memberships have changed. In this case, the respective
|
||||||
|
* groups have to be set to dirty. For instance, if contact A is in group G and H, and then
|
||||||
|
* group membership of G is removed, the contact will be set to dirty because of the changed
|
||||||
|
* {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}. DAVdroid will
|
||||||
|
* then have to check whether the group memberships have actually changed, and if so,
|
||||||
|
* all affected groups have to be set to dirty. To detect changes in group memberships,
|
||||||
|
* DAVdroid always mirrors all {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}
|
||||||
|
* data rows in respective {@link at.bitfire.vcard4android.CachedGroupMembership} rows.
|
||||||
|
* If the cached group memberships are not the same as the current group member ships, the
|
||||||
|
* difference set (in our example G, because its in the cached memberships, but not in the
|
||||||
|
* actual ones) is marked as dirty. This is done in {@link #prepareDirty()}.</li>
|
||||||
|
* <li>When downloading remote contacts, groups (+ member information) may be received
|
||||||
|
* by the actual members. Thus, the member lists have to be cached until all VCards
|
||||||
|
* are received. This is done by caching the member UIDs of each group in
|
||||||
|
* {@link LocalGroup#COLUMN_PENDING_MEMBERS}. In {@link #postProcess()},
|
||||||
|
* these "pending memberships" are assigned to the actual contacs and then cleaned up.</li>
|
||||||
|
* </ol>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
public class ContactsSyncManager extends SyncManager {
|
public class ContactsSyncManager extends SyncManager {
|
||||||
protected static final int MAX_MULTIGET = 10;
|
protected static final int MAX_MULTIGET = 10;
|
||||||
|
|
||||||
final private ContentProviderClient provider;
|
final private ContentProviderClient provider;
|
||||||
final private CollectionInfo remote;
|
final private CollectionInfo remote;
|
||||||
|
|
||||||
private boolean hasVCard4;
|
private boolean hasVCard4;
|
||||||
|
private GroupMethod groupMethod;
|
||||||
|
|
||||||
|
|
||||||
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, CollectionInfo remote) throws InvalidAccountException {
|
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, CollectionInfo remote) throws InvalidAccountException {
|
||||||
@ -107,37 +153,113 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set up Contacts Provider Settings
|
// set up Contacts Provider Settings
|
||||||
ContentValues settings = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
settings.put(ContactsContract.Settings.SHOULD_SYNC, 1);
|
values.put(ContactsContract.Settings.SHOULD_SYNC, 1);
|
||||||
settings.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
||||||
localAddressBook.updateSettings(settings);
|
localAddressBook.updateSettings(values);
|
||||||
|
|
||||||
collectionURL = HttpUrl.parse(url);
|
collectionURL = HttpUrl.parse(url);
|
||||||
davCollection = new DavAddressBook(httpClient, collectionURL);
|
davCollection = new DavAddressBook(httpClient, collectionURL);
|
||||||
|
|
||||||
processChangedGroups();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
||||||
// prepare remote address book
|
// prepare remote address book
|
||||||
hasVCard4 = false;
|
|
||||||
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
||||||
SupportedAddressData supportedAddressData = (SupportedAddressData) davCollection.properties.get(SupportedAddressData.NAME);
|
SupportedAddressData supportedAddressData = (SupportedAddressData)davCollection.properties.get(SupportedAddressData.NAME);
|
||||||
if (supportedAddressData != null)
|
hasVCard4 = supportedAddressData != null && supportedAddressData.hasVCard4();
|
||||||
for (MediaType type : supportedAddressData.types)
|
|
||||||
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
|
|
||||||
hasVCard4 = true;
|
|
||||||
App.log.info("Server advertises VCard/4 support: " + hasVCard4);
|
App.log.info("Server advertises VCard/4 support: " + hasVCard4);
|
||||||
|
|
||||||
|
groupMethod = settings.getGroupMethod();
|
||||||
|
if (GroupMethod.AUTOMATIC.equals(groupMethod))
|
||||||
|
groupMethod = hasVCard4 ? GroupMethod.VCARD4 : GroupMethod.VCARD3_CATEGORIES;
|
||||||
|
App.log.info("Contact group method: " + groupMethod);
|
||||||
|
|
||||||
|
localAddressBook().includeGroups = !GroupMethod.VCARD3_CATEGORIES.equals(groupMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected RequestBody prepareUpload(LocalResource resource) throws IOException, ContactsStorageException {
|
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
||||||
LocalContact local = (LocalContact)resource;
|
super.prepareDirty();
|
||||||
App.log.log(Level.FINE, "Preparing upload of contact " + local.getFileName(), local.getContact());
|
|
||||||
|
LocalAddressBook addressBook = localAddressBook();
|
||||||
|
|
||||||
|
if (GroupMethod.VCARD3_CATEGORIES.equals(groupMethod)) {
|
||||||
|
/* VCard3 group handling: groups memberships are represented as contact CATEGORIES */
|
||||||
|
|
||||||
|
// groups with DELETED=1: set all members to dirty, then remove group
|
||||||
|
for (LocalGroup group : addressBook.getDeletedGroups()) {
|
||||||
|
App.log.fine("Removing group " + group + " and marking its members as dirty");
|
||||||
|
group.markMembersDirty();
|
||||||
|
group.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
|
||||||
|
for (LocalGroup group : addressBook.getDirtyGroups()) {
|
||||||
|
App.log.fine("Marking members of modified group " + group + " as dirty");
|
||||||
|
group.markMembersDirty();
|
||||||
|
group.clearDirty(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* VCard4 group handling: there are group contacts and individual contacts */
|
||||||
|
|
||||||
|
// mark groups with changed members as dirty
|
||||||
|
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||||
|
for (LocalContact contact : addressBook.getDirtyContacts())
|
||||||
|
try {
|
||||||
|
App.log.fine("Looking for changed group memberships of contact " + contact.getFileName());
|
||||||
|
Set<Long> cachedGroups = contact.getCachedGroupMemberships(),
|
||||||
|
currentGroups = contact.getGroupMemberships();
|
||||||
|
for (Long groupID : SetUtils.disjunction(cachedGroups, currentGroups)) {
|
||||||
|
App.log.fine("Marking group as dirty: " + groupID);
|
||||||
|
batch.enqueue(ContentProviderOperation
|
||||||
|
.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
|
||||||
|
.withValue(Groups.DIRTY, 1)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch(FileNotFoundException ignored) {
|
||||||
|
}
|
||||||
|
batch.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RequestBody prepareUpload(@NonNull LocalResource resource) throws IOException, ContactsStorageException {
|
||||||
|
final Contact contact;
|
||||||
|
if (resource instanceof LocalContact) {
|
||||||
|
LocalContact local = ((LocalContact)resource);
|
||||||
|
contact = local.getContact();
|
||||||
|
|
||||||
|
if (groupMethod == GroupMethod.VCARD3_CATEGORIES) {
|
||||||
|
// VCard3: add groups as CATEGORIES
|
||||||
|
for (long groupID : local.getGroupMemberships()) {
|
||||||
|
try {
|
||||||
|
@Cleanup Cursor c = provider.query(
|
||||||
|
localAddressBook().syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
|
||||||
|
new String[] { Groups.TITLE },
|
||||||
|
null, null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if (c != null && c.moveToNext()) {
|
||||||
|
String title = c.getString(0);
|
||||||
|
if (!TextUtils.isEmpty(title))
|
||||||
|
contact.categories.add(title);
|
||||||
|
}
|
||||||
|
} catch(RemoteException e) {
|
||||||
|
throw new ContactsStorageException("Couldn't find group for adding CATEGORIES", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (resource instanceof LocalGroup)
|
||||||
|
contact = ((LocalGroup)resource).getContact();
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException("Argument must be LocalContact or LocalGroup");
|
||||||
|
|
||||||
|
App.log.log(Level.FINE, "Preparing upload of VCard " + resource.getFileName(), contact);
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
local.getContact().write(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0, os);
|
contact.write(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0, groupMethod, os);
|
||||||
|
|
||||||
return RequestBody.create(
|
return RequestBody.create(
|
||||||
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
||||||
@ -213,21 +335,21 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
// process multiget results
|
// process multiget results
|
||||||
for (DavResource remote : davCollection.members) {
|
for (DavResource remote : davCollection.members) {
|
||||||
String eTag;
|
String eTag;
|
||||||
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
|
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
||||||
if (getETag != null)
|
if (getETag != null)
|
||||||
eTag = getETag.eTag;
|
eTag = getETag.eTag;
|
||||||
else
|
else
|
||||||
throw new DavException("Received multi-get response without ETag");
|
throw new DavException("Received multi-get response without ETag");
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
Charset charset = Charsets.UTF_8;
|
||||||
GetContentType getContentType = (GetContentType) remote.properties.get(GetContentType.NAME);
|
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
||||||
if (getContentType != null && getContentType.type != null) {
|
if (getContentType != null && getContentType.type != null) {
|
||||||
MediaType type = MediaType.parse(getContentType.type);
|
MediaType type = MediaType.parse(getContentType.type);
|
||||||
if (type != null)
|
if (type != null)
|
||||||
charset = type.charset(Charsets.UTF_8);
|
charset = type.charset(Charsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddressData addressData = (AddressData) remote.properties.get(AddressData.NAME);
|
AddressData addressData = (AddressData)remote.properties.get(AddressData.NAME);
|
||||||
if (addressData == null || addressData.vCard == null)
|
if (addressData == null || addressData.vCard == null)
|
||||||
throw new DavException("Received multi-get response without address data");
|
throw new DavException("Received multi-get response without address data");
|
||||||
|
|
||||||
@ -238,6 +360,22 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
||||||
|
if (groupMethod == GroupMethod.VCARD3_CATEGORIES) {
|
||||||
|
/* VCard3 group handling: groups memberships are represented as contact CATEGORIES */
|
||||||
|
|
||||||
|
// remove empty groups
|
||||||
|
App.log.info("Removing empty groups");
|
||||||
|
localAddressBook().removeEmptyGroups();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
/* VCard4 group handling: there are group contacts and individual contacts */
|
||||||
|
App.log.info("Assigning memberships of downloaded contact groups");
|
||||||
|
LocalGroup.applyPendingMemberships(localAddressBook());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
|
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
|
||||||
super.saveSyncState();
|
super.saveSyncState();
|
||||||
@ -250,48 +388,78 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
||||||
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
|
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
|
||||||
|
|
||||||
private void processChangedGroups() throws ContactsStorageException {
|
|
||||||
LocalAddressBook addressBook = localAddressBook();
|
|
||||||
|
|
||||||
// groups with DELETED=1: remove group finally
|
|
||||||
for (LocalGroup group : addressBook.getDeletedGroups()) {
|
|
||||||
long groupId = group.getId();
|
|
||||||
App.log.fine("Finally removing group #" + groupId);
|
|
||||||
// remove group memberships, but not as sync adapter (should marks contacts as DIRTY)
|
|
||||||
// NOTE: doesn't work that way because Contact Provider removes the group memberships even for DELETED groups
|
|
||||||
// addressBook.removeGroupMemberships(groupId, false);
|
|
||||||
group.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
|
|
||||||
for (LocalGroup group : addressBook.getDirtyGroups()) {
|
|
||||||
long groupId = group.getId();
|
|
||||||
App.log.fine("Marking members of modified group #" + groupId + " as dirty");
|
|
||||||
addressBook.markMembersDirty(groupId);
|
|
||||||
group.clearDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
||||||
|
App.log.info("Processing CardDAV resource " + fileName);
|
||||||
Contact[] contacts = Contact.fromStream(stream, charset, downloader);
|
Contact[] contacts = Contact.fromStream(stream, charset, downloader);
|
||||||
if (contacts.length == 1) {
|
if (contacts.length == 0) {
|
||||||
Contact newData = contacts[0];
|
App.log.warning("Received VCard without data, ignoring");
|
||||||
|
return;
|
||||||
|
} else if (contacts.length > 1)
|
||||||
|
App.log.warning("Received multiple VCards, using first one");
|
||||||
|
|
||||||
// update local contact, if it exists
|
final Contact newData = contacts[0];
|
||||||
LocalContact localContact = (LocalContact)localResources.get(fileName);
|
|
||||||
if (localContact != null) {
|
// update local contact, if it exists
|
||||||
App.log.info("Updating " + fileName + " in local address book");
|
LocalResource local = localResources.get(fileName);
|
||||||
localContact.eTag = eTag;
|
if (local != null) {
|
||||||
localContact.update(newData);
|
App.log.log(Level.INFO, "Updating " + fileName + " in local address book", newData);
|
||||||
|
|
||||||
|
if (local instanceof LocalGroup && newData.group) {
|
||||||
|
// update group
|
||||||
|
LocalGroup group = (LocalGroup)local;
|
||||||
|
group.eTag = eTag;
|
||||||
|
group.updateFromServer(newData);
|
||||||
syncResult.stats.numUpdates++;
|
syncResult.stats.numUpdates++;
|
||||||
|
|
||||||
|
} else if (local instanceof LocalContact && !newData.group) {
|
||||||
|
// update contact
|
||||||
|
LocalContact contact = (LocalContact)local;
|
||||||
|
contact.eTag = eTag;
|
||||||
|
contact.update(newData);
|
||||||
|
syncResult.stats.numUpdates++;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
App.log.info("Adding " + fileName + " to local address book");
|
// group has become an individual contact or vice versa
|
||||||
localContact = new LocalContact(localAddressBook(), newData, fileName, eTag);
|
try {
|
||||||
localContact.add();
|
local.delete();
|
||||||
syncResult.stats.numInserts++;
|
local = null;
|
||||||
|
} catch(CalendarStorageException e) {
|
||||||
|
// CalendarStorageException is not used by LocalGroup and LocalContact
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
App.log.severe("Received VCard with not exactly one VCARD, ignoring " + fileName);
|
|
||||||
|
if (local == null) {
|
||||||
|
if (newData.group) {
|
||||||
|
App.log.log(Level.INFO, "Creating local group", newData);
|
||||||
|
LocalGroup group = new LocalGroup(localAddressBook(), newData, fileName, eTag);
|
||||||
|
group.create();
|
||||||
|
|
||||||
|
local = group;
|
||||||
|
} else {
|
||||||
|
App.log.log(Level.INFO, "Creating local contact", newData);
|
||||||
|
LocalContact contact = new LocalContact(localAddressBook(), newData, fileName, eTag);
|
||||||
|
contact.create();
|
||||||
|
|
||||||
|
local = contact;
|
||||||
|
}
|
||||||
|
syncResult.stats.numInserts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupMethod == GroupMethod.VCARD3_CATEGORIES && local instanceof LocalContact) {
|
||||||
|
// VCard3: update group memberships from CATEGORIES
|
||||||
|
LocalContact contact = (LocalContact)local;
|
||||||
|
|
||||||
|
BatchOperation batch = new BatchOperation(provider);
|
||||||
|
contact.removeGroupMemberships(batch);
|
||||||
|
|
||||||
|
for (String category : contact.getContact().categories) {
|
||||||
|
long groupID = localAddressBook().findOrCreateGroup(category);
|
||||||
|
contact.addToGroup(batch, groupID);
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +66,8 @@ abstract public class SyncManager {
|
|||||||
SYNC_PHASE_LIST_REMOTE = 7,
|
SYNC_PHASE_LIST_REMOTE = 7,
|
||||||
SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8,
|
SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8,
|
||||||
SYNC_PHASE_DOWNLOAD_REMOTE = 9,
|
SYNC_PHASE_DOWNLOAD_REMOTE = 9,
|
||||||
SYNC_PHASE_SAVE_SYNC_STATE = 10;
|
SYNC_PHASE_POST_PROCESSING = 10,
|
||||||
|
SYNC_PHASE_SAVE_SYNC_STATE = 11;
|
||||||
|
|
||||||
protected final NotificationManager notificationManager;
|
protected final NotificationManager notificationManager;
|
||||||
protected final String uniqueCollectionId;
|
protected final String uniqueCollectionId;
|
||||||
@ -169,6 +170,10 @@ abstract public class SyncManager {
|
|||||||
App.log.info("Downloading remote entries");
|
App.log.info("Downloading remote entries");
|
||||||
downloadRemote();
|
downloadRemote();
|
||||||
|
|
||||||
|
syncPhase = SYNC_PHASE_POST_PROCESSING;
|
||||||
|
App.log.info("Post-processing");
|
||||||
|
postProcess();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
|
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
|
||||||
App.log.info("Saving sync state");
|
App.log.info("Saving sync state");
|
||||||
saveSyncState();
|
saveSyncState();
|
||||||
@ -278,9 +283,10 @@ abstract public class SyncManager {
|
|||||||
|
|
||||||
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
||||||
// assign file names and UIDs to new contacts so that we can use the file name as an index
|
// assign file names and UIDs to new contacts so that we can use the file name as an index
|
||||||
|
App.log.info("Looking for contacts/groups without file name");
|
||||||
for (LocalResource local : localCollection.getWithoutFileName()) {
|
for (LocalResource local : localCollection.getWithoutFileName()) {
|
||||||
String uuid = UUID.randomUUID().toString();
|
String uuid = UUID.randomUUID().toString();
|
||||||
App.log.info("Found local record #" + local.getId() + " without file name; assigning file name/UID based on " + uuid);
|
App.log.fine("Found local record #" + local.getId() + " without file name; assigning file name/UID based on " + uuid);
|
||||||
local.updateFileNameAndUID(uuid);
|
local.updateFileNameAndUID(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,7 +311,6 @@ abstract public class SyncManager {
|
|||||||
RequestBody body = prepareUpload(local);
|
RequestBody body = prepareUpload(local);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (local.getETag() == null) {
|
if (local.getETag() == null) {
|
||||||
App.log.info("Uploading new record " + fileName);
|
App.log.info("Uploading new record " + fileName);
|
||||||
remote.put(body, null, true);
|
remote.put(body, null, true);
|
||||||
@ -313,7 +318,6 @@ abstract public class SyncManager {
|
|||||||
App.log.info("Uploading locally modified record " + fileName);
|
App.log.info("Uploading locally modified record " + fileName);
|
||||||
remote.put(body, local.getETag(), false);
|
remote.put(body, local.getETag(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (ConflictException|PreconditionFailedException e) {
|
} catch (ConflictException|PreconditionFailedException e) {
|
||||||
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
|
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
|
||||||
App.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e);
|
App.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e);
|
||||||
@ -427,6 +431,12 @@ abstract public class SyncManager {
|
|||||||
*/
|
*/
|
||||||
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException, CalendarStorageException;
|
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException, CalendarStorageException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For post-processing of entries, for instance assigning groups.
|
||||||
|
*/
|
||||||
|
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
||||||
|
}
|
||||||
|
|
||||||
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
|
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
|
||||||
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
|
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
|
||||||
(for instance, because another client has uploaded changes), because this will simply
|
(for instance, because another client has uploaded changes), because this will simply
|
||||||
|
@ -40,6 +40,9 @@ import at.bitfire.ical4android.CalendarStorageException;
|
|||||||
import at.bitfire.ical4android.TaskProvider;
|
import at.bitfire.ical4android.TaskProvider;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
|
||||||
|
*/
|
||||||
public class TasksSyncAdapterService extends SyncAdapterService {
|
public class TasksSyncAdapterService extends SyncAdapterService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -30,6 +30,7 @@ import at.bitfire.davdroid.App;
|
|||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.ical4android.TaskProvider;
|
import at.bitfire.ical4android.TaskProvider;
|
||||||
|
import at.bitfire.vcard4android.GroupMethod;
|
||||||
|
|
||||||
public class AccountSettingsActivity extends AppCompatActivity {
|
public class AccountSettingsActivity extends AppCompatActivity {
|
||||||
public final static String EXTRA_ACCOUNT = "account";
|
public final static String EXTRA_ACCOUNT = "account";
|
||||||
@ -209,7 +210,7 @@ public class AccountSettingsActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// category: CalDAV
|
// category: CalDAV
|
||||||
final EditTextPreference prefTimeRangePastDays = (EditTextPreference)findPreference("caldav_time_range_past_days");
|
final EditTextPreference prefTimeRangePastDays = (EditTextPreference)findPreference("time_range_past_days");
|
||||||
Integer pastDays = settings.getTimeRangePastDays();
|
Integer pastDays = settings.getTimeRangePastDays();
|
||||||
if (pastDays != null) {
|
if (pastDays != null) {
|
||||||
prefTimeRangePastDays.setText(pastDays.toString());
|
prefTimeRangePastDays.setText(pastDays.toString());
|
||||||
@ -242,6 +243,18 @@ public class AccountSettingsActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// category: CardDAV
|
||||||
|
final ListPreference prefGroupMethod = (ListPreference)findPreference("contact_group_method");
|
||||||
|
prefGroupMethod.setValue(settings.getGroupMethod().name());
|
||||||
|
prefGroupMethod.setSummary(prefGroupMethod.getEntry());
|
||||||
|
prefGroupMethod.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceChange(Preference preference, Object o) {
|
||||||
|
String name = (String)o;
|
||||||
|
settings.setGroupMethod(GroupMethod.valueOf(name));
|
||||||
|
refresh(); return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,20 @@
|
|||||||
<string name="settings_sync_wifi_only_ssid_on">Will only synchronize over %s</string>
|
<string name="settings_sync_wifi_only_ssid_on">Will only synchronize over %s</string>
|
||||||
<string name="settings_sync_wifi_only_ssid_off">All WiFi connections may be used</string>
|
<string name="settings_sync_wifi_only_ssid_off">All WiFi connections may be used</string>
|
||||||
<string name="settings_sync_wifi_only_ssid_message">Enter the name of a WiFi network (SSID) to restrict synchronization to this network, or leave blank for all WiFi connections.</string>
|
<string name="settings_sync_wifi_only_ssid_message">Enter the name of a WiFi network (SSID) to restrict synchronization to this network, or leave blank for all WiFi connections.</string>
|
||||||
|
<string name="settings_carddav">CardDAV</string>
|
||||||
|
<string name="settings_contact_group_method">Contact group method</string>
|
||||||
|
<string-array name="settings_contact_group_method_values">
|
||||||
|
<item>AUTOMATIC</item>
|
||||||
|
<item>VCARD3_CATEGORIES</item>
|
||||||
|
<item>VCARD4</item>
|
||||||
|
<item>X_ADDRESSBOOK_SERVER</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="settings_contact_group_method_entries">
|
||||||
|
<item>Automatic (VCard3/VCard4)</item>
|
||||||
|
<item>VCard3 only (CATEGORIES)</item>
|
||||||
|
<item>VCard4 only (KIND/MEMBER)</item>
|
||||||
|
<item>Apple (X-ADDRESSBOOK-SERVER)</item>
|
||||||
|
</string-array>
|
||||||
<string name="settings_caldav">CalDAV</string>
|
<string name="settings_caldav">CalDAV</string>
|
||||||
<string name="settings_sync_time_range_past">Past event time limit</string>
|
<string name="settings_sync_time_range_past">Past event time limit</string>
|
||||||
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
|
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
|
||||||
@ -243,6 +257,7 @@
|
|||||||
<item>listing remote entries</item>
|
<item>listing remote entries</item>
|
||||||
<item>comparing local/remote entries</item>
|
<item>comparing local/remote entries</item>
|
||||||
<item>downloading remote entries</item>
|
<item>downloading remote entries</item>
|
||||||
|
<item>post-processing</item>
|
||||||
<item>saving sync state</item>
|
<item>saving sync state</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="sync_error_unauthorized">User name/password wrong</string>
|
<string name="sync_error_unauthorized">User name/password wrong</string>
|
||||||
|
@ -74,10 +74,21 @@
|
|||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/settings_carddav">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:key="contact_group_method"
|
||||||
|
android:persistent="false"
|
||||||
|
android:title="@string/settings_contact_group_method"
|
||||||
|
android:entries="@array/settings_contact_group_method_entries"
|
||||||
|
android:entryValues="@array/settings_contact_group_method_values"/>
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_caldav">
|
<PreferenceCategory android:title="@string/settings_caldav">
|
||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:key="caldav_time_range_past_days"
|
android:key="time_range_past_days"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:title="@string/settings_sync_time_range_past"
|
android:title="@string/settings_sync_time_range_past"
|
||||||
android:dialogMessage="@string/settings_sync_time_range_past_message"
|
android:dialogMessage="@string/settings_sync_time_range_past_message"
|
||||||
@ -90,6 +101,6 @@
|
|||||||
android:summaryOn="@string/settings_manage_calendar_colors_on"
|
android:summaryOn="@string/settings_manage_calendar_colors_on"
|
||||||
android:summaryOff="@string/settings_manage_calendar_colors_off"/>
|
android:summaryOff="@string/settings_manage_calendar_colors_off"/>
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 871d62fffc45cbe5aa061b4ed2b3bd5282a64dd1
|
Subproject commit fde96be29889d29f6ee796cf40f120fba6d50690
|
@ -1 +1 @@
|
|||||||
Subproject commit baffbc628b9ca9733c1b97671d6ae8854cd06391
|
Subproject commit ee884d351b590e2f148561b17814db6d0d9e2a69
|
@ -1 +1 @@
|
|||||||
Subproject commit 33cc8fbf59a114dbcaadfa3cc0fbba5fa01589d2
|
Subproject commit 5bca38c0300f7c18914985e9b42f38033e6aebd8
|
Loading…
Reference in New Issue
Block a user