mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 19:50:18 +00:00
Basic implementation of calendar sync. with common SyncManager
This commit is contained in:
parent
fa4f090ff7
commit
de7ca7b91c
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -85,6 +85,8 @@ build/
|
||||||
# Ignore Gradle GUI config
|
# Ignore Gradle GUI config
|
||||||
gradle-app.setting
|
gradle-app.setting
|
||||||
|
|
||||||
|
|
||||||
### external libs ###
|
### external libs ###
|
||||||
.svn
|
.svn
|
||||||
|
|
||||||
|
# Javadoc
|
||||||
|
javadoc/
|
||||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -7,3 +7,6 @@
|
||||||
[submodule "MemorizingTrustManager"]
|
[submodule "MemorizingTrustManager"]
|
||||||
path = MemorizingTrustManager
|
path = MemorizingTrustManager
|
||||||
url = https://github.com/ge0rg/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 {
|
dependencies {
|
||||||
|
compile 'com.google.guava:guava:18.0'
|
||||||
compile 'dnsjava:dnsjava:2.1.7'
|
compile 'dnsjava:dnsjava:2.1.7'
|
||||||
provided 'org.projectlombok:lombok:1.16.6'
|
provided 'org.projectlombok:lombok:1.16.6'
|
||||||
compile('org.slf4j:slf4j-android:1.7.12')
|
compile('org.slf4j:slf4j-android:1.7.12')
|
||||||
|
|
||||||
compile project(':dav4android')
|
compile project(':dav4android')
|
||||||
|
compile project(':ical4android')
|
||||||
compile project(':vcard4android')
|
compile project(':vcard4android')
|
||||||
|
|
||||||
compile project(':MemorizingTrustManager')
|
compile project(':MemorizingTrustManager')
|
||||||
|
|
|
@ -144,7 +144,7 @@ public class DavResourceFinder {
|
||||||
member.location.toString(),
|
member.location.toString(),
|
||||||
displayName != null ? displayName.displayName : null,
|
displayName != null ? displayName.displayName : null,
|
||||||
description != null ? description.description : 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);
|
CalendarTimezone tz = (CalendarTimezone)member.properties.get(CalendarTimezone.NAME);
|
||||||
|
|
|
@ -24,7 +24,7 @@ import lombok.Cleanup;
|
||||||
import lombok.Synchronized;
|
import lombok.Synchronized;
|
||||||
|
|
||||||
|
|
||||||
public class LocalAddressBook extends AndroidAddressBook {
|
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
|
||||||
|
|
||||||
protected static final String SYNC_STATE_CTAG = "ctag";
|
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).
|
* Returns an array of local contacts, excluding those which have been modified locally (and not uploaded yet).
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public LocalContact[] getAll() throws ContactsStorageException {
|
public LocalContact[] getAll() throws ContactsStorageException {
|
||||||
LocalContact contacts[] = (LocalContact[])queryContacts(null, null);
|
return (LocalContact[])queryContacts(null, null);
|
||||||
return contacts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of local contacts which have been deleted locally. (DELETED != 0).
|
* Returns an array of local contacts which have been deleted locally. (DELETED != 0).
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public LocalContact[] getDeleted() throws ContactsStorageException {
|
public LocalContact[] getDeleted() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + "!=0", null);
|
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).
|
* Returns an array of local contacts which have been changed locally (DIRTY != 0).
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public LocalContact[] getDirty() throws ContactsStorageException {
|
public LocalContact[] getDirty() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(ContactsContract.RawContacts.DIRTY + "!=0", null);
|
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.
|
* Returns an array of local contacts which don't have a file name yet.
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public LocalContact[] getWithoutFileName() throws ContactsStorageException {
|
public LocalContact[] getWithoutFileName() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null);
|
return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null);
|
||||||
}
|
}
|
||||||
|
@ -77,6 +80,7 @@ public class LocalAddressBook extends AndroidAddressBook {
|
||||||
syncState.clear();
|
syncState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getCTag() throws ContactsStorageException {
|
public String getCTag() throws ContactsStorageException {
|
||||||
synchronized (syncState) {
|
synchronized (syncState) {
|
||||||
readSyncState();
|
readSyncState();
|
||||||
|
@ -84,6 +88,7 @@ public class LocalAddressBook extends AndroidAddressBook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setCTag(String cTag) throws ContactsStorageException {
|
public void setCTag(String cTag) throws ContactsStorageException {
|
||||||
synchronized (syncState) {
|
synchronized (syncState) {
|
||||||
readSyncState();
|
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 at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import ezvcard.Ezvcard;
|
import ezvcard.Ezvcard;
|
||||||
|
|
||||||
public class LocalContact extends AndroidContact {
|
public class LocalContact extends AndroidContact implements LocalResource {
|
||||||
static {
|
static {
|
||||||
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION;
|
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(ContactsContract.RawContacts.DIRTY, 0);
|
||||||
values.put(COLUMN_ETAG, eTag);
|
values.put(COLUMN_ETAG, eTag);
|
||||||
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
||||||
|
|
||||||
|
this.eTag = eTag;
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new ContactsStorageException("Couldn't clear dirty flag", 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 {
|
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
|
||||||
try {
|
try {
|
||||||
ContentValues values = new ContentValues(1);
|
String newFileName = uid + ".vcf";
|
||||||
values.put(COLUMN_FILENAME, uid + ".vcf");
|
|
||||||
|
ContentValues values = new ContentValues(2);
|
||||||
|
values.put(COLUMN_FILENAME, newFileName);
|
||||||
values.put(COLUMN_UID, uid);
|
values.put(COLUMN_UID, uid);
|
||||||
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
||||||
|
|
||||||
|
fileName = newFileName;
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new ContactsStorageException("Couldn't update UID", e);
|
throw new ContactsStorageException("Couldn't update UID", e);
|
||||||
}
|
}
|
||||||
|
|
119
app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java
Normal file
119
app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java
Normal file
|
@ -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;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.HttpUrl;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -57,6 +59,7 @@ public class ServerInfo implements Serializable {
|
||||||
description;
|
description;
|
||||||
final Integer color;
|
final Integer color;
|
||||||
|
|
||||||
|
/** full VTIMEZONE definition (not the TZ ID) */
|
||||||
String timezone;
|
String timezone;
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,13 +82,9 @@ public class ServerInfo implements Serializable {
|
||||||
|
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
if (title == null) {
|
if (title == null) {
|
||||||
try {
|
HttpUrl url = HttpUrl.parse(URL);
|
||||||
java.net.URL url = new java.net.URL(URL);
|
return url != null ? url.toString() : "–";
|
||||||
return url.getPath();
|
} else
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
return URL;
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
return title;
|
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.Bundle;
|
||||||
import android.os.IBinder;
|
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 {
|
public class CalendarsSyncAdapterService extends Service {
|
||||||
private static SyncAdapter syncAdapter;
|
private static SyncAdapter syncAdapter;
|
||||||
|
|
||||||
|
@ -38,14 +43,25 @@ public class CalendarsSyncAdapterService extends Service {
|
||||||
|
|
||||||
|
|
||||||
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
|
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
public SyncAdapter(Context context) {
|
public SyncAdapter(Context context) {
|
||||||
super(context, false);
|
super(context, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
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 {
|
public class ContactsSyncAdapterService extends Service {
|
||||||
private static ContactsSyncAdapter syncAdapter;
|
private static ContactsSyncAdapter syncAdapter;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
if (syncAdapter == null)
|
if (syncAdapter == null)
|
||||||
|
@ -48,12 +46,12 @@ public class ContactsSyncAdapterService extends Service {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
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);
|
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, extras, provider, syncResult);
|
||||||
syncManager.performSync();
|
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.accounts.Account;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -29,18 +28,15 @@ 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.HashSet;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavAddressBook;
|
import at.bitfire.dav4android.DavAddressBook;
|
||||||
import at.bitfire.dav4android.DavResource;
|
import at.bitfire.dav4android.DavResource;
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
import at.bitfire.dav4android.exception.DavException;
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
import at.bitfire.dav4android.exception.HttpException;
|
||||||
import at.bitfire.dav4android.exception.PreconditionFailedException;
|
|
||||||
import at.bitfire.dav4android.property.AddressData;
|
import at.bitfire.dav4android.property.AddressData;
|
||||||
import at.bitfire.dav4android.property.GetCTag;
|
import at.bitfire.dav4android.property.GetCTag;
|
||||||
import at.bitfire.dav4android.property.GetContentType;
|
import at.bitfire.dav4android.property.GetContentType;
|
||||||
|
@ -51,6 +47,7 @@ import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||||
import at.bitfire.davdroid.resource.LocalContact;
|
import at.bitfire.davdroid.resource.LocalContact;
|
||||||
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import ezvcard.VCardVersion;
|
import ezvcard.VCardVersion;
|
||||||
|
@ -63,17 +60,8 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
MAX_MULTIGET = 10,
|
MAX_MULTIGET = 10,
|
||||||
NOTIFICATION_ID = 1;
|
NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
protected HttpUrl addressBookURL;
|
|
||||||
protected DavAddressBook davCollection;
|
|
||||||
protected boolean hasVCard4;
|
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) {
|
public ContactsSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result) {
|
||||||
super(NOTIFICATION_ID, context, account, extras, provider, result);
|
super(NOTIFICATION_ID, context, account, extras, provider, result);
|
||||||
|
@ -82,11 +70,11 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepare() {
|
protected void prepare() {
|
||||||
addressBookURL = HttpUrl.parse(settings.getAddressBookURL());
|
collectionURL = HttpUrl.parse(settings.getAddressBookURL());
|
||||||
davCollection = new DavAddressBook(httpClient, addressBookURL);
|
davCollection = new DavAddressBook(httpClient, collectionURL);
|
||||||
|
|
||||||
// prepare local address book
|
// prepare local address book
|
||||||
addressBook = new LocalAddressBook(account, provider);
|
localCollection = new LocalAddressBook(account, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -103,159 +91,23 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void processLocallyDeleted() throws ContactsStorageException {
|
protected RequestBody prepareUpload(LocalResource resource) throws IOException, ContactsStorageException {
|
||||||
// Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before),
|
LocalContact local = (LocalContact)resource;
|
||||||
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
return RequestBody.create(
|
||||||
LocalContact[] localList = addressBook.getDeleted();
|
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
||||||
for (LocalContact local : localList) {
|
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
|
||||||
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
|
@Override
|
||||||
protected void processLocallyCreated() throws ContactsStorageException {
|
protected void listRemote() throws IOException, HttpException, DavException {
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
// fetch list of remote VCards and build hash table to index file name
|
// fetch list of remote VCards and build hash table to index file name
|
||||||
Constants.log.info("Listing remote VCards");
|
davAddressBook().addressbookQuery();
|
||||||
davCollection.queryMemberETags();
|
remoteResources = new HashMap<>(davCollection.members.size());
|
||||||
remoteContacts = new HashMap<>(davCollection.members.size());
|
|
||||||
for (DavResource vCard : davCollection.members) {
|
for (DavResource vCard : davCollection.members) {
|
||||||
String fileName = vCard.fileName();
|
String fileName = vCard.fileName();
|
||||||
Constants.log.debug("Found remote VCard: " + fileName);
|
Constants.log.debug("Found remote VCard: " + fileName);
|
||||||
remoteContacts.put(fileName, vCard);
|
remoteResources.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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +116,7 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
||||||
|
|
||||||
// prepare downloader which may be used to download external resource like contact photos
|
// 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
|
// download new/updated VCards from server
|
||||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
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;
|
String eTag = ((GetETag) remote.properties.get(GetETag.NAME)).eTag;
|
||||||
|
|
||||||
@Cleanup InputStream stream = body.byteStream();
|
@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 {
|
} else {
|
||||||
// multiple contacts, use multi-get
|
// multiple contacts, use multi-get
|
||||||
List<HttpUrl> urls = new LinkedList<>();
|
List<HttpUrl> urls = new LinkedList<>();
|
||||||
for (DavResource remote : bunch)
|
for (DavResource remote : bunch)
|
||||||
urls.add(remote.location);
|
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
|
// process multiget results
|
||||||
for (DavResource remote : davCollection.members) {
|
for (DavResource remote : davCollection.members) {
|
||||||
|
@ -309,28 +161,25 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
throw new DavException("Received multi-get response without address data");
|
throw new DavException("Received multi-get response without address data");
|
||||||
|
|
||||||
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
|
@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);
|
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
|
||||||
if (contacts.length == 1) {
|
if (contacts.length == 1) {
|
||||||
Contact newData = contacts[0];
|
Contact newData = contacts[0];
|
||||||
|
|
||||||
// delete local contact, if it exists
|
// delete local contact, if it exists
|
||||||
LocalContact localContact = localContacts.get(fileName);
|
LocalContact localContact = (LocalContact)localResources.get(fileName);
|
||||||
if (localContact != null) {
|
if (localContact != null) {
|
||||||
Constants.log.info("Updating " + fileName + " in local address book");
|
Constants.log.info("Updating " + fileName + " in local address book");
|
||||||
localContact.eTag = eTag;
|
localContact.eTag = eTag;
|
||||||
|
@ -338,7 +187,7 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
syncResult.stats.numUpdates++;
|
syncResult.stats.numUpdates++;
|
||||||
} else {
|
} else {
|
||||||
Constants.log.info("Adding " + fileName + " to local address book");
|
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();
|
localContact.add();
|
||||||
syncResult.stats.numInserts++;
|
syncResult.stats.numInserts++;
|
||||||
}
|
}
|
||||||
|
@ -347,8 +196,10 @@ public class ContactsSyncManager extends SyncManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// downloader helper class
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
static class ResourceDownloader implements Contact.Downloader {
|
private static class ResourceDownloader implements Contact.Downloader {
|
||||||
final HttpClient httpClient;
|
final HttpClient httpClient;
|
||||||
final HttpUrl baseUrl;
|
final HttpUrl baseUrl;
|
||||||
|
|
||||||
|
|
|
@ -12,20 +12,37 @@ import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.HttpUrl;
|
||||||
|
import com.squareup.okhttp.RequestBody;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.DavException;
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
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.Constants;
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.R;
|
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.davdroid.ui.DebugInfoActivity;
|
||||||
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
|
|
||||||
abstract public class SyncManager {
|
abstract public class SyncManager {
|
||||||
|
@ -38,21 +55,40 @@ abstract public class SyncManager {
|
||||||
SYNC_PHASE_CHECK_SYNC_STATE = 5,
|
SYNC_PHASE_CHECK_SYNC_STATE = 5,
|
||||||
SYNC_PHASE_LIST_LOCAL = 6,
|
SYNC_PHASE_LIST_LOCAL = 6,
|
||||||
SYNC_PHASE_LIST_REMOTE = 7,
|
SYNC_PHASE_LIST_REMOTE = 7,
|
||||||
SYNC_PHASE_COMPARE_ENTRIES = 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_SAVE_SYNC_STATE = 10;
|
||||||
|
|
||||||
final NotificationManager notificationManager;
|
protected final NotificationManager notificationManager;
|
||||||
final int notificationId;
|
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) {
|
public SyncManager(int notificationId, Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult syncResult) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -73,35 +109,46 @@ abstract public class SyncManager {
|
||||||
public void performSync() {
|
public void performSync() {
|
||||||
int syncPhase = SYNC_PHASE_PREPARE;
|
int syncPhase = SYNC_PHASE_PREPARE;
|
||||||
try {
|
try {
|
||||||
|
Constants.log.info("Preparing synchronization");
|
||||||
prepare();
|
prepare();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
|
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
|
||||||
|
Constants.log.info("Querying capabilities");
|
||||||
queryCapabilities();
|
queryCapabilities();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
|
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
|
||||||
|
Constants.log.info("Processing locally deleted entries");
|
||||||
processLocallyDeleted();
|
processLocallyDeleted();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED;
|
syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED;
|
||||||
|
Constants.log.info("Processing locally created entries");
|
||||||
processLocallyCreated();
|
processLocallyCreated();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
|
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
|
||||||
|
Constants.log.info("Uploading dirty entries");
|
||||||
uploadDirty();
|
uploadDirty();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE;
|
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE;
|
||||||
|
Constants.log.info("Checking sync state");
|
||||||
if (checkSyncState()) {
|
if (checkSyncState()) {
|
||||||
syncPhase = SYNC_PHASE_LIST_LOCAL;
|
syncPhase = SYNC_PHASE_LIST_LOCAL;
|
||||||
|
Constants.log.info("Listing local entries");
|
||||||
listLocal();
|
listLocal();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_LIST_REMOTE;
|
syncPhase = SYNC_PHASE_LIST_REMOTE;
|
||||||
|
Constants.log.info("Listing remote entries");
|
||||||
listRemote();
|
listRemote();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_COMPARE_ENTRIES;
|
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE;
|
||||||
compareEntries();
|
Constants.log.info("Comparing local/remote entries");
|
||||||
|
compareLocalRemote();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
|
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
|
||||||
|
Constants.log.info("Downloading remote entries");
|
||||||
downloadRemote();
|
downloadRemote();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
|
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
|
||||||
|
Constants.log.info("Saving sync state");
|
||||||
saveSyncState();
|
saveSyncState();
|
||||||
} else
|
} else
|
||||||
Constants.log.info("Remote collection didn't change, skipping remote sync");
|
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);
|
Constants.log.error("I/O exception during sync, trying again later", e);
|
||||||
syncResult.stats.numIoExceptions++;
|
syncResult.stats.numIoExceptions++;
|
||||||
|
|
||||||
} catch(HttpException e) {
|
} catch(HttpException|DavException e) {
|
||||||
Constants.log.error("HTTP Exception during sync", e);
|
Constants.log.error("HTTP/DAV Exception during sync", e);
|
||||||
syncResult.stats.numParseExceptions++;
|
syncResult.stats.numParseExceptions++;
|
||||||
|
|
||||||
Intent detailsIntent = new Intent(context, DebugInfoActivity.class);
|
Intent detailsIntent = new Intent(context, DebugInfoActivity.class);
|
||||||
|
@ -138,40 +185,188 @@ abstract public class SyncManager {
|
||||||
}
|
}
|
||||||
notificationManager.notify(account.name, notificationId, notification);
|
notificationManager.notify(account.name, notificationId, notification);
|
||||||
|
|
||||||
} catch(DavException e) {
|
} catch(CalendarStorageException|ContactsStorageException e) {
|
||||||
// TODO
|
Constants.log.error("Couldn't access local storage", e);
|
||||||
} catch(ContactsStorageException e) {
|
|
||||||
syncResult.databaseError = true;
|
syncResult.databaseError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
abstract protected void prepare();
|
abstract protected void prepare();
|
||||||
|
|
||||||
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, ContactsStorageException;
|
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, CalendarStorageException, ContactsStorageException;
|
||||||
|
|
||||||
abstract protected void processLocallyDeleted() throws IOException, HttpException, DavException, 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract protected void processLocallyCreated() throws IOException, HttpException, DavException, ContactsStorageException;
|
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 void uploadDirty() throws IOException, HttpException, DavException, ContactsStorageException;
|
abstract protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException, ContactsStorageException;
|
||||||
|
|
||||||
|
protected void uploadDirty() throws IOException, HttpException, CalendarStorageException, ContactsStorageException {
|
||||||
|
// upload dirty contacts
|
||||||
|
for (LocalResource local : localCollection.getDirty()) {
|
||||||
|
final String fileName = local.getFileName();
|
||||||
|
|
||||||
|
DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
|
||||||
|
|
||||||
|
// 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.
|
* 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
|
* @return <ul>
|
||||||
* false if the remote collection hasn't changed
|
* <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;
|
||||||
|
|
||||||
abstract protected void listLocal() throws IOException, HttpException, DavException, ContactsStorageException;
|
String localCTag = null;
|
||||||
|
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
|
||||||
|
Constants.log.info("Manual sync, ignoring CTag");
|
||||||
|
else
|
||||||
|
localCTag = localCollection.getCTag();
|
||||||
|
|
||||||
abstract protected void listRemote() throws IOException, HttpException, DavException, ContactsStorageException;
|
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 compareEntries() 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 downloadRemote() 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 saveSyncState() 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.ContentResolver;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.provider.CalendarContract;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
@ -28,13 +29,16 @@ import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||||
|
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||||
import at.bitfire.davdroid.resource.ServerInfo;
|
import at.bitfire.davdroid.resource.ServerInfo;
|
||||||
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
import at.bitfire.davdroid.syncadapter.AccountSettings;
|
||||||
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Cleanup;
|
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
|
@Override
|
||||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo calendar) throws LocalStorageException {
|
public void createLocalCollection(Account account, ServerInfo.ResourceInfo calendar) {
|
||||||
LocalCalendar.create(account, getActivity().getContentResolver(), 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
|
@Override
|
||||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo todoList) throws LocalStorageException {
|
public void createLocalCollection(Account account, ServerInfo.ResourceInfo todoList) throws LocalStorageException {
|
||||||
LocalTaskList.create(account, getActivity().getContentResolver(), todoList);
|
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
|
1
ical4android
Submodule
1
ical4android
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4e1131ae4607b4220e2d37632fd54a987b633849
|
|
@ -8,5 +8,7 @@
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':dav4android'
|
include ':dav4android'
|
||||||
|
include ':ical4android'
|
||||||
include ':vcard4android'
|
include ':vcard4android'
|
||||||
|
|
||||||
include ':MemorizingTrustManager'
|
include ':MemorizingTrustManager'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 384de9ec6eab1ac36d875330599b2858ce6ba888
|
Subproject commit 53c1695db02cc371369e05cb02a0f1e537ac9eec
|
Loading…
Reference in a new issue