Commit 65c15258 authored by Ricki Hirner's avatar Ricki Hirner 🐑

Process recurring events, exceptions etc.

parent de7ca7b9
......@@ -17,7 +17,5 @@ public class Constants {
WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app",
WEB_URL_VIEW_LOGS = "https://github.com/bitfireAT/davdroid/wiki/How-to-view-the-logs";
//public static final ProdId ICAL_PRODID = new ProdId("-//bitfire web engineering//DAVdroid " + BuildConfig.VERSION_CODE + " (ical4j 2.0-beta1)//EN");
public static final Logger log = LoggerFactory.getLogger("davdroid");
}
......@@ -118,9 +118,6 @@ public class HttpClient extends OkHttpClient {
}
}
// don't follow redirects automatically because this may rewrite DAV methods to GET
setFollowRedirects(false);
// set timeouts
setConnectTimeout(30, TimeUnit.SECONDS);
setWriteTimeout(15, TimeUnit.SECONDS);
......
......@@ -10,10 +10,13 @@ package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
......@@ -22,8 +25,14 @@ import android.provider.CalendarContract.Reminders;
import com.google.common.base.Joiner;
import java.io.FileNotFoundException;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.davdroid.Constants;
import at.bitfire.ical4android.AndroidCalendar;
import at.bitfire.ical4android.AndroidCalendarFactory;
import at.bitfire.ical4android.BatchOperation;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
......@@ -34,9 +43,13 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
public static final String COLUMN_CTAG = Calendars.CAL_SYNC1;
protected static final int
DIRTY_INCREASE_SEQUENCE = 1,
DIRTY_DONT_INCREASE_SEQUENCE = 2;
static String[] BASE_INFO_COLUMNS = new String[] {
Events._ID,
LocalEvent.COLUMN_FILENAME,
Events._SYNC_ID,
LocalEvent.COLUMN_ETAG
};
......@@ -59,38 +72,61 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
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);
if (info.isReadOnly())
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ);
else {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
}
values.put(Calendars.OWNER_ACCOUNT, account.name);
values.put(Calendars.SYNC_EVENTS, 1);
values.put(Calendars.VISIBLE, 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));
if (Build.VERSION.SDK_INT >= 15) {
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);
return (LocalEvent[])queryEvents(Events.ORIGINAL_ID + " IS NULL", null);
}
@Override
public LocalEvent[] getDeleted() throws CalendarStorageException {
return (LocalEvent[])queryEvents(Events.DELETED + "!=0", null);
return (LocalEvent[])queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null);
}
@Override
public LocalEvent[] getWithoutFileName() throws CalendarStorageException {
return (LocalEvent[])queryEvents(LocalEvent.COLUMN_FILENAME + " IS NULL", null);
return (LocalEvent[])queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null);
}
@Override
public LocalResource[] getDirty() throws CalendarStorageException {
return (LocalEvent[])queryEvents(Events.DIRTY + "!=0", null);
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
List<LocalResource> dirty = new LinkedList<>();
// get dirty events which are not required to have an increased SEQUENCE value
for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "=" + DIRTY_DONT_INCREASE_SEQUENCE + " AND " + Events.ORIGINAL_ID + " IS NULL", null))
dirty.add(event);
// get dirty events which are required to have an increased SEQUENCE value
for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "=" + DIRTY_INCREASE_SEQUENCE + " AND " + Events.ORIGINAL_ID + " IS NULL", null)) {
event.getEvent().sequence++;
dirty.add(event);
}
return dirty.toArray(new LocalResource[dirty.size()]);
}
......@@ -117,6 +153,75 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
}
}
public void processDirtyExceptions() throws CalendarStorageException {
// process deleted exceptions
Constants.log.info("Processing deleted exceptions");
try {
@Cleanup Cursor cursor = provider.query(
syncAdapterURI(Events.CONTENT_URI),
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
while (cursor != null && cursor.moveToNext()) {
Constants.log.debug("Found deleted exception, removing; then re-schuling original event");
long id = cursor.getLong(0), // can't be null (by definition)
originalID = cursor.getLong(1); // can't be null (by query)
int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2);
// get original event's SEQUENCE
@Cleanup Cursor cursor2 = provider.query(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
new String[] { LocalEvent.COLUMN_SEQUENCE },
null, null, null);
int originalSequence = cursor.isNull(0) ? 0 : cursor.getInt(0);
BatchOperation batch = new BatchOperation(provider);
// re-schedule original event and set it to DIRTY
batch.enqueue(ContentProviderOperation.newUpdate(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence)
.withValue(Events.DIRTY, DIRTY_INCREASE_SEQUENCE)
.build());
// remove exception
batch.enqueue(ContentProviderOperation.newDelete(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))).build());
batch.commit();
}
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't process locally modified exception", e);
}
// process dirty exceptions
Constants.log.info("Processing dirty exceptions");
try {
@Cleanup Cursor cursor = provider.query(
syncAdapterURI(Events.CONTENT_URI),
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
while (cursor != null && cursor.moveToNext()) {
Constants.log.debug("Found dirty exception, increasing SEQUENCE to re-schedule");
long id = cursor.getLong(0), // can't be null (by definition)
originalID = cursor.getLong(1); // can't be null (by query)
int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2);
BatchOperation batch = new BatchOperation(provider);
// original event to DIRTY
batch.enqueue(ContentProviderOperation.newUpdate(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, DIRTY_DONT_INCREASE_SEQUENCE)
.build());
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(ContentProviderOperation.newUpdate(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0)
.build());
batch.commit();
}
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't process locally modified exception", e);
}
}
public static class Factory implements AndroidCalendarFactory {
public static final Factory INSTANCE = new Factory();
......
......@@ -10,6 +10,8 @@ package at.bitfire.davdroid.resource;
import android.provider.ContactsContract;
import java.io.FileNotFoundException;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
......@@ -17,7 +19,7 @@ public interface LocalCollection {
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
......
......@@ -22,7 +22,7 @@ import ezvcard.Ezvcard;
public class LocalContact extends AndroidContact implements LocalResource {
static {
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION;
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + "vcard4android ez-vcard/" + Ezvcard.VERSION;
}
protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
......
......@@ -10,37 +10,46 @@ 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 android.provider.CalendarContract.Events;
import net.fortuna.ical4j.model.property.ProdId;
import at.bitfire.davdroid.BuildConfig;
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.NonNull;
import lombok.Setter;
public class LocalEvent extends AndroidEvent implements LocalResource {
static {
Event.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
}
static final String COLUMN_FILENAME = CalendarContract.Events.SYNC_DATA1,
COLUMN_ETAG = CalendarContract.Events.SYNC_DATA2,
COLUMN_UID = CalendarContract.Events.UID_2445;
static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1,
COLUMN_UID = CalendarContract.Events.UID_2445,
COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA2;
@Getter protected String fileName;
@Getter @Setter protected String eTag;
public LocalEvent(AndroidCalendar calendar, Event event, String fileName, String eTag) {
public LocalEvent(@NonNull 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) {
protected LocalEvent(@NonNull AndroidCalendar calendar, long id, ContentValues baseInfo) {
super(calendar, id, baseInfo);
fileName = baseInfo.getAsString(COLUMN_FILENAME);
eTag = baseInfo.getAsString(COLUMN_ETAG);
if (baseInfo != null) {
fileName = baseInfo.getAsString(Events._SYNC_ID);
eTag = baseInfo.getAsString(COLUMN_ETAG);
}
}
......@@ -49,17 +58,31 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
@Override
protected void populateEvent(ContentValues values) {
super.populateEvent(values);
fileName = values.getAsString(COLUMN_FILENAME);
fileName = values.getAsString(Events._SYNC_ID);
eTag = values.getAsString(COLUMN_ETAG);
event.uid = values.getAsString(COLUMN_UID);
if (values.containsKey(COLUMN_SEQUENCE))
event.sequence = values.getAsInteger(COLUMN_SEQUENCE);
}
@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);
boolean buildException = recurrence != null;
Event eventToBuild = buildException ? recurrence : event;
builder .withValue(COLUMN_UID, event.uid)
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0);
if (buildException)
builder.withValue(Events.ORIGINAL_SYNC_ID, fileName);
else
builder .withValue(Events._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag);
}
......@@ -70,7 +93,7 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
String newFileName = uid + ".ics";
ContentValues values = new ContentValues(2);
values.put(COLUMN_FILENAME, newFileName);
values.put(Events._SYNC_ID, newFileName);
values.put(COLUMN_UID, uid);
calendar.provider.update(eventSyncURI(), values, null, null);
......@@ -89,6 +112,8 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
ContentValues values = new ContentValues(2);
values.put(CalendarContract.Events.DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
if (event != null)
values.put(COLUMN_SEQUENCE, event.sequence);
calendar.provider.update(eventSyncURI(), values, null, null);
this.eTag = eTag;
......
......@@ -101,6 +101,13 @@ public class CalendarSyncManager extends SyncManager {
localCalendar().update(values);
}
@Override
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
super.prepareDirty();
localCalendar().processDirtyExceptions();
}
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
LocalEvent local = (LocalEvent)resource;
......
......@@ -16,6 +16,7 @@ import android.content.Intent;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.CalendarContract;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCalendar;
......@@ -52,7 +53,7 @@ public class CalendarsSyncAdapterService extends Service {
Constants.log.info("Starting calendar sync (" + authority + ")");
try {
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.findAll(account, provider, LocalCalendar.Factory.INSTANCE)) {
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
Constants.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, provider, syncResult, calendar);
syncManager.performSync();
......
......@@ -24,6 +24,7 @@ import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.RequestBody;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
......@@ -34,6 +35,7 @@ import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.exception.PreconditionFailedException;
import at.bitfire.dav4android.exception.ServiceUnavailableException;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.Constants;
......@@ -50,7 +52,7 @@ abstract public class SyncManager {
protected final int SYNC_PHASE_PREPARE = 0,
SYNC_PHASE_QUERY_CAPABILITIES = 1,
SYNC_PHASE_PROCESS_LOCALLY_DELETED = 2,
SYNC_PHASE_PREPARE_LOCALLY_CREATED = 3,
SYNC_PHASE_PREPARE_DIRTY = 3,
SYNC_PHASE_UPLOAD_DIRTY = 4,
SYNC_PHASE_CHECK_SYNC_STATE = 5,
SYNC_PHASE_LIST_LOCAL = 6,
......@@ -120,9 +122,9 @@ abstract public class SyncManager {
Constants.log.info("Processing locally deleted entries");
processLocallyDeleted();
syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED;
Constants.log.info("Processing locally created entries");
processLocallyCreated();
syncPhase = SYNC_PHASE_PREPARE_DIRTY;
Constants.log.info("Locally preparing dirty entries");
prepareDirty();
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
Constants.log.info("Uploading dirty entries");
......@@ -153,10 +155,18 @@ abstract public class SyncManager {
} else
Constants.log.info("Remote collection didn't change, skipping remote sync");
} catch (IOException e) {
} catch (IOException|ServiceUnavailableException e) {
Constants.log.error("I/O exception during sync, trying again later", e);
syncResult.stats.numIoExceptions++;
if (e instanceof ServiceUnavailableException) {
Date retryAfter = ((ServiceUnavailableException) e).retryAfter;
if (retryAfter != null) {
// how many seconds to wait? getTime() returns ms, so divide by 1000
syncResult.delayUntil = (retryAfter.getTime() - new Date().getTime()) / 1000;
}
}
} catch(HttpException|DavException e) {
Constants.log.error("HTTP/DAV Exception during sync", e);
syncResult.stats.numParseExceptions++;
......@@ -207,7 +217,7 @@ abstract public class SyncManager {
try {
new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
.delete(local.getETag());
} catch (IOException | HttpException e) {
} catch (IOException|HttpException e) {
Constants.log.warn("Couldn't delete " + fileName + " from server");
}
} else
......@@ -217,7 +227,7 @@ abstract public class SyncManager {
}
}
protected void processLocallyCreated() throws CalendarStorageException, ContactsStorageException {
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
// assign file names and UIDs to new contacts so that we can use the file name as an index
for (LocalResource local : localCollection.getWithoutFileName()) {
String uuid = UUID.randomUUID().toString();
......
......@@ -38,8 +38,6 @@ public class DebugInfoActivity extends Activity {
KEY_ACCOUNT = "account",
KEY_PHASE = "phase";
private static final String APP_ID = "at.bitfire.davdroid";
String report;
@Override
......@@ -49,7 +47,7 @@ public class DebugInfoActivity extends Activity {
setContentView(R.layout.debug_info_activity);
TextView tvReport = (TextView)findViewById(R.id.text_report);
tvReport.setText(generateReport(getIntent().getExtras()));
tvReport.setText(report = generateReport(getIntent().getExtras()));
}
@Override
......@@ -96,7 +94,7 @@ public class DebugInfoActivity extends Activity {
try {
PackageManager pm = getPackageManager();
String installedFrom = pm.getInstallerPackageName("at.bitfire.davdroid");
String installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID);
if (TextUtils.isEmpty(installedFrom))
installedFrom = "APK (directly)";
else {
......
......@@ -183,8 +183,8 @@
<item>Vorbereiten der Synchronisierung</item>
<item>Abfragen der Server-Fähigkeiten</item>
<item>Verarbeiten lokal gelöschter Einträge</item>
<item>Vorbereiten neuer lokaler Einträge</item>
<item>Hochladen neuer/geänderter lokaler Einträge</item>
<item>Vorbereiten neuer/geänderter Einträge</item>
<item>Hochladen neuer/geänderter Einträge</item>
<item>Abfragen des Synchronisierungs-Zustands</item>
<item>Auflisten lokaler Einträge</item>
<item>Auflisten der Server-Einträge</item>
......
......@@ -198,7 +198,7 @@
<item>preparing synchronization</item>
<item>querying capabilities</item>
<item>processing locally deleted entries</item>
<item>preparing locally created entries</item>
<item>preparing created/modified entries</item>
<item>uploading created/modified entries</item>
<item>checking sync state</item>
<item>listing local entries</item>
......
Subproject commit 4e1131ae4607b4220e2d37632fd54a987b633849
Subproject commit ea504f2512ad5e9a85391797bdbdeb6c92871cdf
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment