Refactoring

* add overflow marker "…" to LoggingInputStream
* split the populate() methods
* always close cursors (resolves #120)
* introduce LocalStorageException and its subclass RecordNotFoundException and catch RemoteException earlier
* split synchronize() method into phases
* commit after every added/modified record (obsoletes MAX_UPDATES_BEFORE_COMMIT)
* read contact photos from (high-res) asset instead of the thumbnail blob (resolves #121)
pull/2/head
rfc2822 11 years ago
parent 6cfaad35b1
commit 8a651f135b

@ -14,11 +14,14 @@ public class LoggingInputStream extends FilterInputStream {
ByteArrayOutputStream log = new ByteArrayOutputStream(MAX_LENGTH);
int logSize = 0;
boolean overflow = false;
public LoggingInputStream(String tag, InputStream proxy) {
super(proxy);
this.tag = tag;
}
@Override
public boolean markSupported() {
@ -31,7 +34,8 @@ public class LoggingInputStream extends FilterInputStream {
if (logSize < MAX_LENGTH) {
log.write(b);
logSize++;
}
} else
overflow = true;
return b;
}
@ -40,8 +44,10 @@ public class LoggingInputStream extends FilterInputStream {
throws IOException {
int read = super.read(buffer, byteOffset, byteCount);
int bytesToLog = read;
if (bytesToLog + logSize > MAX_LENGTH)
if (bytesToLog + logSize > MAX_LENGTH) {
bytesToLog = MAX_LENGTH - logSize;
overflow = true;
}
if (bytesToLog > 0) {
log.write(buffer, byteOffset, bytesToLog);
logSize += bytesToLog;
@ -51,7 +57,7 @@ public class LoggingInputStream extends FilterInputStream {
@Override
public void close() throws IOException {
Log.d(tag, "Content: " + log.toString());
Log.d(tag, "Content: " + log.toString() + (overflow ? "…" : ""));
super.close();
}

@ -7,6 +7,8 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -15,6 +17,9 @@ import java.util.List;
import java.util.Locale;
import java.util.Set;
import lombok.Cleanup;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
@ -24,6 +29,7 @@ import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentUris;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
@ -105,30 +111,70 @@ public class LocalAddressBook extends LocalCollection<Contact> {
public void setCTag(String cTag) {
accountManager.setUserData(account, Constants.ACCOUNT_KEY_ADDRESSBOOK_CTAG, cTag);
}
/* create/update/delete */
public Contact newResource(long localID, String resourceName, String eTag) {
return new Contact(localID, resourceName, eTag);
}
/* content provider (= database) querying */
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
String where;
if (remoteResources.length != 0) {
List<String> sqlFileNames = new LinkedList<String>();
for (Resource res : remoteResources)
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")";
} else
where = entryColumnRemoteName() + " IS NOT NULL";
Builder builder = ContentProviderOperation.newDelete(entriesURI()).withSelection(where, null);
pendingOperations.add(builder
.withYieldAllowed(true)
.build());
}
/* methods for populating the data object from the content provider */
@Override
public void populate(Resource res) throws RemoteException {
public void populate(Resource res) throws LocalStorageException {
Contact c = (Contact)res;
if (c.isPopulated())
return;
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()),
new String[] { entryColumnUID(), RawContacts.STARRED }, null, null, null);
if (cursor != null && cursor.moveToNext()) {
c.setUid(cursor.getString(0));
c.setStarred(cursor.getInt(1) != 0);
}
try {
@Cleanup("close") Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()),
new String[] { entryColumnUID(), RawContacts.STARRED }, null, null, null);
if (cursor != null && cursor.moveToNext()) {
c.setUid(cursor.getString(0));
c.setStarred(cursor.getInt(1) != 0);
} else
throw new RecordNotFoundException();
// structured name
cursor = providerClient.query(dataURI(), new String[] {
populateStructuredName(c);
populatePhoneNumbers(c);
populateEmailAddresses(c);
populatePhoto(c);
populateOrganization(c);
populateIMPPs(c);
populateNickname(c);
populateNote(c);
populatePostalAddresses(c);
populateURLs(c);
populateEvents(c);
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
private void populateStructuredName(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] {
/* 0 */ StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME,
/* 3 */ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX,
/* 6 */ StructuredName.PHONETIC_GIVEN_NAME, StructuredName.PHONETIC_MIDDLE_NAME, StructuredName.PHONETIC_FAMILY_NAME
}, StructuredName.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(res.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null);
new String[] { String.valueOf(c.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null);
if (cursor != null && cursor.moveToNext()) {
c.setDisplayName(cursor.getString(0));
@ -142,9 +188,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
c.setPhoneticMiddleName(cursor.getString(7));
c.setPhoneticFamilyName(cursor.getString(8));
}
// phone numbers
cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER },
}
protected void populatePhoneNumbers(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER },
Phone.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Phone.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
@ -218,9 +265,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
}
c.getPhoneNumbers().add(number);
}
// email addresses
cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL },
}
protected void populateEmailAddresses(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL },
Email.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Email.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
@ -239,20 +287,26 @@ public class LocalAddressBook extends LocalCollection<Contact> {
String customType = cursor.getString(2);
if (customType != null && !customType.isEmpty())
email.addType(EmailType.get(labelToXName(customType)));
break;
}
c.getEmails().add(email);
}
// photo
cursor = providerClient.query(dataURI(), new String[] { Photo.PHOTO },
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Photo.CONTENT_ITEM_TYPE }, null);
if (cursor != null && cursor.moveToNext())
c.setPhoto(cursor.getBlob(0));
// organization
cursor = providerClient.query(dataURI(), new String[] { Organization.COMPANY, Organization.TITLE },
}
protected void populatePhoto(Contact c) throws RemoteException {
Uri photoUri = Uri.withAppendedPath(
ContentUris.withAppendedId(RawContacts.CONTENT_URI, c.getLocalID()),
RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
try {
@Cleanup AssetFileDescriptor fd = providerClient.openAssetFile(photoUri, "r");
@Cleanup InputStream is = fd.createInputStream();
c.setPhoto(IOUtils.toByteArray(is));
} catch(IOException ex) {
Log.w(TAG, "Couldn't read contact photo", ex);
}
}
protected void populateOrganization(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Organization.COMPANY, Organization.TITLE },
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Organization.CONTENT_ITEM_TYPE }, null);
if (cursor != null && cursor.moveToNext()) {
@ -263,9 +317,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
if (role != null && !role.isEmpty())
c.setRole(role);
}
// IMPPs
cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL },
}
protected void populateIMPPs(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL },
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Im.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
@ -302,7 +357,6 @@ public class LocalAddressBook extends LocalCollection<Contact> {
break;
case Im.PROTOCOL_CUSTOM:
impp = new Impp(cursor.getString(4), handle);
break;
}
if (impp != null) {
@ -317,28 +371,30 @@ public class LocalAddressBook extends LocalCollection<Contact> {
String customType = cursor.getString(2);
if (customType != null && !customType.isEmpty())
impp.addType(ImppType.get(labelToXName(customType)));
break;
}
c.getImpps().add(impp);
}
}
// nick name (max. 1)
cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME },
}
protected void populateNickname(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME },
Nickname.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Nickname.CONTENT_ITEM_TYPE }, null);
if (cursor != null && cursor.moveToNext())
c.setNickName(cursor.getString(0));
// note (max. 1)
cursor = providerClient.query(dataURI(), new String[] { Note.NOTE },
}
protected void populateNote(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Note.NOTE },
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Note.CONTENT_ITEM_TYPE }, null);
if (cursor != null && cursor.moveToNext())
c.setNote(cursor.getString(0));
// postal addresses
cursor = providerClient.query(dataURI(), new String[] {
}
protected void populatePostalAddresses(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] {
/* 0 */ StructuredPostal.FORMATTED_ADDRESS, StructuredPostal.TYPE, StructuredPostal.LABEL,
/* 3 */ StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
/* 6 */ StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
@ -347,7 +403,7 @@ public class LocalAddressBook extends LocalCollection<Contact> {
new String[] { String.valueOf(c.getLocalID()), StructuredPostal.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
Address address = new Address();
address.setLabel(cursor.getString(0));
switch (cursor.getInt(1)) {
case StructuredPostal.TYPE_HOME:
@ -371,16 +427,18 @@ public class LocalAddressBook extends LocalCollection<Contact> {
address.setCountry(cursor.getString(9));
c.getAddresses().add(address);
}
// URL
cursor = providerClient.query(dataURI(), new String[] { Website.URL },
}
protected void populateURLs(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Website.URL },
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Website.CONTENT_ITEM_TYPE }, null);
if (cursor != null && cursor.moveToNext())
c.getURLs().add(cursor.getString(0));
// events
cursor = providerClient.query(dataURI(), new String[] { CommonDataKinds.Event.TYPE, CommonDataKinds.Event.START_DATE },
}
protected void populateEvents(Contact c) throws RemoteException {
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { CommonDataKinds.Event.TYPE, CommonDataKinds.Event.START_DATE },
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), CommonDataKinds.Event.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
@ -399,66 +457,9 @@ public class LocalAddressBook extends LocalCollection<Contact> {
Log.w(TAG, "Couldn't parse local birthday/anniversary date", e);
}
}
c.populated = true;
return;
}
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
String where;
if (remoteResources.length != 0) {
List<String> sqlFileNames = new LinkedList<String>();
for (Resource res : remoteResources)
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")";
} else
where = entryColumnRemoteName() + " IS NOT NULL";
Builder builder = ContentProviderOperation.newDelete(entriesURI()).withSelection(where, null);
pendingOperations.add(builder
.withYieldAllowed(true)
.build());
}
/* create/update/delete */
public Contact newResource(long localID, String resourceName, String eTag) {
return new Contact(localID, resourceName, eTag);
}
/* private helper methods */
protected Uri dataURI() {
return syncAdapterURI(Data.CONTENT_URI);
}
protected String labelToXName(String label) {
if (label == null)
return null;
String xName = "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US);
return xName;
}
private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) {
return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx);
}
protected String xNameToLabel(String xname) {
if (xname == null)
return null;
// "x-my_property"
// 1. ensure lower case -> "x-my_property"
// 2. remove x- from beginning -> "my_property"
// 3. replace "_" by " " -> "my property"
// 4. capitalize -> "My Property"
return WordUtils.capitalize(StringUtils.removeStart(xname.toLowerCase(Locale.US), "x-").replaceAll("_"," "));
}
/* content builder methods */
@Override
@ -788,4 +789,32 @@ public class LocalAddressBook extends LocalCollection<Contact> {
.withValue(CommonDataKinds.Event.TYPE, type)
.withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate()));
}
/* helper methods */
protected Uri dataURI() {
return syncAdapterURI(Data.CONTENT_URI);
}
protected String labelToXName(String label) {
return "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US);
}
private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) {
return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx);
}
protected String xNameToLabel(String xname) {
if (xname == null)
return null;
// "x-my_property"
// 1. ensure lower case -> "x-my_property"
// 2. remove x- from beginning -> "my_property"
// 3. replace "_" by " " -> "my property"
// 4. capitalize -> "My Property"
return WordUtils.capitalize(StringUtils.removeStart(xname.toLowerCase(Locale.US), "x-").replaceAll("_"," "));
}
}

