Basic implementation of calendar sync. with common SyncManager

pull/2/head
Ricki Hirner 9 years ago
parent d348f54deb
commit 0c819c842b
No known key found for this signature in database
GPG Key ID: C4A212CF0B2B4566

4
.gitignore vendored

@ -85,6 +85,8 @@ build/
# Ignore Gradle GUI config
gradle-app.setting
### external libs ###
.svn
# Javadoc
javadoc/

3
.gitmodules vendored

@ -7,3 +7,6 @@
[submodule "MemorizingTrustManager"]
path = MemorizingTrustManager
url = https://github.com/ge0rg/MemorizingTrustManager
[submodule "ical4android"]
path = ical4android
url = git@gitlab.com:bitfireAT/ical4android.git

@ -50,11 +50,13 @@ configurations.all {
}
dependencies {
compile 'com.google.guava:guava:18.0'
compile 'dnsjava:dnsjava:2.1.7'
provided 'org.projectlombok:lombok:1.16.6'
compile('org.slf4j:slf4j-android:1.7.12')
compile project(':dav4android')
compile project(':ical4android')
compile project(':vcard4android')
compile project(':MemorizingTrustManager')

@ -144,7 +144,7 @@ public class DavResourceFinder {
member.location.toString(),
displayName != null ? displayName.displayName : null,
description != null ? description.description : null,
color != null ? DavUtils.CalDAVtoARGBColor(color.color) : null
color != null ? color.color : null
);
CalendarTimezone tz = (CalendarTimezone)member.properties.get(CalendarTimezone.NAME);

@ -24,7 +24,7 @@ import lombok.Cleanup;
import lombok.Synchronized;
public class LocalAddressBook extends AndroidAddressBook {
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
protected static final String SYNC_STATE_CTAG = "ctag";
@ -39,14 +39,15 @@ public class LocalAddressBook extends AndroidAddressBook {
/**
* Returns an array of local contacts, excluding those which have been modified locally (and not uploaded yet).
*/
@Override
public LocalContact[] getAll() throws ContactsStorageException {
LocalContact contacts[] = (LocalContact[])queryContacts(null, null);
return contacts;
return (LocalContact[])queryContacts(null, null);
}
/**
* Returns an array of local contacts which have been deleted locally. (DELETED != 0).
*/
@Override
public LocalContact[] getDeleted() throws ContactsStorageException {
return (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + "!=0", null);
}
@ -54,6 +55,7 @@ public class LocalAddressBook extends AndroidAddressBook {
/**
* Returns an array of local contacts which have been changed locally (DIRTY != 0).
*/
@Override
public LocalContact[] getDirty() throws ContactsStorageException {
return (LocalContact[])queryContacts(ContactsContract.RawContacts.DIRTY + "!=0", null);
}
@ -61,6 +63,7 @@ public class LocalAddressBook extends AndroidAddressBook {
/**
* Returns an array of local contacts which don't have a file name yet.
*/
@Override
public LocalContact[] getWithoutFileName() throws ContactsStorageException {
return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null);
}
@ -77,6 +80,7 @@ public class LocalAddressBook extends AndroidAddressBook {
syncState.clear();
}
@Override
public String getCTag() throws ContactsStorageException {
synchronized (syncState) {
readSyncState();
@ -84,6 +88,7 @@ public class LocalAddressBook extends AndroidAddressBook {
}
}
@Override
public void setCTag(String cTag) throws ContactsStorageException {
synchronized (syncState) {
readSyncState();

@ -0,0 +1,134 @@
/*
* 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 at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
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 com.google.common.base.Joiner;
import at.bitfire.ical4android.AndroidCalendar;
import at.bitfire.ical4android.AndroidCalendarFactory;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class LocalCalendar extends AndroidCalendar implements LocalCollection {
public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green"
public static final String COLUMN_CTAG = Calendars.CAL_SYNC1;
static String[] BASE_INFO_COLUMNS = new String[] {
Events._ID,
LocalEvent.COLUMN_FILENAME,
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(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws CalendarStorageException {
@Cleanup("release") ContentProviderClient provider = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
if (provider == null)
throw new CalendarStorageException("Couldn't acquire ContentProviderClient for " + CalendarContract.AUTHORITY);
ContentValues values = new ContentValues();
values.put(Calendars.NAME, info.getURL());
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle());
values.put(Calendars.CALENDAR_COLOR, info.color != null ? info.color : defaultColor);
values.put(Calendars.CALENDAR_ACCESS_LEVEL, info.readOnly ? Calendars.CAL_ACCESS_READ : Calendars.CAL_ACCESS_OWNER);
values.put(Calendars.OWNER_ACCOUNT, account.name);
values.put(Calendars.SYNC_EVENTS, 1);
if (info.timezone != null) {
// TODO parse VTIMEZONE
// values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.timezone));
}
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
values.put(Calendars.ALLOWED_AVAILABILITY, Joiner.on(",").join(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY));
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Joiner.on(",").join(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE));
return create(account, provider, values);
}
@Override
public LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException {
return (LocalEvent[])queryEvents(null, null);
}
@Override
public LocalEvent[] getDeleted() throws CalendarStorageException {
return (LocalEvent[])queryEvents(Events.DELETED + "!=0", null);
}
@Override
public LocalEvent[] getWithoutFileName() throws CalendarStorageException {
return (LocalEvent[])queryEvents(LocalEvent.COLUMN_FILENAME + " IS NULL", null);
}
@Override
public LocalResource[] getDirty() throws CalendarStorageException {
return (LocalEvent[])queryEvents(Events.DIRTY + "!=0", null);
}
@Override
public String getCTag() throws CalendarStorageException {
try {
@Cleanup Cursor cursor = provider.query(calendarSyncURI(), new String[] { COLUMN_CTAG }, null, null, null);
if (cursor != null && cursor.moveToNext())
return cursor.getString(0);
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't read local (last known) CTag", e);
}
return null;
}
@Override
public void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException {
try {
ContentValues values = new ContentValues(1);
values.put(COLUMN_CTAG, cTag);
provider.update(calendarSyncURI(), values, null, null);
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't write local (last known) CTag", 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];
}
}
}

@ -0,0 +1,27 @@
/*
* 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 at.bitfire.davdroid.resource;
import android.provider.ContactsContract;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
public interface LocalCollection {
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
String getCTag() throws CalendarStorageException, ContactsStorageException;
void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException;
}

@ -20,7 +20,7 @@ import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import ezvcard.Ezvcard;
public class LocalContact extends AndroidContact {
public class LocalContact extends AndroidContact implements LocalResource {
static {
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION;
}
@ -39,6 +39,8 @@ public class LocalContact extends AndroidContact {
values.put(ContactsContract.RawContacts.DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
addressBook.provider.update(rawContactSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't clear dirty flag", e);
}
@ -46,10 +48,14 @@ public class LocalContact extends AndroidContact {
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
try {
ContentValues values = new ContentValues(1);
values.put(COLUMN_FILENAME, uid + ".vcf");
String newFileName = uid + ".vcf";
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);
}

@ -0,0 +1,119 @@
/*
* 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 at.bitfire.davdroid.resource;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.CalendarContract;
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 lombok.Getter;
import lombok.Setter;
public class LocalEvent extends AndroidEvent implements LocalResource {
static final String COLUMN_FILENAME = CalendarContract.Events.SYNC_DATA1,
COLUMN_ETAG = CalendarContract.Events.SYNC_DATA2,
COLUMN_UID = CalendarContract.Events.UID_2445;
@Getter protected String fileName;
@Getter @Setter protected String eTag;
public LocalEvent(AndroidCalendar calendar, Event event, String fileName, String eTag) {
super(calendar, event);
this.fileName = fileName;
this.eTag = eTag;
}
protected LocalEvent(AndroidCalendar calendar, long id, ContentValues baseInfo) {
super(calendar, id, baseInfo);
fileName = baseInfo.getAsString(COLUMN_FILENAME);
eTag = baseInfo.getAsString(COLUMN_ETAG);
}
/* process LocalEvent-specific fields */
@Override
protected void populateEvent(ContentValues values) {
super.populateEvent(values);
fileName = values.getAsString(COLUMN_FILENAME);
eTag = values.getAsString(COLUMN_ETAG);
event.uid = values.getAsString(COLUMN_UID);
}
@Override
protected void buildEvent(Event recurrence, ContentProviderOperation.Builder builder) {
super.buildEvent(recurrence, builder);
builder .withValue(COLUMN_FILENAME, fileName)
.withValue(COLUMN_ETAG, eTag)
.withValue(COLUMN_UID, event.uid);
}
/* custom queries */
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
try {
String newFileName = uid + ".ics";
ContentValues values = new ContentValues(2);
values.put(COLUMN_FILENAME, 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);
calendar.provider.update(eventSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
static class Factory implements AndroidEventFactory {
static final Factory INSTANCE = new Factory();
@Override
public AndroidEvent newInstance(AndroidCalendar calendar, long id, ContentValues baseInfo) {
return new LocalEvent(calendar, id, baseInfo);
}
@Override
public AndroidEvent newInstance(AndroidCalendar calendar, Event event) {
return new LocalEvent(calendar, event, null, null);
}
@Override
public AndroidEvent[] newArray(int size) {
return new LocalEvent[size];
}
}
}

@ -0,0 +1,26 @@
/*
* 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 at.bitfire.davdroid.resource;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
public interface LocalResource {
Long getId();
String getFileName();
String getETag();
int delete() throws CalendarStorageException, ContactsStorageException;
void updateFileNameAndUID(String uuid) throws CalendarStorageException, ContactsStorageException;
void clearDirty(String eTag) throws CalendarStorageException, ContactsStorageException;
}

@ -7,6 +7,8 @@
*/
package at.bitfire.davdroid.resource;
import com.squareup.okhttp.HttpUrl;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
@ -57,6 +59,7 @@ public class ServerInfo implements Serializable {
description;
final Integer color;
/** full VTIMEZONE definition (not the TZ ID) */
String timezone;
@ -79,13 +82,9 @@ public class ServerInfo implements Serializable {
public String getTitle() {
if (title == null) {
try {
java.net.URL url = new java.net.URL(URL);
return url.getPath();
} catch (MalformedURLException e) {
return URL;
}
} else
HttpUrl url = HttpUrl.parse(URL);
return url != null ? url.toString() : "";
} else
return title;
}
}

@ -0,0 +1,213 @@
/*
* 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 at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.provider.CalendarContract.Calendars;
import android.text.TextUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ResponseBody;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.dav4android.DavCalendar;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.AddressData;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarData;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.davdroid.resource.LocalEvent;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.ical4android.AndroidHostInfo;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class CalendarSyncManager extends SyncManager {
protected static final int
MAX_MULTIGET = 30,
NOTIFICATION_ID = 2;
protected AndroidHostInfo hostInfo;
public CalendarSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result, LocalCalendar calendar) {
super(NOTIFICATION_ID, context, account, extras, provider, result);
localCollection = calendar;
}
@Override
protected void prepare() {
Thread.currentThread().setContextClassLoader(context.getClassLoader());
hostInfo = new AndroidHostInfo(context.getContentResolver());
collectionURL = HttpUrl.parse(localCalendar().getName());
davCollection = new DavCalendar(httpClient, collectionURL);
}
@Override
protected void queryCapabilities() throws DavException, IOException, HttpException, CalendarStorageException {
davCollection.propfind(0, DisplayName.NAME, CalendarColor.NAME, GetCTag.NAME);
// update name and color
DisplayName pDisplayName = (DisplayName)davCollection.properties.get(DisplayName.NAME);
String displayName = (pDisplayName != null && !TextUtils.isEmpty(pDisplayName.displayName)) ?
pDisplayName.displayName : collectionURL.toString();
CalendarColor pColor = (CalendarColor)davCollection.properties.get(CalendarColor.NAME);
int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor;
ContentValues values = new ContentValues(2);
Constants.log.info("Setting new calendar name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
values.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
values.put(Calendars.CALENDAR_COLOR, color);
localCalendar().update(values);
}
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
LocalEvent local = (LocalEvent)resource;
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
local.getEvent().toStream().toByteArray()
);
}
@Override
protected void listRemote() throws IOException, HttpException, DavException {
// fetch list of remote VEVENTs and build hash table to index file name
davCalendar().calendarQuery("VEVENT");
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
Constants.log.debug("Found remote VEVENT: " + fileName);
remoteResources.put(fileName, vCard);
}
}
@Override
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
Constants.log.info("Downloading " + toDownload.size() + " events (" + MAX_MULTIGET + " at once)");
// download new/updated iCalendars from server
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
Constants.log.info("Downloading " + Joiner.on(" + ").join(bunch));
if (bunch.length == 1) {
// only one contact, use GET
DavResource remote = bunch[0];
ResponseBody body = remote.get("text/calendar");
String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag;
@Cleanup InputStream stream = body.byteStream();
processVEvent(remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8));
} else {
// multiple contacts, use multi-get
List<HttpUrl> urls = new LinkedList<>();
for (DavResource remote : bunch)
urls.add(remote.location);
davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()]));
// process multiget results
for (DavResource remote : davCollection.members) {
String eTag;
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
if (getETag != null)
eTag = getETag.eTag;
else
throw new DavException("Received multi-get response without ETag");
Charset charset = Charsets.UTF_8;
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
if (getContentType != null && getContentType.type != null) {
MediaType type = MediaType.parse(getContentType.type);
if (type != null)
charset = type.charset(Charsets.UTF_8);
}
CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME);
if (calendarData == null || calendarData.iCalendar == null)
throw new DavException("Received multi-get response without address data");
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes());
processVEvent(remote.fileName(), eTag, stream, charset);
}
}
}
}
// helpers
private LocalCalendar localCalendar() { return ((LocalCalendar)localCollection); }
private DavCalendar davCalendar() { return (DavCalendar)davCollection; }
private void processVEvent(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
Event[] events;
try {
events = Event.fromStream(stream, charset, hostInfo);
} catch (InvalidCalendarException e) {
Constants.log.error("Received invalid iCalendar, ignoring");
return;
}
if (events.length == 1) {
Event newData = events[0];
// delete local event, if it exists
LocalEvent localEvent = (LocalEvent)localResources.get(fileName);
if (localEvent != null) {
Constants.log.info("Updating " + fileName + " in local calendar");
localEvent.setETag(eTag);
localEvent.update(newData);
syncResult.stats.numUpdates++;
} else {
Constants.log.info("Adding " + fileName + " to local calendar");
localEvent = new LocalEvent(localCalendar(), newData, fileName, eTag);
localEvent.add();
syncResult.stats.numInserts++;
}
} else
Constants.log.error("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName);
}
}

@ -17,6 +17,11 @@ import android.content.SyncResult;
import android.os.Bundle;
import android.os.IBinder;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.ical4android.CalendarStorageException;
public class CalendarsSyncAdapterService extends Service {
private static SyncAdapter syncAdapter;
@ -38,14 +43,25 @@ public class CalendarsSyncAdapterService extends Service {
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
public SyncAdapter(Context context) {
super(context, false);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Constants.log.info("Starting calendar sync (" + authority + ")");
try {
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.findAll(account, provider, LocalCalendar.Factory.INSTANCE)) {
Constants.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, provider, syncResult, calendar);
syncManager.performSync();
}
} catch (CalendarStorageException e) {
Constants.log.error("Couldn't get list of local calendars", e);
}
Constants.log.info("Calendar sync complete");
}
}

@ -22,8 +22,6 @@ import at.bitfire.davdroid.Constants;
public class ContactsSyncAdapterService extends Service {
private static ContactsSyncAdapter syncAdapter;
@Override
public void onCreate() {
if (syncAdapter == null)
@ -48,12 +46,12 @@ public class ContactsSyncAdapterService extends Service {
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Constants.log.info("Starting contacts sync (" + authority + ")");
Constants.log.info("Starting address book sync (" + authority + ")");
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, extras, provider, syncResult);
syncManager.performSync();
Constants.log.info("Sync complete for authority " + authority);
Constants.log.info("Address book sync complete");
}
}

@ -10,7 +10,6 @@ package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
@ -29,18 +28,15 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import at.bitfire.dav4android.DavAddressBook;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.exception.PreconditionFailedException;
import at.bitfire.dav4android.property.AddressData;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
@ -51,6 +47,7 @@ import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import ezvcard.VCardVersion;
@ -63,17 +60,8 @@ public class ContactsSyncManager extends SyncManager {
MAX_MULTIGET = 10,
NOTIFICATION_ID = 1;
protected HttpUrl addressBookURL;
protected DavAddressBook davCollection;
protected boolean hasVCard4;
protected LocalAddressBook addressBook;
String currentCTag;
Map<String, LocalContact> localContacts;
Map<String, DavResource> remoteContacts;
Set<DavResource> toDownload;
public ContactsSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result) {
super(NOTIFICATION_ID, context, account, extras, provider, result);
@ -82,11 +70,11 @@ public class ContactsSyncManager extends SyncManager {
@Override
protected void prepare() {
addressBookURL = HttpUrl.parse(settings.getAddressBookURL());
davCollection = new DavAddressBook(httpClient, addressBookURL);
collectionURL = HttpUrl.parse(settings.getAddressBookURL());
davCollection = new DavAddressBook(httpClient, collectionURL);
// prepare local address book
addressBook = new LocalAddressBook(account, provider);
localCollection = new LocalAddressBook(account, provider);
}
@Override
@ -103,159 +91,23 @@ public class ContactsSyncManager extends SyncManager {
}
@Override
protected void processLocallyDeleted() throws ContactsStorageException {
// Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before),
// but only if they don't have changed on the server. Then finally remove them from the local address book.
LocalContact[] localList = addressBook.getDeleted();
for (LocalContact local : localList) {
final String fileName = local.getFileName();
if (!TextUtils.isEmpty(fileName)) {
Constants.log.info(fileName + " has been deleted locally -> deleting from server");
try {
new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build())
.delete(local.eTag);
} catch (IOException | HttpException e) {
Constants.log.warn("Couldn't delete " + fileName + " from server");
}
} else
Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded");
local.delete();
syncResult.stats.numDeletes++;
}
}
@Override
protected void processLocallyCreated() throws ContactsStorageException {
// assign file names and UIDs to new contacts so that we can use the file name as an index
for (LocalContact local : addressBook.getWithoutFileName()) {
String uuid = UUID.randomUUID().toString();
Constants.log.info("Found local contact #" + local.getId() + " without file name; assigning name UID/name " + uuid + "[.vcf]");
local.updateFileNameAndUID(uuid);
}
}
@Override
protected void uploadDirty() throws ContactsStorageException, IOException, HttpException {
// upload dirty contacts
for (LocalContact local : addressBook.getDirty()) {
final String fileName = local.getFileName();
DavResource remote = new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build());
RequestBody vCard = RequestBody.create(
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
);
try {
if (local.eTag == null) {
Constants.log.info("Uploading new contact " + fileName);
remote.put(vCard, null, true);
// TODO handle 30x
} else {
Constants.log.info("Uploading locally modified contact " + fileName);
remote.put(vCard, local.eTag, false);
// TODO handle 30x
}
} catch (PreconditionFailedException e) {
Constants.log.info("Contact has been modified on the server before upload, ignoring", e);
}
String eTag = null;
GetETag newETag = (GetETag) remote.properties.get(GetETag.NAME);
if (newETag != null) {
eTag = newETag.eTag;
Constants.log.debug("Received new ETag=" + eTag + " after uploading");
} else
Constants.log.debug("Didn't receive new ETag after uploading, setting to null");
local.clearDirty(eTag);
}
}
@Override
protected boolean checkSyncState() throws ContactsStorageException {
// check CTag (ignore on manual sync)
currentCTag = null;
GetCTag getCTag = (GetCTag) davCollection.properties.get(GetCTag.NAME);
if (getCTag != null)
currentCTag = getCTag.cTag;
String localCTag = null;
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
Constants.log.info("Manual sync, ignoring CTag");
else
localCTag = addressBook.getCTag();
if (currentCTag != null && currentCTag.equals(localCTag)) {
Constants.log.info("Remote address book didn't change (CTag=" + currentCTag + "), no need to list VCards");
return false;
} else
return true;
protected RequestBody prepareUpload(LocalResource resource) throws IOException, ContactsStorageException {
LocalContact local = (LocalContact)resource;
return RequestBody.create(
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
);
}
@Override
protected void listLocal() throws ContactsStorageException {
// fetch list of local contacts and build hash table to index file name
LocalContact[] localList = addressBook.getAll();
localContacts = new HashMap<>(localList.length);
for (LocalContact contact : localList) {
Constants.log.debug("Found local contact: " + contact.getFileName());
localContacts.put(contact.getFileName(), contact);
}
}
@Override
protected void listRemote() throws IOException, HttpException, DavException, ContactsStorageException {
protected void listRemote() throws IOException, HttpException, DavException {
// fetch list of remote VCards and build hash table to index file name
Constants.log.info("Listing remote VCards");
davCollection.queryMemberETags();
remoteContacts = new HashMap<>(davCollection.members.size());
davAddressBook().addressbookQuery();
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
Constants.log.debug("Found remote VCard: " + fileName);
remoteContacts.put(fileName, vCard);
}
}
@Override
protected void compareEntries() throws IOException, HttpException, DavException, ContactsStorageException {
/* check which contacts
1. are not present anymore remotely -> delete immediately on local side
2. updated remotely -> add to downloadNames
3. added remotely -> add to downloadNames
*/
toDownload = new HashSet<>();
for (String localName : localContacts.keySet()) {
DavResource remote = remoteContacts.get(localName);
if (remote == null) {
Constants.log.info(localName + " is not on server anymore, deleting");
localContacts.get(localName).delete();
syncResult.stats.numDeletes++;
} else {
// contact is still on server, check whether it has been updated remotely
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
if (getETag == null || getETag.eTag == null)
throw new DavException("Server didn't provide ETag");
String localETag = localContacts.get(localName).eTag,
remoteETag = getETag.eTag;
if (remoteETag.equals(localETag))
syncResult.stats.numSkippedEntries++;
else {
Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")");
toDownload.add(remote);
}
// remote entry has been seen, remove from list
remoteContacts.remove(localName);
}
}
// add all unseen (= remotely added) remote contacts
if (!remoteContacts.isEmpty()) {
Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteContacts.keySet()));
toDownload.addAll(remoteContacts.values());
remoteResources.put(fileName, vCard);
}
}
@ -264,7 +116,7 @@ public class ContactsSyncManager extends SyncManager {
Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
// prepare downloader which may be used to download external resource like contact photos
Contact.Downloader downloader = new ResourceDownloader(httpClient, addressBookURL);
Contact.Downloader downloader = new ResourceDownloader(httpClient, collectionURL);
// download new/updated VCards from server
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
@ -278,14 +130,14 @@ public class ContactsSyncManager extends SyncManager {
String eTag = ((GetETag) remote.properties.get(GetETag.NAME)).eTag;
@Cleanup InputStream stream = body.byteStream();
processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader);
processVCard(remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader);
} else {
// multiple contacts, use multi-get
List<HttpUrl> urls = new LinkedList<>();
for (DavResource remote : bunch)
urls.add(remote.location);
davCollection.multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
davAddressBook().multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
// process multiget results
for (DavResource remote : davCollection.members) {
@ -309,28 +161,25 @@ public class ContactsSyncManager extends SyncManager {
throw new DavException("Received multi-get response without address data");
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader);
processVCard(remote.fileName(), eTag, stream, charset, downloader);
}
}
}
}
@Override
protected void saveSyncState() throws ContactsStorageException {
/* 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
cause all remote entries to be listed at the next sync. */
Constants.log.info("Saving sync state: CTag=" + currentCTag);
addressBook.setCTag(currentCTag);
}
private void processVCard(SyncResult syncResult, LocalAddressBook addressBook, Map<String, LocalContact>localContacts, String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
// helpers
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
if (contacts.length == 1) {
Contact newData = contacts[0];
// delete local contact, if it exists
LocalContact localContact = localContacts.get(fileName);
LocalContact localContact = (LocalContact)localResources.get(fileName);
if (localContact != null) {
Constants.log.info("Updating " + fileName + " in local address book");
localContact.eTag = eTag;
@ -338,7 +187,7 @@ public class ContactsSyncManager extends SyncManager {
syncResult.stats.numUpdates++;
} else {
Constants.log.info("Adding " + fileName + " to local address book");
localContact = new LocalContact(addressBook, newData, fileName, eTag);
localContact = new LocalContact(localAddressBook(), newData, fileName, eTag);
localContact.add();
syncResult.stats.numInserts++;
}
@ -347,8 +196,10 @@ public class ContactsSyncManager extends SyncManager {
}
// downloader helper class
@RequiredArgsConstructor
static class ResourceDownloader implements Contact.Downloader {
private static class ResourceDownloader implements Contact.Downloader {
final HttpClient httpClient;
final HttpUrl baseUrl;

@ -12,20 +12,37 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.RequestBody;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.exception.PreconditionFailedException;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.davdroid.ui.DebugInfoActivity;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
abstract public class SyncManager {
@ -38,21 +55,40 @@ abstract public class SyncManager {
SYNC_PHASE_CHECK_SYNC_STATE = 5,
SYNC_PHASE_LIST_LOCAL = 6,
SYNC_PHASE_LIST_REMOTE = 7,
SYNC_PHASE_COMPARE_ENTRIES = 8,
SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8,
SYNC_PHASE_DOWNLOAD_REMOTE = 9,
SYNC_PHASE_SAVE_SYNC_STATE = 10;
final NotificationManager notificationManager;
final int notificationId;
protected final NotificationManager notificationManager;
protected final int notificationId;
protected final Context context;
protected final Account account;
protected final Bundle extras;
protected final ContentProviderClient provider;
protected final SyncResult syncResult;
protected final AccountSettings settings;
protected LocalCollection localCollection;
protected final HttpClient httpClient;
protected HttpUrl collectionURL;
protected DavResource davCollection;
/** remote CTag at the time of {@link #listRemote()} */
protected String remoteCTag = null;
/** sync-able resources in the local collection, as enumerated by {@link #listLocal()} */
protected Map<String, LocalResource> localResources;
/** sync-able resources in the remote collection, as enumerated by {@link #listRemote()} */
protected Map<String, DavResource> remoteResources;
/** resources which have changed on the server, as determined by {@link #compareLocalRemote()} */
protected Set<DavResource> toDownload;
final Context context;
final Account account;
final Bundle extras;
final ContentProviderClient provider;
final SyncResult syncResult;
final AccountSettings settings;
final HttpClient httpClient;
public SyncManager(int notificationId, Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult syncResult) {
this.context = context;
@ -73,35 +109,46 @@ abstract public class SyncManager {
public void performSync() {
int syncPhase = SYNC_PHASE_PREPARE;
try {
Constants.log.info("Preparing synchronization");
prepare();
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
Constants.log.info("Querying capabilities");
queryCapabilities();
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
Constants.log.info("Processing locally deleted entries");
processLocallyDeleted();
syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED;
Constants.log.info("Processing locally created entries");
processLocallyCreated();
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
Constants.log.info("Uploading dirty entries");
uploadDirty();
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE;
Constants.log.info("Checking sync state");
if (checkSyncState()) {
syncPhase = SYNC_PHASE_LIST_LOCAL;
Constants.log.info("Listing local entries");
listLocal();
syncPhase = SYNC_PHASE_LIST_REMOTE;
Constants.log.info("Listing remote entries");
listRemote();
syncPhase = SYNC_PHASE_COMPARE_ENTRIES;
compareEntries();
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE;
Constants.log.info("Comparing local/remote entries");
compareLocalRemote();
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
Constants.log.info("Downloading remote entries");
downloadRemote();
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
Constants.log.info("Saving sync state");
saveSyncState();
} else
Constants.log.info("Remote collection didn't change, skipping remote sync");
@ -110,8 +157,8 @@ abstract public class SyncManager {
Constants.log.error("I/O exception during sync, trying again later", e);
syncResult.stats.numIoExceptions++;
} catch(HttpException e) {
Constants.log.error("HTTP Exception during sync", e);
} catch(HttpException|DavException e) {
Constants.log.error("HTTP/DAV Exception during sync", e);
syncResult.stats.numParseExceptions++;
Intent detailsIntent = new Intent(context, DebugInfoActivity.class);
@ -138,40 +185,188 @@ abstract public class SyncManager {
}
notificationManager.notify(account.name, notificationId, notification);
} catch(DavException e) {
// TODO
} catch(ContactsStorageException e) {
} catch(CalendarStorageException|ContactsStorageException e) {
Constants.log.error("Couldn't access local storage", e);
syncResult.databaseError = true;
}
}
abstract protected void prepare();
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, ContactsStorageException;
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, CalendarStorageException, ContactsStorageException;
protected void processLocallyDeleted() throws CalendarStorageException, ContactsStorageException {
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
// but only if they don't have changed on the server. Then finally remove them from the local address book.
LocalResource[] localList = localCollection.getDeleted();
for (LocalResource local : localList) {
final String fileName = local.getFileName();
if (!TextUtils.isEmpty(fileName)) {
Constants.log.info(fileName + " has been deleted locally -> deleting from server");
try {
new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
.delete(local.getETag());
} catch (IOException | HttpException e) {
Constants.log.warn("Couldn't delete " + fileName + " from server");
}
} else
Constants.log.info("Removing local record #" + local.getId() + " which has been deleted locally and was never uploaded");
local.delete();
syncResult.stats.numDeletes++;
}
}
protected void processLocallyCreated() throws CalendarStorageException, ContactsStorageException {
// assign file names and UIDs to new contacts so that we can use the file name as an index
for (LocalResource local : localCollection.getWithoutFileName()) {
String uuid = UUID.randomUUID().toString();
Constants.log.info("Found local record #" + local.getId() + " without file name; assigning file name/UID based on " + uuid);
local.updateFileNameAndUID(uuid);
}
}
abstract protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException, ContactsStorageException;
abstract protected void processLocallyDeleted() throws IOException, HttpException, DavException, ContactsStorageException;
protected void uploadDirty() throws IOException, HttpException, CalendarStorageException, ContactsStorageException {
// upload dirty contacts
for (LocalResource local : localCollection.getDirty()) {
final String fileName = local.getFileName();
abstract protected void processLocallyCreated() throws IOException, HttpException, DavException, ContactsStorageException;
DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
abstract protected void uploadDirty() throws IOException, HttpException, DavException, ContactsStorageException;
// generate entity to upload (VCard, iCal, whatever)
RequestBody body = prepareUpload(local);
try {
if (local.getETag() == null) {
Constants.log.info("Uploading new record " + fileName);
remote.put(body, null, true);
// TODO handle 30x
} else {
Constants.log.info("Uploading locally modified record " + fileName);
remote.put(body, local.getETag(), false);
// TODO handle 30x
}
} catch (PreconditionFailedException e) {
Constants.log.info("Resource has been modified on the server before upload, ignoring", e);
}
String eTag = null;
GetETag newETag = (GetETag) remote.properties.get(GetETag.NAME);
if (newETag != null) {
eTag = newETag.eTag;
Constants.log.debug("Received new ETag=" + eTag + " after uploading");
} else
Constants.log.debug("Didn't receive new ETag after uploading, setting to null");
local.clearDirty(eTag);
}
}
/**
* Checks the current sync state (e.g. CTag) and whether synchronization from remote is required.
* @return true if the remote collection has changed, i.e. synchronization from remote is required
* false if the remote collection hasn't changed
* @return <ul>
* <li><code>true</code> if the remote collection has changed, i.e. synchronization from remote is required</li>
* <li><code>false</code> if the remote collection hasn't changed</li>
* </ul>
*/
abstract protected boolean checkSyncState() throws IOException, HttpException, DavException, ContactsStorageException;
protected boolean checkSyncState() throws CalendarStorageException, ContactsStorageException {
// check CTag (ignore on manual sync)
GetCTag getCTag = (GetCTag)davCollection.properties.get(GetCTag.NAME);
if (getCTag != null)
remoteCTag = getCTag.cTag;
String localCTag = null;
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
Constants.log.info("Manual sync, ignoring CTag");
else
localCTag = localCollection.getCTag();
if (remoteCTag != null && remoteCTag.equals(localCTag)) {
Constants.log.info("Remote collection didn't change (CTag=" + remoteCTag + "), no need to query children");
return false;
} else
return true;
}
abstract protected void listLocal() throws IOException, HttpException, DavException, ContactsStorageException;
/**
* Lists all local resources which should be taken into account for synchronization into {@link #localResources}.
*/
protected void listLocal() throws CalendarStorageException, ContactsStorageException {
// fetch list of local contacts and build hash table to index file name
LocalResource[] localList = localCollection.getAll();
localResources = new HashMap<>(localList.length);
for (LocalResource resource : localList) {
Constants.log.debug("Found local resource: " + resource.getFileName());
localResources.put(resource.getFileName(), resource);
}
}
abstract protected void listRemote() throws IOException, HttpException, DavException, ContactsStorageException;
/**
* Lists all members of the remote collection which should be taken into account for synchronization into {@link #remoteResources}.
*/
abstract protected void listRemote() throws IOException, HttpException, DavException;
abstract protected void compareEntries() throws IOException, HttpException, DavException, ContactsStorageException;
/**
* Compares {@link #localResources} and {@link #remoteResources} by file name and ETag:
* <ul>
* <li>Local resources which are not available in the remote collection (anymore) will be removed.</li>
* <li>Resources whose remote ETag has changed will be added into {@link #toDownload}</li>
* </ul>
*/
protected void compareLocalRemote() throws IOException, HttpException, DavException, CalendarStorageException, ContactsStorageException {
/* check which contacts
1. are not present anymore remotely -> delete immediately on local side
2. updated remotely -> add to downloadNames
3. added remotely -> add to downloadNames
*/
toDownload = new HashSet<>();
for (String localName : localResources.keySet()) {
DavResource remote = remoteResources.get(localName);
if (remote == null) {
Constants.log.info(localName + " is not on server anymore, deleting");
localResources.get(localName).delete();
syncResult.stats.numDeletes++;
} else {
// contact is still on server, check whether it has been updated remotely
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
if (getETag == null || getETag.eTag == null)
throw new DavException("Server didn't provide ETag");
String localETag = localResources.get(localName).getETag(),
remoteETag = getETag.eTag;
if (remoteETag.equals(localETag))
syncResult.stats.numSkippedEntries++;
else {
Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")");
toDownload.add(remote);
}
// remote entry has been seen, remove from list
remoteResources.remove(localName);
}
}
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException;
// add all unseen (= remotely added) remote contacts
if (!remoteResources.isEmpty()) {
Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteResources.keySet()));
toDownload.addAll(remoteResources.values());
}
}
abstract protected void saveSyncState() throws IOException, HttpException, DavException, ContactsStorageException;
/**
* Downloads the remote resources in {@link #toDownload} and stores them locally.
*/
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException, CalendarStorageException;
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
/* 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
cause all remote entries to be listed at the next sync. */
Constants.log.info("Saving CTag=" + remoteCTag);
localCollection.setCTag(remoteCTag);
}
}

@ -14,6 +14,7 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
@ -28,13 +29,16 @@ import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Calendar;
import java.util.List;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.ServerInfo;
import at.bitfire.davdroid.syncadapter.AccountSettings;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
@ -105,14 +109,18 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
}
});
/*addSync(account, CalendarContract.AUTHORITY, serverInfo.getCalendars(), new AddSyncCallback() {
addSync(account, CalendarContract.AUTHORITY, serverInfo.getCalendars(), new AddSyncCallback() {
@Override
public void createLocalCollection(Account account, ServerInfo.ResourceInfo calendar) throws LocalStorageException {
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
public void createLocalCollection(Account account, ServerInfo.ResourceInfo calendar) {
try {
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
} catch(CalendarStorageException e) {
Constants.log.error("Couldn't create local calendar", e);
}
}
});
addSync(account, LocalTaskList.TASKS_AUTHORITY, serverInfo.getTaskLists(), new AddSyncCallback() {
/*addSync(account, LocalTaskList.TASKS_AUTHORITY, serverInfo.getTaskLists(), new AddSyncCallback() {
@Override
public void createLocalCollection(Account account, ServerInfo.ResourceInfo todoList) throws LocalStorageException {
LocalTaskList.create(account, getActivity().getContentResolver(), todoList);

File diff suppressed because it is too large Load Diff

@ -1,56 +0,0 @@
/*
* Copyright (C) 2014 Marten Gajda <marten@dmfs.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.dmfs.provider.tasks;
import java.util.HashMap;
import java.util.Map;
import android.net.Uri;
public class UriFactory
{
public final String authority;
private final Map<String, Uri> mUriMap = new HashMap<String, Uri>(16);
UriFactory(String authority)
{
this.authority = authority;
mUriMap.put((String) null, Uri.parse("content://" + authority));
}
void addUri(String path)
{
mUriMap.put(path, Uri.parse("content://" + authority + "/" + path));
}
public Uri getUri()
{
return mUriMap.get(null);
}
public Uri getUri(String path)
{
return mUriMap.get(path);
}
}

@ -1 +1 @@
Subproject commit e6c3ee6da90a94d3c77675b8fdd9be7e2d5f83e3
Subproject commit 7530deb497c7c0ee78a583e7371ef9bfc4458a2e

@ -0,0 +1 @@
Subproject commit 4e1131ae4607b4220e2d37632fd54a987b633849

@ -8,5 +8,7 @@
include ':app'
include ':dav4android'
include ':ical4android'
include ':vcard4android'
include ':MemorizingTrustManager'

@ -1 +1 @@
Subproject commit 384de9ec6eab1ac36d875330599b2858ce6ba888
Subproject commit 53c1695db02cc371369e05cb02a0f1e537ac9eec
Loading…
Cancel
Save