/* * * * Copyright (C) 2014 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * / */ package org.anhonesteffort.flock.sync.calendar; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; import android.provider.CalendarContract; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import org.anhonesteffort.flock.util.guava.Optional; import org.anhonesteffort.flock.sync.InvalidLocalComponentException; import org.anhonesteffort.flock.sync.InvalidRemoteComponentException; import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; import org.anhonesteffort.flock.webdav.ComponentETagPair; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.ComponentList; import net.fortuna.ical4j.model.ConstraintViolationException; 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; import net.fortuna.ical4j.model.Property; 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.component.VEvent; import net.fortuna.ical4j.model.component.VToDo; import net.fortuna.ical4j.model.parameter.Cn; import net.fortuna.ical4j.model.parameter.CuType; import net.fortuna.ical4j.model.parameter.PartStat; import net.fortuna.ical4j.model.parameter.Role; import net.fortuna.ical4j.model.property.Action; import net.fortuna.ical4j.model.property.Attendee; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.ExDate; import net.fortuna.ical4j.model.property.ExRule; import net.fortuna.ical4j.model.property.Location; 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.Status; import net.fortuna.ical4j.model.property.Summary; import net.fortuna.ical4j.model.property.Transp; import net.fortuna.ical4j.model.property.Trigger; import net.fortuna.ical4j.model.property.Uid; import net.fortuna.ical4j.model.property.Version; import net.fortuna.ical4j.model.property.XProperty; import net.fortuna.ical4j.util.Calendars; import org.apache.commons.lang.StringUtils; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; import java.util.LinkedList; import java.util.List; import java.util.UUID; /** * Programmer: rhodey * * Much thanks to the DAVDroid project and especially * Richard Hirner (bitfire web engineering) for leading * the way in shoving VEvent objects into Androids Calendar * Content Provider. This would have been much more of a * pain without a couple hints from the DAVDroid codebase. */ public class EventFactory { private static final String TAG = "org.anhonesteffort.flock.sync.calendar.EventFactory"; protected static final String COLUMN_NAME_EVENT_UID = CalendarContract.Events._SYNC_ID; protected static final String COLUMN_NAME_EVENT_ETAG = CalendarContract.Events.SYNC_DATA1; protected static final String COLUMN_NAME_COPIED_EVENT_ID = CalendarContract.Events.SYNC_DATA2; private static final String PROPERTY_NAME_FLOCK_ALL_DAY = "X-FLOCK-ALL-DAY"; private static final String PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID = "X-FLOCK-ORIGINAL-SYNC-ID"; private static final String PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME = "X-FLOCK-ORIGINAL-INSTANCE-TIME"; protected static final String PROPERTY_NAME_FLOCK_COPY_EVENT_ID = "X-FLOCK-COPY-EVENT-ID"; private static String getUid(VEvent vEvent) { if (vEvent.getUid() != null) return vEvent.getUid().getValue(); return null; } private static String getUid(Calendar component) { try { Uid uid = Calendars.getUid(component); if (uid != null) return uid.getValue(); return null; } catch (ConstraintViolationException e) { return null; } } protected static String[] getProjectionForEvent() { return new String[] { CalendarContract.Events._ID, // 00 CalendarContract.Events.CALENDAR_ID, // 01 CalendarContract.Events.ORGANIZER, // 02 CalendarContract.Events.TITLE, // 03 CalendarContract.Events.EVENT_LOCATION, // 04 CalendarContract.Events.DESCRIPTION, // 05 CalendarContract.Events.DTSTART, // 06 CalendarContract.Events.DTEND, // 07 CalendarContract.Events.EVENT_TIMEZONE, // 08 CalendarContract.Events.EVENT_END_TIMEZONE, // 09 CalendarContract.Events.DURATION, // 10 CalendarContract.Events.ALL_DAY, // 11 CalendarContract.Events.RRULE, // 12 CalendarContract.Events.RDATE, // 13 CalendarContract.Events.EXRULE, // 14 CalendarContract.Events.EXDATE, // 15 CalendarContract.Events.HAS_ALARM, // 16 CalendarContract.Events.HAS_ATTENDEE_DATA, // 17 CalendarContract.Events.ORIGINAL_ID, // 18 CalendarContract.Events.ORIGINAL_SYNC_ID, // 19 CalendarContract.Events.ORIGINAL_INSTANCE_TIME, // 20 CalendarContract.Events.ORIGINAL_ALL_DAY, // 21 CalendarContract.Events.AVAILABILITY, // 22 CalendarContract.Events.STATUS, // 23 COLUMN_NAME_EVENT_UID, // 24 COLUMN_NAME_EVENT_ETAG, // 25 COLUMN_NAME_COPIED_EVENT_ID // 26 }; } protected static ContentValues getValuesForEvent(Cursor cursor) { ContentValues values = new ContentValues(25); values.put(CalendarContract.Events._ID, cursor.getInt(0)); values.put(CalendarContract.Events.CALENDAR_ID, cursor.getInt(1)); values.put(CalendarContract.Events.ORGANIZER, cursor.getString(2)); values.put(CalendarContract.Events.TITLE, cursor.getString(3)); values.put(CalendarContract.Events.EVENT_LOCATION, cursor.getString(4)); values.put(CalendarContract.Events.DESCRIPTION, cursor.getString(5)); values.put(CalendarContract.Events.DTSTART, cursor.getLong(6)); values.put(CalendarContract.Events.DTEND, cursor.getLong(7)); values.put(CalendarContract.Events.EVENT_TIMEZONE, cursor.getString(8)); values.put(CalendarContract.Events.EVENT_END_TIMEZONE, cursor.getString(9)); values.put(CalendarContract.Events.DURATION, cursor.getString(10)); values.put(CalendarContract.Events.ALL_DAY, (cursor.getInt(11) != 0)); values.put(CalendarContract.Events.RRULE, cursor.getString(12)); values.put(CalendarContract.Events.RDATE, cursor.getString(13)); values.put(CalendarContract.Events.EXRULE, cursor.getString(14)); values.put(CalendarContract.Events.EXDATE, cursor.getString(15)); values.put(CalendarContract.Events.HAS_ALARM, (cursor.getInt(16) != 0)); values.put(CalendarContract.Events.HAS_ATTENDEE_DATA, (cursor.getInt(17) != 0)); values.put(CalendarContract.Events.ORIGINAL_ID, cursor.getLong(18)); values.put(CalendarContract.Events.ORIGINAL_SYNC_ID, cursor.getString(19)); values.put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, cursor.getLong(20)); values.put(CalendarContract.Events.ORIGINAL_ALL_DAY, (cursor.getInt(21) != 0)); values.put(CalendarContract.Events.AVAILABILITY, cursor.getInt(22)); values.put(CalendarContract.Events.STATUS, cursor.getInt(23)); values.put(COLUMN_NAME_EVENT_UID, cursor.getString(24)); values.put(COLUMN_NAME_EVENT_ETAG, cursor.getString(25)); values.put(COLUMN_NAME_COPIED_EVENT_ID, cursor.getLong(26)); return values; } protected static boolean isRecurrenceException(VEvent vEvent) { return vEvent.getProperty(EventFactory.PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID) != null; } protected static boolean isCopiedRecurrenceWithExceptions(VEvent vEvent) { return vEvent.getProperty(EventFactory.PROPERTY_NAME_FLOCK_COPY_EVENT_ID) != null; } protected static Optional<Long> getLocalIdFromCopiedRecurrenceWithExceptions(VEvent event) { if (!isCopiedRecurrenceWithExceptions(event)) return Optional.absent(); return Optional.of(Long.valueOf( event.getProperty(EventFactory.PROPERTY_NAME_FLOCK_COPY_EVENT_ID).getValue() )); } protected static void handleAttachPropertiesForCopiedRecurrenceWithExceptions(VEvent event, Long eventId) { event.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_COPY_EVENT_ID, String.valueOf(eventId))); String uid = UUID.randomUUID().toString(); Log.d(TAG, "setting uuid as " + uid); if (event.getUid() != null) event.getUid().setValue(uid); else event.getProperties().add(new Uid(uid)); } private static void handleAttachPropertiesForCopiedRecurrenceWithExceptions(ContentValues values, VEvent event) { Long copiedEventId = values.getAsLong(COLUMN_NAME_COPIED_EVENT_ID); if (copiedEventId != null && copiedEventId > 0) event.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_COPY_EVENT_ID, String.valueOf(copiedEventId))); } private static void handleAddValuesForRecurrenceProperties(VEvent event, ContentValues values) { Property copyIdProp = event.getProperty(PROPERTY_NAME_FLOCK_COPY_EVENT_ID); if (copyIdProp != null) values.put(COLUMN_NAME_COPIED_EVENT_ID, Long.valueOf(copyIdProp.getValue())); } protected static void handleReplaceOriginalSyncId(String path, String syncId, VEvent event) throws InvalidLocalComponentException { try { Property originalSyncIdProp = event.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID); if (originalSyncIdProp != null) originalSyncIdProp.setValue(syncId); else event.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID, syncId)); } catch (ParseException e) { throw new InvalidLocalComponentException("cmon now ical4j", CalDavConstants.CALDAV_NAMESPACE, path, getUid(event), e); } catch (URISyntaxException e) { throw new InvalidLocalComponentException("cmon now ical4j", CalDavConstants.CALDAV_NAMESPACE, path, getUid(event), e); } catch (IOException e) { throw new InvalidLocalComponentException("cmon now ical4j", CalDavConstants.CALDAV_NAMESPACE, path, getUid(event), e); } } private static void handleAddValuesForDeletionExceptionToRecurring(LocalEventCollection hack, VEvent vEvent, ContentValues eventValues) throws InvalidRemoteComponentException, RemoteException { Log.w(TAG, "gonna try and import deletion exception to androids recurrence model..."); Property originalSyncIdProp = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID); if (originalSyncIdProp == null || TextUtils.isEmpty(originalSyncIdProp.getValue())) throw new InvalidRemoteComponentException("original sync id prop required on recurring event deletion exceptions", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); if (vEvent.getStartDate() == null || vEvent.getStartDate().getDate() == null) throw new InvalidRemoteComponentException("deletion exception VEvents must have a start time", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); eventValues.put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, vEvent.getStartDate().getDate().getTime()); if (vEvent.getProperty(RRule.RRULE) == null && vEvent.getProperty(RRule.RDATE) == null && (vEvent.getEndDate() == null || vEvent.getEndDate().getDate() == null) && vEvent.getDuration() == null) { eventValues.put(CalendarContract.Events.ALL_DAY, 1); eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); } else if ((vEvent.getProperty(RRule.RRULE) != null || vEvent.getProperty(RRule.RDATE) != null) && vEvent.getDuration() == null) { eventValues.put(CalendarContract.Events.ALL_DAY, 1); eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); } Optional<Long> originalLocalId = hack.getLocalIdForUid(originalSyncIdProp.getValue()); if (!originalLocalId.isPresent()) { throw new InvalidRemoteComponentException("unable to build content values for recurrence deletion " + "exception, cannot find original event " + originalSyncIdProp.getValue() + " in collection", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); } eventValues.put(CalendarContract.Events.ORIGINAL_ID, originalLocalId.get()); eventValues.put(CalendarContract.Events.ORIGINAL_SYNC_ID, originalSyncIdProp.getValue()); eventValues.put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CANCELED); } private static void handleAddValuesForEditExceptionToRecurring(LocalEventCollection hack, VEvent vEvent, ContentValues eventValues) throws InvalidRemoteComponentException, RemoteException { Log.w(TAG, "gonna try and import edit exception to androids recurrence model..."); Property originalSyncIdProp = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID); Property originalInstanceTime = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME); if (originalSyncIdProp == null || TextUtils.isEmpty(originalSyncIdProp.getValue())) throw new InvalidRemoteComponentException("original sync id prop required on recurring event edit exceptions", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); if (originalInstanceTime == null || TextUtils.isEmpty(originalInstanceTime.getValue())) throw new InvalidRemoteComponentException("original instance time prop required on recurring event edit exceptions", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); eventValues.put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, Long.valueOf(originalInstanceTime.getValue())); if (vEvent.getProperty(RRule.RRULE) == null && vEvent.getProperty(RRule.RDATE) == null && (vEvent.getEndDate() == null || vEvent.getEndDate().getDate() == null) && vEvent.getDuration() == null) { eventValues.put(CalendarContract.Events.ALL_DAY, 1); eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); } else if ((vEvent.getProperty(RRule.RRULE) != null || vEvent.getProperty(RRule.RDATE) != null) && vEvent.getDuration() == null) { eventValues.put(CalendarContract.Events.ALL_DAY, 1); eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); } Optional<Long> originalLocalId = hack.getLocalIdForUid(originalSyncIdProp.getValue()); if (!originalLocalId.isPresent()) { throw new InvalidRemoteComponentException("unable to build content values for recurrence edit " + "exception, cannot find original event " + originalSyncIdProp.getValue() + " in collection", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); } eventValues.put(CalendarContract.Events.ORIGINAL_ID, originalLocalId.get()); eventValues.put(CalendarContract.Events.ORIGINAL_SYNC_ID, originalSyncIdProp.getValue()); } protected static ContentValues getValuesForEvent(LocalEventCollection hack, Long calendarId, ComponentETagPair<Calendar> component) throws InvalidRemoteComponentException, RemoteException { VEvent vEvent = (VEvent) component.getComponent().getComponent(VEvent.VEVENT); if (vEvent != null) { ContentValues values = new ContentValues(); handleAddValuesForRecurrenceProperties(vEvent, values); values.put(CalendarContract.Events.CALENDAR_ID, calendarId); if (vEvent.getUid() != null && vEvent.getUid().getValue() != null) values.put(COLUMN_NAME_EVENT_UID, vEvent.getUid().getValue()); else values.putNull(COLUMN_NAME_EVENT_UID); if (component.getETag().isPresent()) values.put(COLUMN_NAME_EVENT_ETAG, component.getETag().get()); DtStart dtStart = vEvent.getStartDate(); if (dtStart != null && dtStart.getDate() != null) { if (dtStart.getTimeZone() != null) values.put(CalendarContract.Events.EVENT_TIMEZONE, dtStart.getTimeZone().getID()); values.put(CalendarContract.Events.DTSTART, dtStart.getDate().getTime()); } else { Log.e(TAG, "no start date found on event"); throw new InvalidRemoteComponentException("no start date found on event", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(vEvent)); } Status status = vEvent.getStatus(); Property originalInstanceTimeProp = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME); if (status != null && status != Status.VEVENT_CANCELLED && originalInstanceTimeProp != null) handleAddValuesForEditExceptionToRecurring(hack, vEvent, values); if (status != null && status == Status.VEVENT_CONFIRMED) values.put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CONFIRMED); else if (status != null && status == Status.VEVENT_CANCELLED) handleAddValuesForDeletionExceptionToRecurring(hack, vEvent, values); else values.put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_TENTATIVE); Summary summary = vEvent.getSummary(); if (summary != null) values.put(CalendarContract.Events.TITLE, summary.getValue()); Location location = vEvent.getLocation(); if (location != null) values.put(CalendarContract.Events.EVENT_LOCATION, location.getValue()); Description description = vEvent.getDescription(); if (description != null) values.put(CalendarContract.Events.DESCRIPTION, description.getValue()); Transp transparency = vEvent.getTransparency(); if (transparency != null && transparency == Transp.OPAQUE) values.put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY); else values.put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_FREE); Organizer organizer = vEvent.getOrganizer(); if (organizer != null && organizer.getCalAddress() != null) { URI organizerAddress = organizer.getCalAddress(); if (organizerAddress.getScheme() != null && organizerAddress.getScheme().equalsIgnoreCase("mailto")) values.put(CalendarContract.Events.ORGANIZER, organizerAddress.getSchemeSpecificPart()); } RRule rRule = (RRule) vEvent.getProperty(RRule.RRULE); RDate rDate = (RDate) vEvent.getProperty(RDate.RDATE); ExRule exRule = (ExRule) vEvent.getProperty(ExRule.EXRULE); ExDate exDate = (ExDate) vEvent.getProperty(ExDate.EXDATE); if (rRule != null) values.put(CalendarContract.Events.RRULE, rRule.getValue()); if (rDate != null) values.put(CalendarContract.Events.RDATE, rDate.getValue()); if (exRule != null) values.put(CalendarContract.Events.EXRULE, exRule.getValue()); if (exDate != null) values.put(CalendarContract.Events.EXDATE, exDate.getValue()); if (vEvent.getProperty(PROPERTY_NAME_FLOCK_ALL_DAY) != null) values.put(CalendarContract.Events.ALL_DAY, 1); if (rRule == null && rDate == null) { DtEnd dtEnd = vEvent.getEndDate(); if (dtEnd != null && dtEnd.getDate() != null) { if (dtEnd.getTimeZone() != null) values.put(CalendarContract.Events.EVENT_TIMEZONE, dtEnd.getTimeZone().getID()); values.put(CalendarContract.Events.DTEND, dtEnd.getDate().getTime()); } else if (vEvent.getDuration() != null) { Duration duration = vEvent.getDuration(); java.util.Date endDate = duration.getDuration().getTime(dtStart.getDate()); values.put(CalendarContract.Events.DTEND, endDate.getTime()); } else { Log.w(TAG, "event is missing date end and duration, assuming all day event."); values.put(CalendarContract.Events.DTEND, dtStart.getDate().getTime() + DateUtils.DAY_IN_MILLIS); values.put(CalendarContract.Events.ALL_DAY, 1); } } else if (vEvent.getDuration() != null) values.put(CalendarContract.Events.DURATION, vEvent.getDuration().getValue()); PropertyList attendees = vEvent.getProperties(Attendee.ATTENDEE); if (attendees != null && attendees.size() > 0) values.put(CalendarContract.Events.HAS_ATTENDEE_DATA, 1); else values.put(CalendarContract.Events.HAS_ATTENDEE_DATA, 0); PropertyList alarms = vEvent.getProperties(VAlarm.VALARM); if (alarms != null && alarms.size() > 0) values.put(CalendarContract.Events.HAS_ALARM, 1); else values.put(CalendarContract.Events.HAS_ALARM, 0); return values; } Log.e(TAG, "no VEVENT found in component"); throw new InvalidRemoteComponentException("no VEVENT found in component", CalDavConstants.CALDAV_NAMESPACE, hack.getPath(), getUid(component.getComponent())); } private static void handleAddPropertiesForDeletionExceptionToRecurring(String path, ContentValues eventValues, VEvent vEvent) throws InvalidLocalComponentException { Log.w(TAG, "gonna try and export deletion exception from androids recurrence model..."); vEvent.getProperties().add(Status.VEVENT_CANCELLED); Long originalLocalId = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_ID); String originalSyncId = eventValues.getAsString(CalendarContract.Events.ORIGINAL_SYNC_ID); String syncId = eventValues.getAsString(COLUMN_NAME_EVENT_UID); if (TextUtils.isEmpty(originalSyncId)) throw new InvalidLocalComponentException("original sync id required on recurring event deletion exceptions", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent)); if (originalLocalId == null || originalLocalId < 1) throw new InvalidLocalComponentException("original local id required on recurring event deletion exceptions", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent)); if (vEvent.getUid() == null) vEvent.getProperties().add(new Uid(syncId)); else if (vEvent.getUid().getValue() == null) vEvent.getUid().setValue(syncId); XProperty originalSyncIdProp = new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID, originalSyncId); vEvent.getProperties().add(originalSyncIdProp); } private static void handleAddPropertiesForEditExceptionToRecurring(String path, ContentValues eventValues, VEvent vEvent) throws InvalidLocalComponentException { Log.w(TAG, "gonna try and export edit exception from androids recurrence model..."); Long originalLocalId = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_ID); String originalSyncId = eventValues.getAsString(CalendarContract.Events.ORIGINAL_SYNC_ID); Long originalInstanceTime = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_INSTANCE_TIME); String syncId = eventValues.getAsString(COLUMN_NAME_EVENT_UID); if (TextUtils.isEmpty(originalSyncId)) throw new InvalidLocalComponentException("original sync id required on recurring event edit exceptions", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent)); if (originalLocalId == null || originalLocalId < 1 || originalInstanceTime == null) throw new InvalidLocalComponentException("original local id and instance time required on recurring event edit exceptions", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent)); if (vEvent.getUid() == null) vEvent.getProperties().add(new Uid(syncId)); else if (vEvent.getUid().getValue() == null) vEvent.getUid().setValue(syncId); vEvent.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID, originalSyncId)); vEvent.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME, String.valueOf(originalInstanceTime))); } protected static ComponentETagPair<Calendar> getEventComponent(String path, ContentValues eventValues) throws InvalidLocalComponentException { Calendar calendar = new Calendar(); VEvent vEvent = new VEvent(); TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); calendar.getProperties().add(Version.VERSION_2_0); handleAttachPropertiesForCopiedRecurrenceWithExceptions(eventValues, vEvent); String uidText = eventValues.getAsString(COLUMN_NAME_EVENT_UID); if (!StringUtils.isEmpty(uidText)) { Uid eventUid = new Uid(uidText); vEvent.getProperties().add(eventUid); } try { String organizerText = eventValues.getAsString(CalendarContract.Events.ORGANIZER); if (StringUtils.isNotEmpty(organizerText)) { URI organizerEmail = new URI("mailto", organizerText, null); Organizer organizer = new Organizer(organizerEmail); vEvent.getProperties().add(organizer); } } catch (URISyntaxException e) { Log.e(TAG, "caught exception while parsing URI from organizerText", e); throw new InvalidLocalComponentException("caught exception while parsing URI from organizerText", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent), e); } String summaryText = eventValues.getAsString(CalendarContract.Events.TITLE); if (StringUtils.isNotEmpty(summaryText)) { Summary summary = new Summary(summaryText); vEvent.getProperties().add(summary); } String locationText = eventValues.getAsString(CalendarContract.Events.EVENT_LOCATION); if (StringUtils.isNotEmpty(locationText)) { Location location = new Location(locationText); vEvent.getProperties().add(location); } String descriptionText = eventValues.getAsString(CalendarContract.Events.DESCRIPTION); if (StringUtils.isNotEmpty(descriptionText)) { Description description = new Description(descriptionText); vEvent.getProperties().add(description); } Integer status = eventValues.getAsInteger(CalendarContract.Events.STATUS); Long originalInstanceTime = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_INSTANCE_TIME); if (status != null && status != CalendarContract.Events.STATUS_CANCELED && originalInstanceTime != null && originalInstanceTime > 0) { handleAddPropertiesForEditExceptionToRecurring(path, eventValues, vEvent); } if (status != null && status == CalendarContract.Events.STATUS_CONFIRMED) vEvent.getProperties().add(Status.VEVENT_CONFIRMED); else if (status != null && status == CalendarContract.Events.STATUS_CANCELED) handleAddPropertiesForDeletionExceptionToRecurring(path, eventValues, vEvent); else vEvent.getProperties().add(Status.VEVENT_TENTATIVE); Integer availability = eventValues.getAsInteger(CalendarContract.Events.AVAILABILITY); if (availability != null && availability == CalendarContract.Events.AVAILABILITY_BUSY) vEvent.getProperties().add(Transp.OPAQUE); else vEvent.getProperties().add(Transp.TRANSPARENT); Long dtStartMilliseconds = eventValues.getAsLong(CalendarContract.Events.DTSTART); if (dtStartMilliseconds == null) dtStartMilliseconds = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_INSTANCE_TIME); if (dtStartMilliseconds != null) { DtStart dtStart = new DtStart(new Date(dtStartMilliseconds)); String dtStartTZText = eventValues.getAsString(CalendarContract.Events.EVENT_TIMEZONE); if (dtStartTZText != null) { DateTime startDate = new DateTime(dtStartMilliseconds); TimeZone startTimeZone = registry.getTimeZone(dtStartTZText); startDate.setTimeZone(startTimeZone); dtStart = new DtStart(startDate); } vEvent.getProperties().add(dtStart); } else { Log.e(TAG, "no start date found on event"); throw new InvalidLocalComponentException("no start date found on event", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent)); } Boolean allDay = eventValues.getAsBoolean(CalendarContract.Events.ALL_DAY); Long dtEndMilliseconds = eventValues.getAsLong(CalendarContract.Events.DTEND); if (allDay) vEvent.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ALL_DAY, "true")); if (dtEndMilliseconds != null && dtEndMilliseconds > 0) { DtEnd dtEnd = new DtEnd(new Date(dtEndMilliseconds)); String dtStartTZText = eventValues.getAsString(CalendarContract.Events.EVENT_TIMEZONE); if (dtStartTZText != null) { DateTime endDate = new DateTime(dtEndMilliseconds); TimeZone endTimeZone = registry.getTimeZone(dtStartTZText); endDate.setTimeZone(endTimeZone); dtEnd = new DtEnd(endDate); } vEvent.getProperties().add(dtEnd); } String durationText = eventValues.getAsString(CalendarContract.Events.DURATION); if (StringUtils.isNotEmpty(durationText)) { Dur dur = new Dur(durationText); Duration duration = new Duration(dur); vEvent.getProperties().add(duration); } try { String rRuleText = eventValues.getAsString(CalendarContract.Events.RRULE); if (StringUtils.isNotEmpty(rRuleText)) { RRule rRule = new RRule(rRuleText); vEvent.getProperties().add(rRule); } String rDateText = eventValues.getAsString(CalendarContract.Events.RDATE); if (StringUtils.isNotEmpty(rDateText)) { RDate rDate = new RDate(); rDate.setValue(rDateText); vEvent.getProperties().add(rDate); } String exRuleText = eventValues.getAsString(CalendarContract.Events.EXRULE); if (StringUtils.isNotEmpty(exRuleText)) { ExRule exRule = new ExRule(); exRule.setValue(exRuleText); vEvent.getProperties().add(exRule); } String exDateText = eventValues.getAsString(CalendarContract.Events.EXDATE); if (StringUtils.isNotEmpty(exDateText)) { ExDate exDate = new ExDate(); exDate.setValue(exDateText); vEvent.getProperties().add(exDate); } } catch (ParseException e) { Log.e(TAG, "caught exception while parsing recurrence rule stuff from event values", e); throw new InvalidLocalComponentException("caught exception while parsing recurrence rule stuff from event values", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent), e); } calendar.getComponents().add(vEvent); Optional<String> eTag = Optional.fromNullable(eventValues.getAsString(COLUMN_NAME_EVENT_ETAG)); return new ComponentETagPair<Calendar>(calendar, eTag); } public static String[] getProjectionForAttendee() { return new String[] { CalendarContract.Attendees.EVENT_ID, // 00 CalendarContract.Attendees.ATTENDEE_EMAIL, // 01 CalendarContract.Attendees.ATTENDEE_NAME, // 02 CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, // 03 CalendarContract.Attendees.ATTENDEE_TYPE, // 04 CalendarContract.Attendees.ATTENDEE_STATUS, // 05 CalendarContract.Attendees.ATTENDEE_IDENTITY, // 06 CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE // 07 }; } public static ContentValues getValuesForAttendee(Cursor cursor) { ContentValues values = new ContentValues(8); values.put(CalendarContract.Attendees.EVENT_ID, cursor.getLong(0)); values.put(CalendarContract.Attendees.ATTENDEE_EMAIL, cursor.getString(1)); values.put(CalendarContract.Attendees.ATTENDEE_NAME, cursor.getString(2)); values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, cursor.getInt(3)); values.put(CalendarContract.Attendees.ATTENDEE_TYPE, cursor.getInt(4)); values.put(CalendarContract.Attendees.ATTENDEE_STATUS, cursor.getInt(5)); values.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, cursor.getString(6)); values.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, cursor.getString(7)); return values; } public static List<ContentValues> getValuesForAttendees(Calendar component) { List<ContentValues> valuesList = new LinkedList<ContentValues>(); VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); if (vEvent == null || vEvent.getProperties(Attendee.ATTENDEE) == null) return valuesList; PropertyList attendeeList = vEvent.getProperties(Attendee.ATTENDEE); for (int i = 0; i < attendeeList.size(); i++) { ContentValues values = new ContentValues(); Attendee attendee = (Attendee) attendeeList.get(i); if (attendee != null) { String email = Uri.parse(attendee.getValue()).getSchemeSpecificPart(); Cn name = (Cn) attendee.getParameter(Cn.CN); Role relationship = (Role) attendee.getParameter(Parameter.ROLE); PartStat status = (PartStat) attendee.getParameter(Parameter.PARTSTAT); if (StringUtils.isNotEmpty(email)) values.put(CalendarContract.Attendees.ATTENDEE_EMAIL, email); if (name != null) values.put(CalendarContract.Attendees.ATTENDEE_NAME, name.getValue()); if (relationship != null) { if (relationship == Role.CHAIR) { values.put(CalendarContract.Attendees.ATTENDEE_TYPE, CalendarContract.Attendees.TYPE_REQUIRED); values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); } else if (relationship == Role.REQ_PARTICIPANT) { values.put(CalendarContract.Attendees.ATTENDEE_TYPE, CalendarContract.Attendees.TYPE_REQUIRED); values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, CalendarContract.Attendees.RELATIONSHIP_ATTENDEE); } else { values.put(CalendarContract.Attendees.ATTENDEE_TYPE, CalendarContract.Attendees.TYPE_OPTIONAL); values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, CalendarContract.Attendees.RELATIONSHIP_ATTENDEE); } } else { values.put(CalendarContract.Attendees.ATTENDEE_TYPE, CalendarContract.Attendees.TYPE_OPTIONAL); values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, CalendarContract.Attendees.RELATIONSHIP_ATTENDEE); } if (status != null) { if (status == PartStat.NEEDS_ACTION) values.put(CalendarContract.Attendees.ATTENDEE_STATUS, CalendarContract.Attendees.ATTENDEE_STATUS_INVITED); else if (status == PartStat.TENTATIVE) values.put(CalendarContract.Attendees.ATTENDEE_STATUS, CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE); else if (status == PartStat.ACCEPTED) values.put(CalendarContract.Attendees.ATTENDEE_STATUS, CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED); else if (status == PartStat.DECLINED) values.put(CalendarContract.Attendees.ATTENDEE_STATUS, CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED); else values.put(CalendarContract.Attendees.ATTENDEE_STATUS, CalendarContract.Attendees.ATTENDEE_STATUS_NONE); } else values.put(CalendarContract.Attendees.ATTENDEE_STATUS, CalendarContract.Attendees.ATTENDEE_STATUS_NONE); valuesList.add(values); } } return valuesList; } public static void addAttendee(String path, Calendar component, ContentValues attendeeValues) throws InvalidLocalComponentException { VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); if (vEvent == null) { Log.e(TAG, "unable to add attendee to component with no VEVENT"); throw new InvalidLocalComponentException("unable to add attendee to component with no VEVENT", CalDavConstants.CALDAV_NAMESPACE, path, getUid(component)); } String email = attendeeValues.getAsString(CalendarContract.Attendees.ATTENDEE_EMAIL); String name = attendeeValues.getAsString(CalendarContract.Attendees.ATTENDEE_NAME); Integer type = attendeeValues.getAsInteger(CalendarContract.Attendees.ATTENDEE_TYPE); Integer relationship = attendeeValues.getAsInteger(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP); Integer status = attendeeValues.getAsInteger(CalendarContract.Attendees.ATTENDEE_STATUS); if (StringUtils.isEmpty(email)) { Log.w(TAG, "attendee email is null or empty, not going to add anything"); return; } try { Attendee attendee = new Attendee(new URI("mailto", email, null)); ParameterList attendeeParams = attendee.getParameters(); attendeeParams.add(CuType.INDIVIDUAL); if (StringUtils.isNotEmpty(name)) attendeeParams.add(new Cn(name)); if (relationship != null && relationship == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER) attendeeParams.add(Role.CHAIR); else if (type != null && type == CalendarContract.Attendees.TYPE_REQUIRED) attendeeParams.add(Role.REQ_PARTICIPANT); else attendeeParams.add(Role.OPT_PARTICIPANT); if (status != null) { switch (status) { case CalendarContract.Attendees.ATTENDEE_STATUS_INVITED: attendeeParams.add(PartStat.NEEDS_ACTION); break; case CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED: attendeeParams.add(PartStat.ACCEPTED); break; case CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED: attendeeParams.add(PartStat.DECLINED); break; case CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE: attendeeParams.add(PartStat.TENTATIVE); break; } } vEvent.getProperties().add(attendee); } catch (URISyntaxException e) { Log.e(TAG, "caught exception while adding email to attendee", e); throw new InvalidLocalComponentException("caught exception while adding email to attendee", CalDavConstants.CALDAV_NAMESPACE, path, getUid(vEvent), e); } } public static String [] getProjectionForReminder() { return new String[] { CalendarContract.Reminders.EVENT_ID, // 00 CalendarContract.Reminders.MINUTES, // 01 CalendarContract.Reminders.METHOD // 02 }; } public static Optional<ContentValues> getValuesForReminder(Cursor cursor) { if (cursor.isNull(0) || cursor.isNull(1)) return Optional.absent(); ContentValues values = new ContentValues(3); values.put(CalendarContract.Reminders.EVENT_ID, cursor.getLong(0)); values.put(CalendarContract.Reminders.MINUTES, cursor.getInt(1)); values.put(CalendarContract.Reminders.METHOD, cursor.getInt(2)); return Optional.of(values); } public static List<ContentValues> getValuesForReminders(Calendar component) { List<ContentValues> valueList = new LinkedList<ContentValues>(); VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); VToDo vToDo = (VToDo) component.getComponent(VToDo.VTODO); ComponentList vAlarms; if (vEvent != null) vAlarms = vEvent.getAlarms(); else if (vToDo != null) vAlarms = vToDo.getAlarms(); else return valueList; for (int i = 0; i < vAlarms.size(); i++) { VAlarm vAlarm = (VAlarm) vAlarms.get(i); if (vAlarm != null) { Trigger trigger = vAlarm.getTrigger(); if (trigger != null && trigger.getDuration() != null) { ContentValues values = new ContentValues(); values.put(CalendarContract.Reminders.MINUTES, (trigger.getDuration().getMinutes())); values.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT); valueList.add(values); } } } return valueList; } // TODO: can we support more alarm types? public static void addReminder(Calendar component, ContentValues reminderValues) { Integer minutes = reminderValues.getAsInteger(CalendarContract.Reminders.MINUTES); if (minutes == null) { Log.w(TAG, "reminder minutes is null, nothing to add."); return; } VAlarm vAlarm = new VAlarm(new Dur(0, 0, -minutes, 0)); PropertyList alarmProps = vAlarm.getProperties(); alarmProps.add(Action.DISPLAY); VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); VToDo vToDo = (VToDo) component.getComponent(VEvent.VTODO); if (vEvent != null) { if (vEvent.getSummary() != null) alarmProps.add(new Description(vEvent.getSummary().getValue())); vEvent.getAlarms().add(vAlarm); } else if (vToDo != null) { if (vToDo.getSummary() != null) alarmProps.add(new Description(vToDo.getSummary().getValue())); vToDo.getAlarms().add(vAlarm); } } }