mirror of
https://github.com/etesync/android
synced 2024-12-23 15:18:14 +00:00
Synchronize exceptions of recurring events to the Calendar storage (server to client)
* Event class finds and processes exceptions of recurring events * workaround for iCloud and other services that provide RECURRENCE-ID as DATETIME even if the original event is an all-day event * VEvents are generated with all time zone definitions (including time zone definitions of exceptions)
This commit is contained in:
parent
f6eee6c910
commit
495cdf7c7e
@ -58,8 +58,10 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@ -145,19 +147,44 @@ public class Event extends Resource {
|
||||
throw new InvalidResourceException(e);
|
||||
}
|
||||
|
||||
// event
|
||||
ComponentList events = ical.getComponents(Component.VEVENT);
|
||||
if (events == null || events.isEmpty())
|
||||
throw new InvalidResourceException("No VEVENT found");
|
||||
VEvent event = (VEvent)events.get(0);
|
||||
|
||||
|
||||
// find master VEVENT (the one that is not an exception, i.e. the one without RECURRENCE-ID)
|
||||
VEvent master = null;
|
||||
for (Object objEvent : events) {
|
||||
VEvent event = (VEvent)objEvent;
|
||||
if (event.getRecurrenceId() == null) {
|
||||
master = event;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (master == null)
|
||||
throw new InvalidResourceException("No VEVENT without RECURRENCE-ID found");
|
||||
// set event data from master VEVENT
|
||||
fromVEvent(master);
|
||||
|
||||
// find and process exceptions
|
||||
for (Object objEvent : events) {
|
||||
VEvent event = (VEvent)objEvent;
|
||||
if (event.getRecurrenceId() != null) {
|
||||
Event exception = new Event(name, null);
|
||||
exception.fromVEvent(event);
|
||||
exceptions.add(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void fromVEvent(VEvent event) throws InvalidResourceException {
|
||||
if (event.getUid() != null)
|
||||
uid = event.getUid().getValue();
|
||||
else {
|
||||
Log.w(TAG, "Received VEVENT without UID, generating new one");
|
||||
generateUID();
|
||||
}
|
||||
|
||||
recurrenceId = event.getRecurrenceId();
|
||||
|
||||
if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null)
|
||||
throw new InvalidResourceException("Invalid start time/end time/duration");
|
||||
|
||||
@ -165,7 +192,7 @@ public class Event extends Resource {
|
||||
validateTimeZone(dtStart);
|
||||
validateTimeZone(dtEnd);
|
||||
}
|
||||
|
||||
|
||||
// all-day events and "events on that day":
|
||||
// * related UNIX times must be in UTC
|
||||
// * must have a duration (set to one day if missing)
|
||||
@ -176,26 +203,26 @@ public class Event extends Resource {
|
||||
c.add(Calendar.DATE, 1);
|
||||
dtEnd.setDate(new Date(c.getTimeInMillis()));
|
||||
}
|
||||
|
||||
|
||||
rrule = (RRule)event.getProperty(Property.RRULE);
|
||||
rdate = (RDate)event.getProperty(Property.RDATE);
|
||||
exrule = (ExRule)event.getProperty(Property.EXRULE);
|
||||
exdate = (ExDate)event.getProperty(Property.EXDATE);
|
||||
|
||||
|
||||
if (event.getSummary() != null)
|
||||
summary = event.getSummary().getValue();
|
||||
if (event.getLocation() != null)
|
||||
location = event.getLocation().getValue();
|
||||
if (event.getDescription() != null)
|
||||
description = event.getDescription().getValue();
|
||||
|
||||
|
||||
status = event.getStatus();
|
||||
opaque = event.getTransparency() != Transp.TRANSPARENT;
|
||||
|
||||
opaque = event.getTransparency() != Transp.TRANSPARENT;
|
||||
|
||||
organizer = event.getOrganizer();
|
||||
for (Object o : event.getProperties(Property.ATTENDEE))
|
||||
attendees.add((Attendee)o);
|
||||
|
||||
|
||||
Clazz classification = event.getClassification();
|
||||
if (classification != null) {
|
||||
if (classification == Clazz.PUBLIC)
|
||||
@ -203,10 +230,12 @@ public class Event extends Resource {
|
||||
else if (classification == Clazz.CONFIDENTIAL || classification == Clazz.PRIVATE)
|
||||
forPublic = false;
|
||||
}
|
||||
|
||||
|
||||
this.alarms = event.getAlarms();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ByteArrayOutputStream toEntity() throws IOException {
|
||||
@ -214,26 +243,38 @@ public class Event extends Resource {
|
||||
ical.getProperties().add(Version.VERSION_2_0);
|
||||
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN"));
|
||||
|
||||
// "main event" (without exceptions)
|
||||
// "master event" (without exceptions)
|
||||
ComponentList components = ical.getComponents();
|
||||
VEvent mainEvent = toVEvent(this);
|
||||
components.add(mainEvent);
|
||||
VEvent master = toVEvent();
|
||||
components.add(master);
|
||||
|
||||
// remember used time zones
|
||||
Set<net.fortuna.ical4j.model.TimeZone> usedTimeZones = new HashSet<>();
|
||||
if (dtStart != null && dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(dtStart.getTimeZone());
|
||||
if (dtEnd != null && dtEnd.getTimeZone() != null)
|
||||
usedTimeZones.add(dtEnd.getTimeZone());
|
||||
|
||||
// recurrence exceptions
|
||||
for (Event exception : exceptions) {
|
||||
VEvent vException = toVEvent(exception);
|
||||
vException.getProperties().add(mainEvent.getProperty(Property.UID));
|
||||
// create VEVENT for exception
|
||||
VEvent vException = exception.toVEvent();
|
||||
|
||||
// set UID to UID of master event
|
||||
vException.getProperties().add(master.getProperty(Property.UID));
|
||||
|
||||
components.add(vException);
|
||||
|
||||
// remember used time zones
|
||||
if (exception.dtStart != null && exception.dtStart.getTimeZone() != null)
|
||||
usedTimeZones.add(exception.dtStart.getTimeZone());
|
||||
if (exception.dtEnd != null && exception.dtEnd.getTimeZone() != null)
|
||||
usedTimeZones.add(exception.dtEnd.getTimeZone());
|
||||
}
|
||||
|
||||
// add VTIMEZONE components
|
||||
net.fortuna.ical4j.model.TimeZone
|
||||
tzStart = (dtStart == null ? null : dtStart.getTimeZone()),
|
||||
tzEnd = (dtEnd == null ? null : dtEnd.getTimeZone());
|
||||
if (tzStart != null)
|
||||
ical.getComponents().add(tzStart.getVTimeZone());
|
||||
if (tzEnd != null && tzEnd != tzStart)
|
||||
ical.getComponents().add(tzEnd.getVTimeZone());
|
||||
for (net.fortuna.ical4j.model.TimeZone timeZone : usedTimeZones)
|
||||
ical.getComponents().add(timeZone.getVTimeZone());
|
||||
|
||||
CalendarOutputter output = new CalendarOutputter(false);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
@ -245,50 +286,50 @@ public class Event extends Resource {
|
||||
return os;
|
||||
}
|
||||
|
||||
protected static VEvent toVEvent(Event e) {
|
||||
protected VEvent toVEvent() {
|
||||
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);
|
||||
if (uid != null)
|
||||
props.add(new Uid(uid));
|
||||
if (recurrenceId != null)
|
||||
props.add(recurrenceId);
|
||||
|
||||
props.add(e.dtStart);
|
||||
if (e.dtEnd != null)
|
||||
props.add(e.dtEnd);
|
||||
if (e.duration != null)
|
||||
props.add(e.duration);
|
||||
props.add(dtStart);
|
||||
if (dtEnd != null)
|
||||
props.add(dtEnd);
|
||||
if (duration != null)
|
||||
props.add(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 (rrule != null)
|
||||
props.add(rrule);
|
||||
if (rdate != null)
|
||||
props.add(rdate);
|
||||
if (exrule != null)
|
||||
props.add(exrule);
|
||||
if (exdate != null)
|
||||
props.add(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 (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 (e.status != null)
|
||||
props.add(e.status);
|
||||
if (!e.opaque)
|
||||
if (status != null)
|
||||
props.add(status);
|
||||
if (!opaque)
|
||||
props.add(Transp.TRANSPARENT);
|
||||
|
||||
if (e.organizer != null)
|
||||
props.add(e.organizer);
|
||||
props.addAll(e.attendees);
|
||||
if (organizer != null)
|
||||
props.add(organizer);
|
||||
props.addAll(attendees);
|
||||
|
||||
if (e.forPublic != null)
|
||||
event.getProperties().add(e.forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
|
||||
if (forPublic != null)
|
||||
event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
|
||||
|
||||
event.getAlarms().addAll(e.alarms);
|
||||
event.getAlarms().addAll(alarms);
|
||||
|
||||
props.add(new LastModified());
|
||||
return event;
|
||||
|
@ -35,6 +35,9 @@ import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.Parameter;
|
||||
import net.fortuna.ical4j.model.ParameterList;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.parameter.Cn;
|
||||
import net.fortuna.ical4j.model.parameter.CuType;
|
||||
@ -51,6 +54,7 @@ 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.util.TimeZones;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
@ -211,11 +215,12 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
// mark (recurring) events with changed/deleted exceptions as dirty
|
||||
String where = entryColumnID() + " IN (SELECT DISTINCT " + Events.ORIGINAL_ID + " FROM events WHERE " +
|
||||
Events.ORIGINAL_ID + " IS NOT NULL AND (" + Events.DIRTY + "=1 OR " + Events.DELETED + "=1))";
|
||||
Log.i(TAG, where);
|
||||
ContentValues dirty = new ContentValues(1);
|
||||
dirty.put(CalendarContract.Events.DIRTY, 1);
|
||||
try {
|
||||
providerClient.update(entriesURI(), dirty, where, null);
|
||||
int rows = providerClient.update(entriesURI(), dirty, where, null);
|
||||
if (rows > 0)
|
||||
Log.d(TAG, rows + " event(s) marked as dirty because of dirty/deleted exceptions");
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Couldn't mark events with updated exceptions as dirty", e);
|
||||
}
|
||||
@ -540,9 +545,6 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
|
||||
builder = builder
|
||||
.withValue(Events.CALENDAR_ID, id)
|
||||
.withValue(entryColumnRemoteName(), event.getName())
|
||||
.withValue(entryColumnETag(), event.getETag())
|
||||
.withValue(entryColumnUID(), event.getUid())
|
||||
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
||||
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
||||
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
|
||||
@ -550,7 +552,26 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
.withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1)
|
||||
.withValue(Events.GUESTS_CAN_MODIFY, 1)
|
||||
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
||||
|
||||
|
||||
RecurrenceId recurrenceId = event.getRecurrenceId();
|
||||
if (recurrenceId == null) {
|
||||
// this event is a "master event" (not an exception)
|
||||
builder = builder
|
||||
.withValue(entryColumnRemoteName(), event.getName())
|
||||
.withValue(entryColumnETag(), event.getETag())
|
||||
.withValue(entryColumnUID(), event.getUid());
|
||||
} else {
|
||||
// this event is an exception for a recurring event -> calculate
|
||||
// 1. ORIGINAL_INSTANCE_TIME when the original instance would have occured (ms UTC)
|
||||
// 2. ORIGINAL_ALL_DAY was the original instance an all-day event?
|
||||
builder = builder.withValue(Events.ORIGINAL_SYNC_ID, event.getName());
|
||||
|
||||
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY is set in buildExceptions.
|
||||
// It's not possible to use only the RECURRENCE-ID to calculate
|
||||
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY because iCloud sends DATE-TIME
|
||||
// RECURRENCE-IDs even if the original event is an all-day event.
|
||||
}
|
||||
|
||||
boolean recurring = false;
|
||||
if (event.getRrule() != null) {
|
||||
recurring = true;
|
||||
@ -612,8 +633,13 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
@Override
|
||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||
Event event = (Event)resource;
|
||||
// add exceptions
|
||||
for (Event exception : event.getExceptions())
|
||||
pendingOperations.add(buildException(newDataInsertBuilder(Events.CONTENT_URI, Events.ORIGINAL_ID, localID, backrefIdx), event, exception).build());
|
||||
// add attendees
|
||||
for (Attendee attendee : event.getAttendees())
|
||||
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
||||
// add reminders
|
||||
for (VAlarm alarm : event.getAlarms())
|
||||
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
|
||||
}
|
||||
@ -621,14 +647,54 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
@Override
|
||||
protected void removeDataRows(Resource resource) {
|
||||
Event event = (Event)resource;
|
||||
// delete exceptions
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(entriesURI())
|
||||
.withSelection(Events.ORIGINAL_ID + "=?",
|
||||
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||
// delete attendees
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
||||
.withSelection(Attendees.EVENT_ID + "=?",
|
||||
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||
// delete reminders
|
||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
|
||||
.withSelection(Reminders.EVENT_ID + "=?",
|
||||
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||
}
|
||||
|
||||
|
||||
protected Builder buildException(Builder builder, Event master, Event exception) {
|
||||
buildEntry(builder, exception);
|
||||
builder.withValue(Events.ORIGINAL_SYNC_ID, exception.getName());
|
||||
|
||||
// Some servers (iCloud, for instance) return RECURRENCE-ID with DATE-TIME even if
|
||||
// the original event is an all-day event. Workaround: determine value of ORIGINAL_ALL_DAY
|
||||
// by original event type (all-day or not) and not by whether RECURRENCE-ID is DATE or DATE-TIME.
|
||||
|
||||
RecurrenceId recurrenceId = exception.getRecurrenceId();
|
||||
Date date = recurrenceId.getDate();
|
||||
|
||||
boolean originalAllDay = master.isAllDay();
|
||||
long originalInstanceTime;
|
||||
|
||||
if (originalAllDay && date instanceof DateTime) {
|
||||
String value = recurrenceId.getValue();
|
||||
if (value.matches("^\\d{8}T\\d{6}$"))
|
||||
try {
|
||||
// no "Z" at the end indicates "local" time
|
||||
// so this is a "local" time, but it should be a ical4j Date without time
|
||||
date = new Date(value.substring(0, 8));
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Couldn't parse DATE part of DATE-TIME RECURRENCE-ID", e);
|
||||
}
|
||||
}
|
||||
originalInstanceTime = date.getTime();
|
||||
Log.i(TAG, "Original instance time: " + date.getTime()/1000);
|
||||
|
||||
builder.withValue(Events.ORIGINAL_INSTANCE_TIME, originalInstanceTime);
|
||||
builder.withValue(Events.ORIGINAL_ALL_DAY, originalAllDay ? 1 : 0);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
||||
|
@ -350,7 +350,7 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) {
|
||||
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, int backrefIdx) {
|
||||
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
||||
if (backrefIdx != -1)
|
||||
return builder.withValueBackReference(refFieldName, backrefIdx);
|
||||
|
Loading…
Reference in New Issue
Block a user