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:
parent
2696e64a83
commit
a405d07baf
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user