1
0
mirror of https://github.com/etesync/android synced 2025-03-25 03:45:46 +00:00

Sync recurring event exceptions to CalDAV server

* added SQL filter possibility to generic LocalCollection
* added exceptions of recurring events to Event
* process exceptions of recurring events in LocalCalendar
This commit is contained in:
Ricki Hirner 2015-04-28 01:36:01 +02:00
parent 2696e64a83
commit a405d07baf
3 changed files with 151 additions and 85 deletions

View File

@ -41,6 +41,7 @@ import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.RecurrenceId;
import net.fortuna.ical4j.model.property.Status;
import net.fortuna.ical4j.model.property.Summary;
import net.fortuna.ical4j.model.property.Transp;
@ -75,32 +76,29 @@ public class Event extends Resource {
public final static String MIME_TYPE = "text/calendar";
private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry();
@Getter @Setter protected RecurrenceId recurrenceId;
@Getter @Setter protected String summary, location, description;
@Getter @Setter private String summary, location, description;
@Getter protected DtStart dtStart;
@Getter protected DtEnd dtEnd;
@Getter @Setter protected Duration duration;
@Getter @Setter protected RDate rdate;
@Getter @Setter protected RRule rrule;
@Getter @Setter protected ExDate exdate;
@Getter @Setter protected ExRule exrule;
@Getter protected List<Event> exceptions = new LinkedList<>();
@Getter @Setter protected Boolean forPublic;
@Getter @Setter protected Status status;
@Getter private DtStart dtStart;
@Getter private DtEnd dtEnd;
@Getter @Setter private Duration duration;
@Getter @Setter private RDate rdate;
@Getter @Setter private RRule rrule;
@Getter @Setter private ExDate exdate;
@Getter @Setter private ExRule exrule;
@Getter @Setter protected boolean opaque;
@Getter @Setter private Boolean forPublic;
@Getter @Setter private Status status;
@Getter @Setter private boolean opaque;
@Getter @Setter private Organizer organizer;
@Getter private List<Attendee> attendees = new LinkedList<Attendee>();
public void addAttendee(Attendee attendee) {
attendees.add(attendee);
}
@Getter private List<VAlarm> alarms = new LinkedList<VAlarm>();
public void addAlarm(VAlarm alarm) {
alarms.add(alarm);
}
@Getter @Setter protected Organizer organizer;
@Getter protected List<Attendee> attendees = new LinkedList<Attendee>();
@Getter protected List<VAlarm> alarms = new LinkedList<VAlarm>();
static {
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);
@ -215,51 +213,18 @@ public class Event extends Resource {
net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
ical.getProperties().add(Version.VERSION_2_0);
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN"));
VEvent event = new VEvent();
PropertyList props = event.getProperties();
if (uid != null)
props.add(new Uid(uid));
props.add(dtStart);
if (dtEnd != null)
props.add(dtEnd);
if (duration != null)
props.add(duration);
if (rrule != null)
props.add(rrule);
if (rdate != null)
props.add(rdate);
if (exrule != null)
props.add(exrule);
if (exdate != null)
props.add(exdate);
if (summary != null && !summary.isEmpty())
props.add(new Summary(summary));
if (location != null && !location.isEmpty())
props.add(new Location(location));
if (description != null && !description.isEmpty())
props.add(new Description(description));
if (status != null)
props.add(status);
if (!opaque)
props.add(Transp.TRANSPARENT);
if (organizer != null)
props.add(organizer);
props.addAll(attendees);
if (forPublic != null)
event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
event.getAlarms().addAll(alarms);
props.add(new LastModified());
ical.getComponents().add(event);
// "main event" (without exceptions)
ComponentList components = ical.getComponents();
VEvent mainEvent = toVEvent(this);
components.add(mainEvent);
// recurrence exceptions
for (Event exception : exceptions) {
VEvent vException = toVEvent(exception);
vException.getProperties().add(mainEvent.getProperty(Property.UID));
components.add(vException);
}
// add VTIMEZONE components
net.fortuna.ical4j.model.TimeZone
@ -280,6 +245,55 @@ public class Event extends Resource {
return os;
}
protected static VEvent toVEvent(Event e) {
VEvent event = new VEvent();
PropertyList props = event.getProperties();
if (e.uid != null)
props.add(new Uid(e.uid));
if (e.recurrenceId != null)
props.add(e.recurrenceId);
props.add(e.dtStart);
if (e.dtEnd != null)
props.add(e.dtEnd);
if (e.duration != null)
props.add(e.duration);
if (e.rrule != null)
props.add(e.rrule);
if (e.rdate != null)
props.add(e.rdate);
if (e.exrule != null)
props.add(e.exrule);
if (e.exdate != null)
props.add(e.exdate);
if (e.summary != null && !e.summary.isEmpty())
props.add(new Summary(e.summary));
if (e.location != null && !e.location.isEmpty())
props.add(new Location(e.location));
if (e.description != null && !e.description.isEmpty())
props.add(new Description(e.description));
if (e.status != null)
props.add(e.status);
if (!e.opaque)
props.add(Transp.TRANSPARENT);
if (e.organizer != null)
props.add(e.organizer);
props.addAll(e.attendees);
if (e.forPublic != null)
event.getProperties().add(e.forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
event.getAlarms().addAll(e.alarms);
props.add(new LastModified());
return event;
}
public long getDtStartInMillis() {
return dtStart.getDate().getTime();

View File

@ -29,6 +29,8 @@ import android.provider.CalendarContract.Reminders;
import android.provider.ContactsContract;
import android.util.Log;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Dur;
import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.ParameterList;
@ -47,6 +49,7 @@ import net.fortuna.ical4j.model.property.ExRule;
import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.RecurrenceId;
import net.fortuna.ical4j.model.property.Status;
import org.apache.commons.lang.StringUtils;
@ -172,6 +175,7 @@ public class LocalCalendar extends LocalCollection<Event> {
super(account, providerClient);
this.id = id;
this.url = url;
sqlFilter = "ORIGINAL_ID IS NULL";
}
@ -210,18 +214,20 @@ public class LocalCalendar extends LocalCollection<Event> {
}
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
String where;
String where = entryColumnParentID() + "=?";
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, ",") + ")";
where += " AND " + entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")";
} else
where = entryColumnRemoteName() + " IS NOT NULL";
where += " AND " + entryColumnRemoteName() + " IS NOT NULL";
if (sqlFilter != null)
where += " AND (" + sqlFilter + ")";
Builder builder = ContentProviderOperation.newDelete(entriesURI())
.withSelection(entryColumnParentID() + "=? AND (" + where + ")", new String[] { String.valueOf(id) });
.withSelection(where, new String[] { String.valueOf(id) });
pendingOperations.add(builder
.withYieldAllowed(true)
.build());
@ -229,7 +235,6 @@ public class LocalCalendar extends LocalCollection<Event> {
/* methods for populating the data object from the content provider */
@Override
public void populate(Resource resource) throws LocalStorageException {
@ -243,7 +248,8 @@ public class LocalCalendar extends LocalCollection<Event> {
/* 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
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY,
/* 20 */ Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_INSTANCE_TIME
}, null, null, null);
if (cursor != null && cursor.moveToNext()) {
e.setUid(cursor.getString(17));
@ -311,7 +317,21 @@ public class LocalCalendar extends LocalCollection<Event> {
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
}
// recurrence exceptions
if (!cursor.isNull(21)) {
long originalInstanceTime = cursor.getLong(21);
boolean originalAllDay = cursor.getInt(20) != 0;
Date originalDate = originalAllDay ?
new Date(originalInstanceTime) :
new DateTime(originalInstanceTime);
if (originalDate instanceof DateTime)
((DateTime)originalDate).setUtc(true);
e.setRecurrenceId(new RecurrenceId(originalDate));
} else
// this event may have exceptions
populateExceptions(e);
// status
switch (cursor.getInt(8)) {
case Events.STATUS_CONFIRMED:
@ -355,15 +375,35 @@ public class LocalCalendar extends LocalCollection<Event> {
}
}
void populateExceptions(Event e) throws RemoteException {
Uri exceptionsUri = Events.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@Cleanup Cursor c = providerClient.query(exceptionsUri, new String[] {
/* 0 */ Events._ID, entryColumnRemoteName()
}, Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
long exceptionId = c.getLong(0);
String exceptionRemoteName = c.getString(1);
Log.i(TAG, "Found exception ID " + exceptionId + " of original ID " + e.getLocalID());
try {
Event exception = new Event(exceptionId, exceptionRemoteName, null);
populate(exception);
e.getExceptions().add(exception);
} catch (LocalStorageException ex) {
Log.e(TAG, "Couldn't find exception details, ignoring");
}
}
}
void populateAttendees(Event e) throws RemoteException {
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@Cleanup Cursor c = providerClient.query(attendeesUri, new String[] {
@Cleanup 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);
}, 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));
@ -408,13 +448,13 @@ public class LocalCalendar extends LocalCollection<Event> {
break;
}
e.addAttendee(attendee);
e.getAttendees().add(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()
@ -435,7 +475,7 @@ public class LocalCalendar extends LocalCollection<Event> {
props.add(Action.DISPLAY);
props.add(new Description(e.getSummary()));
}
e.addAlarm(alarm);
e.getAlarms().add(alarm);
}
}

View File

@ -32,7 +32,7 @@ import lombok.Cleanup;
* @param <T> Subtype of Resource that can be stored in the collection
*/
public abstract class LocalCollection<T extends Resource> {
private static final String TAG = "davdroid.LocalCollection";
private static final String TAG = "davdroid.Collection";
protected Account account;
protected ContentProviderClient providerClient;
@ -66,6 +66,9 @@ public abstract class LocalCollection<T extends Resource> {
/** column name of an entry's UID */
abstract protected String entryColumnUID();
/** SQL filter expression */
String sqlFilter;
LocalCollection(Account account, ContentProviderClient providerClient) {
@ -97,6 +100,8 @@ public abstract class LocalCollection<T extends Resource> {
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
if (sqlFilter != null)
where += " AND (" + sqlFilter + ")";
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() },
@ -136,6 +141,8 @@ public abstract class LocalCollection<T extends Resource> {
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
if (sqlFilter != null)
where += " AND (" + sqlFilter + ")";
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
@ -163,6 +170,8 @@ public abstract class LocalCollection<T extends Resource> {
String where = entryColumnDeleted() + "=1";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
if (sqlFilter != null)
where += " AND (" + sqlFilter + ")";
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
@ -191,7 +200,7 @@ public abstract class LocalCollection<T extends Resource> {
public T findById(long localID, boolean populate) throws LocalStorageException {
try {
@Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
new String[] { entryColumnRemoteName(), entryColumnETag() }, sqlFilter, null, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
if (populate)
@ -214,10 +223,13 @@ public abstract class LocalCollection<T extends Resource> {
* @throws LocalStorageException when the content provider couldn't be queried
*/
public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException {
String where = entryColumnRemoteName() + "=?";
if (sqlFilter != null)
where += " AND (" + sqlFilter + ")";
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
where, new String[] { remoteName }, null);
if (cursor != null && cursor.moveToNext()) {
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
if (populate)