@ -16,6 +16,7 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Cleanup;
import lombok.Getter;
import net.fortuna.ical4j.model.Dur;
import net.fortuna.ical4j.model.Parameter;
@ -137,7 +138,7 @@ public class LocalCalendar extends LocalCollection<Event> {
}
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
Cursor cursor = providerClient.query(calendarsURI(account),
@Cleanup("close") Cursor cursor = providerClient.query(calendarsURI(account),
new String[] { Calendars._ID, Calendars.NAME, COLLECTION_COLUMN_CTAG },
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
@ -164,200 +165,12 @@ public class LocalCalendar extends LocalCollection<Event> {
.build());
}
/* content provider (= database) querying */
@Override
public void populate(Resource resource) throws RemoteException {
Event e = (Event)resource;
if (e.isPopulated())
return;
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()),
new String[] {
/* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION,
/* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY,
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY
}, null, null, null);
if (cursor != null && cursor.moveToNext()) {
e.setUid(cursor.getString(17));
e.setSummary(cursor.getString(0));
e.setLocation(cursor.getString(1));
e.setDescription(cursor.getString(2));
long tsStart = cursor.getLong(3),
tsEnd = cursor.getLong(4);
String tzId;
if (cursor.getInt(7) != 0) { // ALL_DAY != 0
tzId = null; // -> use UTC
} else {
// use the start time zone for the end time, too
// because the Samsung Planner UI allows the user to change the time zone
// but it will change the start time zone only
tzId = cursor.getString(5);
//tzIdEnd = cursor.getString(6);
}
e.setDtStart(tsStart, tzId);
if (tsEnd != 0)
e.setDtEnd(tsEnd, tzId);
// recurrence
try {
String duration = cursor.getString(18);
if (duration != null && !duration.isEmpty())
e.setDuration(new Duration(new Dur(duration)));
String strRRule = cursor.getString(10);
if (strRRule != null && !strRRule.isEmpty())
e.setRrule(new RRule(strRRule));
String strRDate = cursor.getString(11);
if (strRDate != null && !strRDate.isEmpty()) {
RDate rDate = new RDate();
rDate.setValue(strRDate);
e.setRdate(rDate);
}
String strExRule = cursor.getString(12);
if (strExRule != null && !strExRule.isEmpty()) {
ExRule exRule = new ExRule();
exRule.setValue(strExRule);
e.setExrule(exRule);
}
String strExDate = cursor.getString(13);
if (strExDate != null && !strExDate.isEmpty()) {
// ignored, see https://code.google.com/p/android/issues/detail?id=21426
ExDate exDate = new ExDate();
exDate.setValue(strExDate);
e.setExdate(exDate);
}
} catch (ParseException ex) {
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
}
// status
switch (cursor.getInt(8)) {
case Events.STATUS_CONFIRMED:
e.setStatus(Status.VEVENT_CONFIRMED);
break;
case Events.STATUS_TENTATIVE:
e.setStatus(Status.VEVENT_TENTATIVE);
break;
case Events.STATUS_CANCELED:
e.setStatus(Status.VEVENT_CANCELLED);
}
// availability
e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE);
// attendees
if (cursor.getInt(14) != 0) { // has attendees
try {
e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null)));
} catch (URISyntaxException ex) {
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
}
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
Cursor c = providerClient.query(attendeesUri, new String[] {
/* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE,
/* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS
}, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
try {
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
ParameterList params = attendee.getParameters();
String cn = c.getString(1);
if (cn != null)
params.add(new Cn(cn));
// type
int type = c.getInt(2);
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
// role
int relationship = c.getInt(3);
switch (relationship) {
case Attendees.RELATIONSHIP_ORGANIZER:
params.add(Role.CHAIR);
break;
case Attendees.RELATIONSHIP_ATTENDEE:
case Attendees.RELATIONSHIP_PERFORMER:
case Attendees.RELATIONSHIP_SPEAKER:
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
break;
case Attendees.RELATIONSHIP_NONE:
params.add(Role.NON_PARTICIPANT);
}
// status
switch (c.getInt(4)) {
case Attendees.ATTENDEE_STATUS_INVITED:
params.add(PartStat.NEEDS_ACTION);
break;
case Attendees.ATTENDEE_STATUS_ACCEPTED:
params.add(PartStat.ACCEPTED);
break;
case Attendees.ATTENDEE_STATUS_DECLINED:
params.add(PartStat.DECLINED);
break;
case Attendees.ATTENDEE_STATUS_TENTATIVE:
params.add(PartStat.TENTATIVE);
break;
}
e.addAttendee(attendee);
} catch (URISyntaxException ex) {
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
}
}
}
// classification
switch (cursor.getInt(9)) {
case Events.ACCESS_CONFIDENTIAL:
case Events.ACCESS_PRIVATE:
e.setForPublic(false);
break;
case Events.ACCESS_PUBLIC:
e.setForPublic(true);
}
// reminders
Uri remindersUri = Reminders.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
Cursor c = providerClient.query(remindersUri, new String[] {
/* 0 */ Reminders.MINUTES, Reminders.METHOD
}, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0));
PropertyList props = alarm.getProperties();
switch (c.getInt(1)) {
/*case Reminders.METHOD_EMAIL:
props.add(Action.EMAIL);
break;*/
default:
props.add(Action.DISPLAY);
props.add(new Description(e.getSummary()));
}
e.addAlarm(alarm);
}
}
/* create/update/delete */
public Event newResource(long localID, String resourceName, String eTag) {
return new Event(localID, resourceName, eTag);
}
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
String where;
@ -376,27 +189,212 @@ public class LocalCalendar extends LocalCollection<Event> {
.withYieldAllowed(true)
.build());
}
/* create/update/delete */
public Event newResource(long localID, String resourceName, String eTag) {
return new Event(localID, resourceName, eTag);
}
/* methods for populating the data object from the content provider */
@Override
public void populate(Resource resource) throws LocalStorageException {
Event e = (Event)resource;
try {
@Cleanup("close") Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()),
new String[] {
/* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION,
/* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY,
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY
}, null, null, null);
if (cursor != null && cursor.moveToNext()) {
e.setUid(cursor.getString(17));
e.setSummary(cursor.getString(0));
e.setLocation(cursor.getString(1));
e.setDescription(cursor.getString(2));
long tsStart = cursor.getLong(3),
tsEnd = cursor.getLong(4);
String tzId;
if (cursor.getInt(7) != 0) { // ALL_DAY != 0
tzId = null; // -> use UTC
} else {
// use the start time zone for the end time, too
// because the Samsung Planner UI allows the user to change the time zone
// but it will change the start time zone only
tzId = cursor.getString(5);
//tzIdEnd = cursor.getString(6);
}
e.setDtStart(tsStart, tzId);
if (tsEnd != 0)
e.setDtEnd(tsEnd, tzId);
/* private helper methods */
// recurrence
try {
String duration = cursor.getString(18);
if (duration != null && !duration.isEmpty())
e.setDuration(new Duration(new Dur(duration)));
String strRRule = cursor.getString(10);
if (strRRule != null && !strRRule.isEmpty())
e.setRrule(new RRule(strRRule));
String strRDate = cursor.getString(11);
if (strRDate != null && !strRDate.isEmpty()) {
RDate rDate = new RDate();
rDate.setValue(strRDate);
e.setRdate(rDate);
}
String strExRule = cursor.getString(12);
if (strExRule != null && !strExRule.isEmpty()) {
ExRule exRule = new ExRule();
exRule.setValue(strExRule);
e.setExrule(exRule);
}
String strExDate = cursor.getString(13);
if (strExDate != null && !strExDate.isEmpty()) {
// ignored, see https://code.google.com/p/android/issues/detail?id=21426
ExDate exDate = new ExDate();
exDate.setValue(strExDate);
e.setExdate(exDate);
}
} catch (ParseException ex) {
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
}
protected static Uri calendarsURI(Account account) {
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
// status
switch (cursor.getInt(8)) {
case Events.STATUS_CONFIRMED:
e.setStatus(Status.VEVENT_CONFIRMED);
break;
case Events.STATUS_TENTATIVE:
e.setStatus(Status.VEVENT_TENTATIVE);
break;
case Events.STATUS_CANCELED:
e.setStatus(Status.VEVENT_CANCELLED);
}
// availability
e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE);
// attendees
if (cursor.getInt(14) != 0) { // has attendees
try {
e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null)));
} catch (URISyntaxException ex) {
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
}
populateAttendees(e);
}
// classification
switch (cursor.getInt(9)) {
case Events.ACCESS_CONFIDENTIAL:
case Events.ACCESS_PRIVATE:
e.setForPublic(false);
break;
case Events.ACCESS_PUBLIC:
e.setForPublic(true);
}
populateReminders(e);
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
protected Uri calendarsURI() {
return calendarsURI(account);
void populateAttendees(Event e) throws RemoteException {
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@Cleanup("close") Cursor c = providerClient.query(attendeesUri, new String[] {
/* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE,
/* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS
}, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
try {
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
ParameterList params = attendee.getParameters();
String cn = c.getString(1);
if (cn != null)
params.add(new Cn(cn));
// type
int type = c.getInt(2);
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
// role
int relationship = c.getInt(3);
switch (relationship) {
case Attendees.RELATIONSHIP_ORGANIZER:
params.add(Role.CHAIR);
break;
case Attendees.RELATIONSHIP_ATTENDEE:
case Attendees.RELATIONSHIP_PERFORMER:
case Attendees.RELATIONSHIP_SPEAKER:
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
break;
case Attendees.RELATIONSHIP_NONE:
params.add(Role.NON_PARTICIPANT);
}
// status
switch (c.getInt(4)) {
case Attendees.ATTENDEE_STATUS_INVITED:
params.add(PartStat.NEEDS_ACTION);
break;
case Attendees.ATTENDEE_STATUS_ACCEPTED:
params.add(PartStat.ACCEPTED);
break;
case Attendees.ATTENDEE_STATUS_DECLINED:
params.add(PartStat.DECLINED);
break;
case Attendees.ATTENDEE_STATUS_TENTATIVE:
params.add(PartStat.TENTATIVE);
break;
}
e.addAttendee(attendee);
} catch (URISyntaxException ex) {
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
}
}
}
void populateReminders(Event e) throws RemoteException {
// reminders
Uri remindersUri = Reminders.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@Cleanup("close") Cursor c = providerClient.query(remindersUri, new String[] {
/* 0 */ Reminders.MINUTES, Reminders.METHOD
}, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0));
PropertyList props = alarm.getProperties();
switch (c.getInt(1)) {
/*case Reminders.METHOD_EMAIL:
props.add(Action.EMAIL);
break;*/
default:
props.add(Action.DISPLAY);
props.add(new Description(e.getSummary()));
}
e.addAlarm(alarm);
}
}
/* content builder methods */
@ -570,4 +568,19 @@ public class LocalCalendar extends LocalCollection<Event> {
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
.withValue(Reminders.MINUTES, minutes);
}
/* private helper methods */
protected static Uri calendarsURI(Account account) {
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
}
protected Uri calendarsURI() {
return calendarsURI(account);
}
}

@ -10,6 +10,7 @@ package at.bitfire.davdroid.resource;
import java.util.ArrayList;
import java.util.LinkedList;
import lombok.Cleanup;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
@ -63,91 +64,111 @@ public abstract class LocalCollection<T extends Resource> {
// content provider (= database) querying
public Resource[] findDirty() throws RemoteException {
public Resource[] findDirty() throws LocalStorageException {
String where = entryColumnDirty() + "=1";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null);
LinkedList<T> dirty = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
T resource = findById(cursor.getLong(0), true);
if (resource != null)
dirty.add(resource);
try {
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null);
LinkedList<T> dirty = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
T resource = findById(cursor.getLong(0), true);
if (resource != null)
dirty.add(resource);
}
return dirty.toArray(new Resource[0]);
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
return dirty.toArray(new Resource[0]);
}
public Resource[] findDeleted() throws RemoteException {
public Resource[] findDeleted() throws LocalStorageException {
String where = entryColumnDeleted() + "=1";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null);
LinkedList<T> deleted = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
T resource = findById(cursor.getLong(0), false);
if (resource != null)
deleted.add(resource);
try {
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null);
LinkedList<T> deleted = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
T resource = findById(cursor.getLong(0), false);
if (resource != null)
deleted.add(resource);
}
return deleted.toArray(new Resource[0]);
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
return deleted.toArray(new Resource[0]);
}
public Resource[] findNew() throws RemoteException {
public Resource[] findNew() throws LocalStorageException {
String where = entryColumnDirty() + "=1 AND " + entryColumnRemoteName() + " IS NULL";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() },
where, null, null);
LinkedList<T> fresh = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
T resource = findById(cursor.getLong(0), true);
if (resource != null) {
resource.initialize();
// new record: set generated UID + remote file name in database
pendingOperations.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
.withValue(entryColumnUID(), resource.getUid())
.withValue(entryColumnRemoteName(), resource.getName())
.build());
fresh.add(resource);
try {
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() },
where, null, null);
LinkedList<T> fresh = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
T resource = findById(cursor.getLong(0), true);
if (resource != null) {
resource.initialize();
// new record: set generated UID + remote file name in database
pendingOperations.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
.withValue(entryColumnUID(), resource.getUid())
.withValue(entryColumnRemoteName(), resource.getName())
.build());
fresh.add(resource);
}
}
return fresh.toArray(new Resource[0]);
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
return fresh.toArray(new Resource[0]);
}
public T findById(long localID, boolean populate) throws RemoteException {
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
if (populate)
populate(resource);
return resource;
} else
return null;
public T findById(long localID, boolean populate) throws LocalStorageException {
try {
@Cleanup("close") Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
if (populate)
populate(resource);
return resource;
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
public T findByRemoteName(String remoteName, boolean populate) throws RemoteException {
Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
if (populate)
populate(resource);
return resource;
} else
return null;
public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException {
try {
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
if (populate)
populate(resource);
return resource;
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
public abstract void populate(Resource record) throws RemoteException;
public abstract void populate(Resource record) throws LocalStorageException;
protected void queueOperation(Builder builder) {
if (builder != null)
@ -169,18 +190,16 @@ public abstract class LocalCollection<T extends Resource> {
addDataRows(resource, -1, idx);
}
public void updateByRemoteName(Resource remoteResource) throws RemoteException {
public void updateByRemoteName(Resource remoteResource) throws LocalStorageException {
T localResource = findByRemoteName(remoteResource.getName(), false);
if (localResource != null) {
pendingOperations.add(
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource)
.withValue(entryColumnETag(), remoteResource.getETag())
.withYieldAllowed(true)
.build());
removeDataRows(localResource);
addDataRows(remoteResource, localResource.getLocalID(), -1);
}
pendingOperations.add(
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource)
.withValue(entryColumnETag(), remoteResource.getETag())
.withYieldAllowed(true)
.build());
removeDataRows(localResource);
addDataRows(remoteResource, localResource.getLocalID(), -1);
}
public void delete(Resource resource) {
@ -198,12 +217,17 @@ public abstract class LocalCollection<T extends Resource> {
.withValue(entryColumnDirty(), 0).build());
}
public void commit() throws RemoteException, OperationApplicationException {
if (!pendingOperations.isEmpty()) {
Log.i(TAG, "Committing " + pendingOperations.size() + " operations");
providerClient.applyBatch(pendingOperations);
pendingOperations.clear();
}
public void commit() throws LocalStorageException {
if (!pendingOperations.isEmpty())
try {
Log.i(TAG, "Committing " + pendingOperations.size() + " operations");
providerClient.applyBatch(pendingOperations);
pendingOperations.clear();
} catch (RemoteException ex) {
throw new LocalStorageException(ex);
} catch(OperationApplicationException ex) {
throw new LocalStorageException(ex);
}
}

@ -0,0 +1,24 @@
package at.bitfire.davdroid.resource;
public class LocalStorageException extends Exception {
private static final long serialVersionUID = -7787658815291629529L;
private static final String detailMessage = "Couldn't access local content provider";
public LocalStorageException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
public LocalStorageException(String detailMessage) {
super(detailMessage);
}
public LocalStorageException(Throwable throwable) {
super(detailMessage, throwable);
}
public LocalStorageException() {
super(detailMessage);
}
}

@ -0,0 +1,17 @@
package at.bitfire.davdroid.resource;
public class RecordNotFoundException extends LocalStorageException {
private static final long serialVersionUID = 4961024282198632578L;
private static final String detailMessage = "Record not found in local content provider";
RecordNotFoundException(Throwable ex) {
super(detailMessage, ex);
}
RecordNotFoundException() {
super(detailMessage);
}
}

@ -23,8 +23,6 @@ public abstract class Resource {
@Getter @Setter protected String uid;
@Getter protected long localID;
@Getter protected boolean populated = false;
public Resource(String name, String ETag) {
this.name = name;

@ -15,13 +15,12 @@ import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalStorageException;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.webdav.DAVException;
@ -71,19 +70,16 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
} catch (AuthenticationException ex) {
syncResult.stats.numAuthExceptions++;
Log.e(TAG, "HTTP authorization error", ex);
Log.e(TAG, "HTTP authentication failed", ex);
} catch (DAVException ex) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "Invalid DAV response", ex);
} catch (HttpException ex) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, "HTTP error", ex);
} catch (OperationApplicationException ex) {
} catch (LocalStorageException ex) {
syncResult.databaseError = true;
Log.e(TAG, "Content provider operation error", ex);
} catch (RemoteException ex) {
syncResult.databaseError = true;
Log.e(TAG, "Remote process (content provider?) died", ex);
Log.e(TAG, "Local storage (content provider) exception", ex);
} catch (IOException ex) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, "I/O error", ex);

@ -17,11 +17,11 @@ import org.apache.http.HttpException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.os.RemoteException;
import android.util.Log;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalStorageException;
import at.bitfire.davdroid.resource.RecordNotFoundException;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.resource.Resource;
import at.bitfire.davdroid.webdav.NotFoundException;
@ -29,7 +29,6 @@ import at.bitfire.davdroid.webdav.PreconditionFailedException;
public class SyncManager {
private static final String TAG = "davdroid.SyncManager";
private static final int MAX_UPDATES_BEFORE_COMMIT = 25;
protected Account account;
protected AccountManager accountManager;
@ -40,132 +39,149 @@ public class SyncManager {
this.accountManager = accountManager;
}
public void synchronize(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> dav, boolean manualSync, SyncResult syncResult) throws RemoteException, OperationApplicationException, IOException, HttpException {
boolean fetchCollection = false;
public void synchronize(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, boolean manualSync, SyncResult syncResult) throws LocalStorageException, IOException, HttpException {
// PHASE 1: push local changes to server
int deletedRemotely = pushDeleted(local, remote),
addedRemotely = pushNew(local, remote),
updatedRemotely = pushDirty(local, remote);
// PHASE 1: UPLOAD LOCALLY-CHANGED RESOURCES
// remove deleted resources from remote
syncResult.stats.numEntries = deletedRemotely + addedRemotely + updatedRemotely;
// PHASE 2A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed)
boolean fetchCollection = syncResult.stats.numEntries > 0;
if (manualSync) {
Log.i(TAG, "Synchronization forced");
fetchCollection = true;
}
if (!fetchCollection) {
String currentCTag = remote.getCTag(),
lastCTag = local.getCTag();
if (currentCTag == null || !currentCTag.equals(lastCTag))
fetchCollection = true;
}
// PHASE 2B: detect details of remote changes
Log.i(TAG, "Fetching remote resource list");
Set<Resource> remotelyAdded = new HashSet<Resource>(),
remotelyUpdated = new HashSet<Resource>();
Resource[] remoteResources = remote.getMemberETags();
if (remoteResources != null) {
for (Resource remoteResource : remoteResources) {
try {
Resource localResource = local.findByRemoteName(remoteResource.getName(), false);
if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
remotelyUpdated.add(remoteResource);
} catch(RecordNotFoundException e) {
remotelyAdded.add(remoteResource);
}
}
}
// PHASE 3: pull remote changes from server
syncResult.stats.numInserts = pullNew(local, remote, remotelyAdded);
syncResult.stats.numUpdates = pullChanged(local, remote, remotelyUpdated);
Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore");
local.deleteAllExceptRemoteNames(remoteResources);
local.commit();
// update collection CTag
Log.i(TAG, "Sync complete, fetching new CTag");
local.setCTag(remote.getCTag());
local.commit();
}
private int pushDeleted(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
int count = 0;
Resource[] deletedResources = local.findDeleted();
if (deletedResources != null) {
Log.i(TAG, "Remotely removing " + deletedResources.length + " deleted resource(s) (if not changed)");
for (Resource res : deletedResources) {
try {
if (res.getName() != null) // is this resource even present remotely?
dav.delete(res);
remote.delete(res);
} catch(NotFoundException e) {
Log.i(TAG, "Locally-deleted resource has already been removed from server");
} catch(PreconditionFailedException e) {
Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile");
}
fetchCollection = true;
local.delete(res);
count++;
}
local.commit();
}
// upload new resources
return count;
}
private int pushNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
int count = 0;
Resource[] newResources = local.findNew();
if (newResources != null) {
Log.i(TAG, "Uploading " + newResources.length + " new resource(s) (if not existing)");
for (Resource res : newResources) {
try {
dav.add(res);
remote.add(res);
} catch(PreconditionFailedException e) {
Log.i(TAG, "Didn't overwrite existing resource with other content");
} catch (ValidationException e) {
Log.e(TAG, "Couldn't create entity for adding: " + e.toString());
}
fetchCollection = true;
local.clearDirty(res);
count++;
}
local.commit();
}
// upload modified resources
return count;
}
private int pushDirty(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
int count = 0;
Resource[] dirtyResources = local.findDirty();
if (dirtyResources != null) {
Log.i(TAG, "Uploading " + dirtyResources.length + " modified resource(s) (if not changed)");
for (Resource res : dirtyResources) {
try {
dav.update(res);
remote.update(res);
} catch(PreconditionFailedException e) {
Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile");
} catch (ValidationException e) {
Log.e(TAG, "Couldn't create entity for updating: " + e.toString());
}
fetchCollection = true;
local.clearDirty(res);
count++;
}
local.commit();
}
// PHASE 2A: FETCH REMOTE COLLECTION STATUS
// has collection changed -> fetch resources?
if (manualSync) {
Log.i(TAG, "Synchronization forced");
fetchCollection = true;
}
if (!fetchCollection) {
String currentCTag = dav.getCTag(),
lastCTag = local.getCTag();
if (currentCTag == null || !currentCTag.equals(lastCTag))
fetchCollection = true;
}
if (!fetchCollection)
return;
// PHASE 2B: FETCH REMOTE COLLECTION SUMMARY
// fetch remote resources -> add/overwrite local resources
Log.i(TAG, "Fetching remote resource list");
Set<Resource> resourcesToAdd = new HashSet<Resource>(),
resourcesToUpdate = new HashSet<Resource>();
Resource[] remoteResources = dav.getMemberETags();
if (remoteResources == null) // failure
return;
for (Resource remoteResource : remoteResources) {
Resource localResource = local.findByRemoteName(remoteResource.getName(), true);
if (localResource == null)
resourcesToAdd.add(remoteResource);
else if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
resourcesToUpdate.add(remoteResource);
}
// PHASE 3: DOWNLOAD NEW/REMOTELY-CHANGED RESOURCES
Log.i(TAG, "Adding " + resourcesToAdd.size() + " remote resource(s)");
return count;
}
private int pullNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Set<Resource> resourcesToAdd) throws LocalStorageException, IOException, HttpException {
int count = 0;
Log.i(TAG, "Fetching " + resourcesToAdd.size() + " new remote resource(s)");
if (!resourcesToAdd.isEmpty()) {
for (Resource res : dav.multiGet(resourcesToAdd.toArray(new Resource[0]))) {
for (Resource res : remote.multiGet(resourcesToAdd.toArray(new Resource[0]))) {
Log.i(TAG, "Adding " + res.getName());
local.add(res);
if (++syncResult.stats.numInserts % MAX_UPDATES_BEFORE_COMMIT == 0) // avoid TransactionTooLargeException
local.commit();
local.commit();
count++;
}
local.commit();
}
Log.i(TAG, "Updating from " + resourcesToUpdate.size() + " remote resource(s)");
return count;
}
private int pullChanged(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Set<Resource> resourcesToUpdate) throws LocalStorageException, IOException, HttpException {
int count = 0;
Log.i(TAG, "Fetching " + resourcesToUpdate.size() + " updated remote resource(s)");
if (!resourcesToUpdate.isEmpty())
for (Resource res : dav.multiGet(resourcesToUpdate.toArray(new Resource[0]))) {
for (Resource res : remote.multiGet(resourcesToUpdate.toArray(new Resource[0]))) {
Log.i(TAG, "Updating " + res.getName());
local.updateByRemoteName(res);
if (++syncResult.stats.numUpdates % MAX_UPDATES_BEFORE_COMMIT == 0) // avoid TransactionTooLargeException
local.commit();
local.commit();
count++;
}
local.commit();
// delete remotely removed resources
Log.i(TAG, "Removing resources that are missing remotely");
local.deleteAllExceptRemoteNames(remoteResources);
local.commit();
// update collection CTag
local.setCTag(dav.getCTag());
local.commit();
return count;
}
}

Loading…
Cancel
Save