Java tutorial
/* * Copyright 2013 2015 Ricki Hirner (bitfire web engineering). * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html */ package com.granita.icloudcalsync.resource; import android.accounts.Account; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentProviderOperation.Builder; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Entity; import android.content.EntityIterator; import android.database.Cursor; import android.database.DatabaseUtils; import android.net.Uri; import android.os.Build; import android.os.RemoteException; import android.provider.CalendarContract; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Reminders; 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; import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.component.VAlarm; 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.DateListProperty; import net.fortuna.ical4j.model.property.Description; 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.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; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; import java.util.LinkedList; import java.util.List; import com.granita.icloudcalsync.DAVUtils; import com.granita.icloudcalsync.DateUtils; import lombok.Cleanup; import lombok.Getter; import lombok.Setter; /** * Represents a locally stored calendar, containing Events. * Communicates with the Android Contacts Provider which uses an SQLite * database to store the contacts. */ public class LocalCalendar extends LocalCollection<Event> { private static final String TAG = "davdroid.LocalCalendar"; @Getter protected long id; //custom code start @Setter @Getter protected String url; //custom code end protected static String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1; /* database fields */ @Override protected Uri entriesURI() { return syncAdapterURI(Events.CONTENT_URI); } @Override protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; } @Override protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; } @Override protected String entryColumnParentID() { return Events.CALENDAR_ID; } @Override protected String entryColumnID() { return Events._ID; } @Override protected String entryColumnRemoteName() { return Events._SYNC_ID; } @Override protected String entryColumnETag() { return Events.SYNC_DATA1; } @Override protected String entryColumnDirty() { return Events.DIRTY; } @Override protected String entryColumnDeleted() { return Events.DELETED; } @Override @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) protected String entryColumnUID() { return (android.os.Build.VERSION.SDK_INT >= 17) ? Events.UID_2445 : Events.SYNC_DATA2; } /* class methods, constructor */ @SuppressLint("InlinedApi") public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException { final ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY); if (client == null) throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)"); ContentValues values = new ContentValues(); values.put(Calendars.ACCOUNT_NAME, account.name); values.put(Calendars.ACCOUNT_TYPE, account.type); values.put(Calendars.NAME, info.getURL()); values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle()); values.put(Calendars.CALENDAR_COLOR, DAVUtils.CalDAVtoARGBColor(info.getColor())); values.put(Calendars.OWNER_ACCOUNT, account.name); values.put(Calendars.SYNC_EVENTS, 1); values.put(Calendars.VISIBLE, 1); values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT); 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_ORGANIZER_RESPOND, 1); values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1); } if (android.os.Build.VERSION.SDK_INT >= 15) { values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE); values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE); } if (info.getTimezone() != null) values.put(Calendars.CALENDAR_TIME_ZONE, info.getTimezone()); Log.i(TAG, "Inserting calendar: " + values.toString()); try { return client.insert(calendarsURI(account), values); } catch (RemoteException e) { throw new LocalStorageException(e); } } public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException { @Cleanup Cursor cursor = providerClient.query(calendarsURI(account), new String[] { Calendars._ID, Calendars.NAME }, Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null); LinkedList<LocalCalendar> calendars = new LinkedList<>(); while (cursor != null && cursor.moveToNext()) calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1))); return calendars.toArray(new LocalCalendar[0]); } public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) throws RemoteException { super(account, providerClient); this.id = id; this.url = url; sqlFilter = "ORIGINAL_ID IS NULL"; } /* collection operations */ @Override public String getCTag() throws LocalStorageException { try { @Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id), new String[] { COLLECTION_COLUMN_CTAG }, null, null, null); if (c != null && c.moveToFirst()) return c.getString(0); else throw new LocalStorageException("Couldn't query calendar CTag"); } catch (RemoteException e) { throw new LocalStorageException(e); } } @Override public void setCTag(String cTag) throws LocalStorageException { ContentValues values = new ContentValues(1); values.put(COLLECTION_COLUMN_CTAG, cTag); try { providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null); } catch (RemoteException e) { throw new LocalStorageException(e); } } @Override public long[] findUpdated() throws LocalStorageException { // 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))"; ContentValues dirty = new ContentValues(1); dirty.put(CalendarContract.Events.DIRTY, 1); try { 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); } // new find and return updated (master) events return super.findUpdated(); } /* create/update/delete */ public Event newResource(long localID, String resourceName, String eTag) { return new Event(localID, resourceName, eTag); } public void deleteAllExceptRemoteNames(Resource[] remoteResources) { List<String> sqlFileNames = new LinkedList<>(); for (Resource res : remoteResources) sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName())); // delete master events String where = entryColumnParentID() + "=?"; where += sqlFileNames.isEmpty() ? " AND " + entryColumnRemoteName() + " IS NOT NULL" : // don't retain anything (delete all) " AND " + entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name if (sqlFilter != null) where += " AND (" + sqlFilter + ")"; pendingOperations.add(ContentProviderOperation.newDelete(entriesURI()) .withSelection(where, new String[] { String.valueOf(id) }).build()); // delete exceptions, too where = entryColumnParentID() + "=?"; where += sqlFileNames.isEmpty() ? " AND " + Events.ORIGINAL_SYNC_ID + " IS NOT NULL" : // don't retain anything (delete all) " AND " + Events.ORIGINAL_SYNC_ID + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name pendingOperations.add(ContentProviderOperation.newDelete(entriesURI()) .withSelection(where, new String[] { String.valueOf(id) }).withYieldAllowed(true).build()); } @Override public void delete(Resource resource) { super.delete(resource); // delete all exceptions of this event, too pendingOperations.add(ContentProviderOperation.newDelete(entriesURI()) .withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(resource.getLocalID()) }) .build()); } @Override public void clearDirty(Resource resource) { super.clearDirty(resource); // clear dirty flag of all exceptions of this event pendingOperations.add(ContentProviderOperation.newUpdate(entriesURI()).withValue(Events.DIRTY, 0) .withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(resource.getLocalID()) }) .build()); } /* methods for populating the data object from the content provider */ @Override public void populate(Resource resource) throws LocalStorageException { Event event = (Event) resource; try { @Cleanup EntityIterator iterEvents = CalendarContract.EventsEntity.newEntityIterator( providerClient.query(syncAdapterURI(CalendarContract.EventsEntity.CONTENT_URI), null, Events._ID + "=" + event.getLocalID(), null, null), providerClient); while (iterEvents.hasNext()) { Entity e = iterEvents.next(); ContentValues values = e.getEntityValues(); populateEvent(event, values); List<Entity.NamedContentValues> subValues = e.getSubValues(); for (Entity.NamedContentValues subValue : subValues) { values = subValue.values; if (Attendees.CONTENT_URI.equals(subValue.uri)) populateAttendee(event, values); if (Reminders.CONTENT_URI.equals(subValue.uri)) populateReminder(event, values); } populateExceptions(event); } } catch (RemoteException ex) { throw new LocalStorageException("Couldn't process locally stored event", ex); } } protected void populateEvent(Event e, ContentValues values) { e.setUid(values.getAsString(entryColumnUID())); e.setSummary(values.getAsString(Events.TITLE)); e.setLocation(values.getAsString(Events.EVENT_LOCATION)); e.setDescription(values.getAsString(Events.DESCRIPTION)); final boolean allDay = values.getAsInteger(Events.ALL_DAY) != 0; final long tsStart = values.getAsLong(Events.DTSTART); final String duration = values.getAsString(Events.DURATION); String tzId = null; Long tsEnd = values.getAsLong(Events.DTEND); if (allDay) { e.setDtStart(tsStart, null); if (tsEnd == null) { Dur dur = new Dur(duration); java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart)); tsEnd = dEnd.getTime(); } e.setDtEnd(tsEnd, null); } else { // use the start time zone for the end time, too // because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only tzId = values.getAsString(Events.EVENT_TIMEZONE); e.setDtStart(tsStart, tzId); if (tsEnd != null) e.setDtEnd(tsEnd, tzId); else if (!StringUtils.isEmpty(duration)) e.setDuration(new Duration(new Dur(duration))); } // recurrence try { String strRRule = values.getAsString(Events.RRULE); if (!StringUtils.isEmpty(strRRule)) e.setRrule(new RRule(strRRule)); String strRDate = values.getAsString(Events.RDATE); if (!StringUtils.isEmpty(strRDate)) { RDate rDate = new RDate(); rDate.setValue(strRDate); e.getRdates().add(rDate); } String strExRule = values.getAsString(Events.EXRULE); if (!StringUtils.isEmpty(strExRule)) { ExRule exRule = new ExRule(); exRule.setValue(strExRule); e.setExrule(exRule); } String strExDate = values.getAsString(Events.EXDATE); if (!StringUtils.isEmpty(strExDate)) { // always empty, see https://code.google.com/p/android/issues/detail?id=172411 ExDate exDate = new ExDate(); exDate.setValue(strExDate); e.getExdates().add(exDate); } } catch (ParseException ex) { Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex); } catch (IllegalArgumentException ex) { Log.w(TAG, "Invalid recurrence rules, ignoring", ex); } if (values.containsKey(Events.ORIGINAL_INSTANCE_TIME)) { // this event is an exception of a recurring event long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); boolean originalAllDay = values.getAsInteger(Events.ORIGINAL_ALL_DAY) != 0; Date originalDate = originalAllDay ? new Date(originalInstanceTime) : new DateTime(originalInstanceTime); if (originalDate instanceof DateTime) ((DateTime) originalDate).setUtc(true); e.setRecurrenceId(new RecurrenceId(originalDate)); } // status if (values.containsKey(Events.STATUS)) switch (values.getAsInteger(Events.STATUS)) { case Events.STATUS_CONFIRMED: e.setStatus(Status.VEVENT_CONFIRMED); break; case Events.STATUS_TENTATIVE: e.setStatus(Status.VEVENT_TENTATIVE); break; case Events.STATUS_CANCELED: e.setStatus(Status.VEVENT_CANCELLED); } // availability e.setOpaque(values.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE); // set ORGANIZER only when there are attendees if (values.getAsInteger(Events.HAS_ATTENDEE_DATA) != 0 && values.containsKey(Events.ORGANIZER)) try { e.setOrganizer(new Organizer(new URI("mailto", values.getAsString(Events.ORGANIZER), null))); } catch (URISyntaxException ex) { Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex); } // classification switch (values.getAsInteger(Events.ACCESS_LEVEL)) { case Events.ACCESS_CONFIDENTIAL: case Events.ACCESS_PRIVATE: e.setForPublic(false); break; case Events.ACCESS_PUBLIC: e.setForPublic(true); } } void populateExceptions(Event e) throws RemoteException { @Cleanup Cursor c = providerClient.query(syncAdapterURI(Events.CONTENT_URI), new String[] { 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); 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 populateAttendee(Event event, ContentValues values) throws RemoteException { try { Attendee attendee = new Attendee(new URI("mailto", values.getAsString(Attendees.ATTENDEE_EMAIL), null)); ParameterList params = attendee.getParameters(); String cn = values.getAsString(Attendees.ATTENDEE_NAME); if (cn != null) params.add(new Cn(cn)); // type int type = values.getAsInteger(Attendees.ATTENDEE_TYPE); params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL); // role int relationship = values.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); switch (relationship) { case Attendees.RELATIONSHIP_ORGANIZER: params.add(Role.CHAIR); break; case Attendees.RELATIONSHIP_ATTENDEE: case Attendees.RELATIONSHIP_PERFORMER: case Attendees.RELATIONSHIP_SPEAKER: params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT); break; case Attendees.RELATIONSHIP_NONE: params.add(Role.NON_PARTICIPANT); } // status switch (values.getAsInteger(Attendees.ATTENDEE_STATUS)) { case Attendees.ATTENDEE_STATUS_INVITED: params.add(PartStat.NEEDS_ACTION); break; case Attendees.ATTENDEE_STATUS_ACCEPTED: params.add(PartStat.ACCEPTED); break; case Attendees.ATTENDEE_STATUS_DECLINED: params.add(PartStat.DECLINED); break; case Attendees.ATTENDEE_STATUS_TENTATIVE: params.add(PartStat.TENTATIVE); break; } event.getAttendees().add(attendee); } catch (URISyntaxException ex) { Log.e(TAG, "Couldn't parse attendee information, ignoring", ex); } } void populateReminder(Event event, ContentValues row) throws RemoteException { VAlarm alarm = new VAlarm(new Dur(0, 0, -row.getAsInteger(Reminders.MINUTES), 0)); PropertyList props = alarm.getProperties(); props.add(Action.DISPLAY); props.add(new Description(event.getSummary())); event.getAlarms().add(alarm); } /* content builder methods */ @Override protected Builder buildEntry(Builder builder, Resource resource, boolean update) { final Event event = (Event) resource; builder.withValue(Events.CALENDAR_ID, id).withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0) .withValue(Events.DTSTART, event.getDtStartInMillis()) .withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID()) .withValue(Events.HAS_ALARM, event.getAlarms().isEmpty() ? 0 : 1) .withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1) .withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1).withValue(Events.GUESTS_CAN_MODIFY, 1) .withValue(Events.GUESTS_CAN_SEE_GUESTS, 1); final RecurrenceId recurrenceId = event.getRecurrenceId(); if (recurrenceId == null) { // this event is a "master event" (not an exception) builder.withValue(entryColumnRemoteName(), event.getName()) .withValue(entryColumnETag(), event.getETag()).withValue(entryColumnUID(), event.getUid()); } else { 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; builder.withValue(Events.RRULE, event.getRrule().getValue()); } if (!event.getRdates().isEmpty()) { recurring = true; builder.withValue(Events.RDATE, recurrenceSetsToAndroidString(event.getRdates())); } if (event.getExrule() != null) builder.withValue(Events.EXRULE, event.getExrule().getValue()); if (!event.getExdates().isEmpty()) builder.withValue(Events.EXDATE, recurrenceSetsToAndroidString(event.getExdates())); // set either DTEND for single-time events or DURATION for recurring events // because that's the way Android likes it (see docs) if (recurring) { // calculate DURATION from start and end date Duration duration = new Duration(event.getDtStart().getDate(), event.getDtEnd().getDate()); builder.withValue(Events.DURATION, duration.getValue()); } else builder.withValue(Events.DTEND, event.getDtEndInMillis()).withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID()); if (event.getSummary() != null) builder.withValue(Events.TITLE, event.getSummary()); if (event.getLocation() != null) builder.withValue(Events.EVENT_LOCATION, event.getLocation()); if (event.getDescription() != null) builder.withValue(Events.DESCRIPTION, event.getDescription()); if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) { URI organizer = event.getOrganizer().getCalAddress(); if (organizer.getScheme() != null && organizer.getScheme().equalsIgnoreCase("mailto")) builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart()); } Status status = event.getStatus(); if (status != null) { int statusCode = Events.STATUS_TENTATIVE; if (status == Status.VEVENT_CONFIRMED) statusCode = Events.STATUS_CONFIRMED; else if (status == Status.VEVENT_CANCELLED) statusCode = Events.STATUS_CANCELED; builder.withValue(Events.STATUS, statusCode); } builder.withValue(Events.AVAILABILITY, event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE); if (event.getForPublic() != null) builder.withValue(Events.ACCESS_LEVEL, event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE); return builder; } @Override protected void addDataRows(Resource resource, long localID, int backrefIdx) { final 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()); } @Override protected void removeDataRows(Resource resource) { final Event event = (Event) resource; // delete exceptions pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Events.CONTENT_URI)) .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, false); 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. final RecurrenceId recurrenceId = exception.getRecurrenceId(); final boolean originalAllDay = master.isAllDay(); Date date = recurrenceId.getDate(); 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); } } builder.withValue(Events.ORIGINAL_INSTANCE_TIME, date.getTime()); builder.withValue(Events.ORIGINAL_ALL_DAY, originalAllDay ? 1 : 0); return builder; } @SuppressLint("InlinedApi") protected Builder buildAttendee(Builder builder, Attendee attendee) { final Uri member = Uri.parse(attendee.getValue()); final String email = member.getSchemeSpecificPart(); final Cn cn = (Cn) attendee.getParameter(Parameter.CN); if (cn != null) builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue()); int type = Attendees.TYPE_NONE; CuType cutype = (CuType) attendee.getParameter(Parameter.CUTYPE); if (cutype == CuType.RESOURCE) type = Attendees.TYPE_RESOURCE; else { Role role = (Role) attendee.getParameter(Parameter.ROLE); int relationship; if (role == Role.CHAIR) relationship = Attendees.RELATIONSHIP_ORGANIZER; else { relationship = Attendees.RELATIONSHIP_ATTENDEE; if (role == Role.OPT_PARTICIPANT) type = Attendees.TYPE_OPTIONAL; else if (role == Role.REQ_PARTICIPANT) type = Attendees.TYPE_REQUIRED; } builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship); } int status = Attendees.ATTENDEE_STATUS_NONE; PartStat partStat = (PartStat) attendee.getParameter(Parameter.PARTSTAT); if (partStat == null || partStat == PartStat.NEEDS_ACTION) status = Attendees.ATTENDEE_STATUS_INVITED; else if (partStat == PartStat.ACCEPTED) status = Attendees.ATTENDEE_STATUS_ACCEPTED; else if (partStat == PartStat.DECLINED) status = Attendees.ATTENDEE_STATUS_DECLINED; else if (partStat == PartStat.TENTATIVE) status = Attendees.ATTENDEE_STATUS_TENTATIVE; return builder.withValue(Attendees.ATTENDEE_EMAIL, email).withValue(Attendees.ATTENDEE_TYPE, type) .withValue(Attendees.ATTENDEE_STATUS, status); } protected Builder buildReminder(Builder builder, VAlarm alarm) { int minutes = 0; if (alarm.getTrigger() != null) { Dur duration = alarm.getTrigger().getDuration(); if (duration != null) { // negative value in TRIGGER means positive value in Reminders.MINUTES and vice versa minutes = -(((duration.getWeeks() * 7 + duration.getDays()) * 24 + duration.getHours()) * 60 + duration.getMinutes()); if (duration.isNegative()) minutes *= -1; } } Log.d(TAG, "Adding alarm " + minutes + " minutes before"); return builder.withValue(Reminders.METHOD, Reminders.METHOD_ALERT).withValue(Reminders.MINUTES, minutes); } /* private helper methods */ protected static Uri calendarsURI(Account account) { return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build(); } protected Uri calendarsURI() { return calendarsURI(account); } /** * Concatenates, if necessary, multiple RDATE/EXDATE lists and prepares * a formatted string as expected by Android calendar provider * @param dates one more more lists of RDATE or EXDATE * @return formatted string for Android calendar provider */ static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates) { String tzID = null; List<String> strDates = new LinkedList<String>(); for (DateListProperty dateList : dates) { if (dateList.getTimeZone() != null) { String thisTzID = DateUtils.findAndroidTimezoneID(dateList.getTimeZone().getID()); if (tzID == null) tzID = thisTzID; else if (!tzID.equals(thisTzID)) Log.w(TAG, "Multiple EXDATEs/RDATEs with different time zones not supported by Android, using " + tzID + " for all dates"); } strDates.add(dateList.getValue()); } // Android expects this format: "[TZID;]date1,date2,date3" String dateStr = ""; if (tzID != null) dateStr += tzID + ";"; dateStr += StringUtils.join(strDates, ","); return dateStr; } }