/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2010 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * 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 Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package de.chbosync.android.syncmlclient.source.pim.calendar; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.TimeZone; import java.util.Vector; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.CalendarContract; import android.provider.CalendarContract.Calendars; import android.text.format.Time; import com.funambol.client.source.AppSyncSource; import com.funambol.common.pim.icalendar.ICalendarSyntaxParser; import com.funambol.common.pim.model.calendar.Event; import com.funambol.common.pim.model.calendar.ExceptionToRecurrenceRule; import com.funambol.common.pim.model.calendar.RecurrencePattern; import com.funambol.common.pim.model.calendar.Reminder; import com.funambol.common.pim.model.common.Property; import com.funambol.common.pim.model.common.PropertyWithTimeZone; import com.funambol.common.pim.model.converter.ConverterException; import com.funambol.common.pim.model.converter.VCalendarConverter; import com.funambol.common.pim.model.icalendar.ICalendarSyntaxParserListenerImpl; import com.funambol.common.pim.model.model.VCalendar; import com.funambol.common.pim.model.utility.TimeUtils; import com.funambol.common.pim.vcalendar.CalendarUtils; import com.funambol.util.DateUtil; import com.funambol.util.Log; import com.funambol.util.StringUtil; import de.chbosync.android.syncmlclient.source.AbstractDataManager; /** * AbstractDataManager for the calendars entries: does the effective events' * addition, modifications and deletion. */ public class CalendarManager extends AbstractDataManager<Calendar> { private static final String LEGACY_ACCESS_LEVEL_STRING_PRIVATE = "PRIVATE"; private static final String LEGACY_ACCESS_LEVEL_STRING_PUBLIC = "PUBLIC"; /** Log entries tag */ private static final String TAG_LOG = "CalendarManager"; private static final int MAX_OPS_PER_BATCH = 499; private static final int COMMIT_THRESHOLD = MAX_OPS_PER_BATCH - 20; private AppSyncSource appSource = null; private int lastEventBackRef = -1; private ArrayList<ContentProviderOperation> ops = null; private Vector<String> newKeys = null; private List<Integer> eventsIdx = null; /** * Default constructor. * @param context the Context object * @param appSource the AppSyncSource object to be related to this manager */ public CalendarManager(Context context, AppSyncSource appSource) { super(context); this.appSource = appSource; } @Override public void beginTransaction() { ops = new ArrayList<ContentProviderOperation>(); eventsIdx = new ArrayList<Integer>(); newKeys = new Vector<String>(); } /** * Load a particular calendar entry * @param key the long formatted entry key to load * @return Calendar the Calendar object related to that entry * @throws IOException if anything went wrong accessing the calendar db */ public Calendar load(String key) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Loading Event: " + key); } long id; try { id = Long.parseLong(key); } catch (Exception e) { Log.error(TAG_LOG, "Invalid key: " + key, e); throw new IOException("Invalid key: " + key); } Calendar cal = new Calendar(); cal.setId(id); Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id); Cursor cursor = resolver.query(uri, null, null, null, null); try { if(cursor != null && cursor.moveToFirst()) { loadCalendarFields(cursor, cal, id); } else { // Item not found throw new IOException("Cannot find event " + key); } } finally { cursor.close(); } return cal; } public Calendar load(Cursor cursor) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Loading event from cursor"); } Calendar cal = new Calendar(); String key = cursor.getString(cursor.getColumnIndexOrThrow(CalendarContract.Events._ID)); Long k = Long.parseLong(key); loadCalendarFields(cursor, cal, k); return cal; } /** * Add a Calendar item to the db. This operation does not actually create the event, but it setup things * so that the event will be created during the commit. This call must be encapsulated into a {@link * beginTransaction} and {@link commit}. * * @param item the Calendar object to be added * @return null as the event id is unknown until the event is actually created. * @throws IOException if anything went wrong accessing the calendar db */ @Override public String add(Calendar item) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Adding Event"); } // Commit if it is time to do it if (ops.size() >= COMMIT_THRESHOLD) { commitSingleBatch(); } Event event = item.getEvent(); ContentValues cv = createEventContentValues(event); Uri uri = asSyncAdapter(CalendarContract.Events.CONTENT_URI, this.accountName, this.accountType); ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(uri); builder.withValues(cv); ops.add(builder.build()); lastEventBackRef = ops.size() - 1; eventsIdx.add(Integer.valueOf(lastEventBackRef)); addReminders(item, -1); return null; } /** * Update a calendar item in the db. This operation does not actually modify the event, but it setup things * so that the event will be modified during the commit. This call must be encapsulated into a {@link * beginTransaction} and {@link commit}. * * @param id the calendar key that represents the calendar to be updated * @param newItem the Calendar object taht must replace the existing one * @throws IOException if anything went wrong accessing the calendar db */ @Override public void update(String key, Calendar newItem) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Updating event: " + key); } // Commit if it is time to do it if (ops.size() >= COMMIT_THRESHOLD) { commitSingleBatch(); } long id; try { id = Long.parseLong(key); } catch(Exception e) { Log.error(TAG_LOG, "Invalid item key " + key, e); throw new IOException("Invalid item key"); } // If the contact does not exist, then we perform an add if (!exists(key)) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Tried to update a non existing event. Creating a new one "); } add(newItem); return; } Event event = newItem.getEvent(); ContentValues cv = createEventContentValues(event); Uri uri = asSyncAdapter(CalendarContract.Events.CONTENT_URI, this.accountName, this.accountType); Uri eventUri = ContentUris.withAppendedId(uri, id); ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(eventUri); builder.withValues(cv); ops.add(builder.build()); // For reminders we remove the old ones and add the new one deleteRemindersForEvent(id, false); addReminders(newItem, id); } /** * Delete a calendar item in the db. This operation does not actually remove the event, but it setup things * so that the event will be removed during the commit. This call must be encapsulated into a {@link * beginTransaction} and {@link commit}. * * @param id the calendar key that must be deleted * @throws IOException if anything went wrong accessing the calendar db */ @Override public void delete(String key) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Deleting event with id: " + key); } // Commit if it is time to do it if (ops.size() >= COMMIT_THRESHOLD) { commitSingleBatch(); } long itemId; try { itemId = Long.parseLong(key); } catch (Exception e) { Log.error(TAG_LOG, "Invalid item key " + key, e); throw new IOException("Invalid item key"); } Uri uri = asSyncAdapter(CalendarContract.Events.CONTENT_URI, this.accountName, this.accountType); Uri eventUri = ContentUris.withAppendedId(uri, itemId); ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete(eventUri); ops.add(builder.build()); deleteRemindersForEvent(itemId, false); } /** * Delete all calendars from the calendar db. This operation does not need to be encapsulated into * a transaction as it begins/commit automatically. * @throws IOException if anything went wrong accessing the calendar db */ @Override public void deleteAll() throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Deleting all events"); } Enumeration<?> keys = getAllKeys(); beginTransaction(); while(keys.hasMoreElements()) { String key = (String)keys.nextElement(); delete(key); // Delete all reminders associated to this item long id = Long.parseLong(key); deleteRemindersForEvent(id, false); } commit(); } @Override public Vector<String> commit() throws IOException { commitSingleBatch(); return newKeys; } private void commitSingleBatch() throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "commitSingleBatch " + ops.size()); } // Now perform all the operations in one shot try { if (ops.size() > 0) { ContentProviderResult[] res = resolver.applyBatch(getAuthority(), ops); for(int i=0;i<eventsIdx.size();++i) { int idx = eventsIdx.get(i).intValue(); if (res[idx].uri == null) { // This item was not properly inserted. Mark this as an error // (A zero length key is the marker) if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Cannot find uri for inserted item, will be marked as failed"); } newKeys.addElement(""); continue; } long id = ContentUris.parseId(res[idx].uri); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "The new contact has id: " + id); } newKeys.addElement("" + id); } } } catch (Exception e) { Log.error(TAG_LOG, "Cannot commit to database", e); throw new IOException("Cannot create event in db"); } finally { ops.clear(); eventsIdx.clear(); } } /** * Check if a calendar with the given id exists in the calendar db * @param id the id which existence is to be checked * @return true if the given id exists in the db false otherwise */ @Override public boolean exists(String key) { long id; try { id = Long.parseLong(key); } catch (Exception e) { Log.error(TAG_LOG, "Invalid item key " + key, e); return false; } Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id); Cursor cur = resolver.query(uri, null, null, null, null); if(cur == null) { return false; } boolean found = cur.getCount() > 0; cur.close(); return found; } /** * Get all of the calendar keys that exist into the DB * @return Enumeration the enumeration object that contains alll of the * calendar keys * @throws IOException if anything went wrong accessing the calendar db */ public Enumeration getAllKeys() throws IOException { CalendarAppSyncSourceConfig config = (CalendarAppSyncSourceConfig)appSource.getConfig(); if (config.getCalendarId() == -1) { throw new IOException("Cannot access undefined calendar"); } String cols[] = {CalendarContract.Events._ID}; Cursor cursor = resolver.query(CalendarContract.Events.CONTENT_URI, cols, CalendarContract.Events.CALENDAR_ID + "='" + config.getCalendarId() + "'", null, null); // The cursor can only be null if the content URI is not correct if (cursor == null) { Log.error(TAG_LOG, "query returned null, probably the content uri is wrong on this device"); throw new IOException("Cannot find content provider " + CalendarContract.Events.CONTENT_URI); } try { int size = cursor.getCount(); Vector<String> itemKeys = new Vector<String>(size); if (!cursor.moveToFirst()) { return itemKeys.elements(); } for (int i = 0; i < size; i++) { String key = cursor.getString(0); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Found item with key: " + key); } itemKeys.addElement(key); cursor.moveToNext(); } return itemKeys.elements(); } catch (Exception e) { Log.error(TAG_LOG, "Cannot get all items keys: ", e); throw new IOException("Cannot get all items keys"); } finally { cursor.close(); } } public Vector<com.funambol.syncml.protocol.Property> getSupportedProperties() { // TODO: FIXME return null; } private void loadCalendarFields(Cursor cursor, Calendar cal, long key) { Event event = new Event(); // Load SUMMARY String summary = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.TITLE)); if(summary != null) { event.setSummary(new Property(summary)); } // Load DESCRIPTION String description = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DESCRIPTION)); if(description != null) { event.setDescription(new Property(description)); } // Load LOCATION String location = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_LOCATION)); if(location != null) { event.setLocation(new Property(location)); } // Load TIMEZONE String tz = cursor.getString(cursor.getColumnIndexOrThrow(CalendarContract.Events.EVENT_TIMEZONE)); // Load ALL_DAY boolean allday = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.ALL_DAY)) == 1; if (allday) { event.setAllDay(allday); } // Load DTSTART String dtstart = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DTSTART)); if(!StringUtil.isNullOrEmpty(dtstart)) { // The dstart was shifted to UTC because expressed as msecs dtstart = CalendarUtils.formatDateTime(Long.parseLong(dtstart), allday, tz); event.setDtStart(new PropertyWithTimeZone(dtstart, tz)); } // Load DTEND String dtend = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DTEND)); if(!StringUtil.isNullOrEmpty(dtend)) { // Substract a day if this is an all day event long endMillis = Long.parseLong(dtend); if(allday) { endMillis -= CalendarUtils.DAY_FACTOR; } dtend = CalendarUtils.formatDateTime(endMillis, allday, tz); event.setDtEnd(new PropertyWithTimeZone(dtend, tz)); } // Load DURATION String duration = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DURATION)); if (duration != null) { event.setDuration(new Property(duration)); } // Load VISIBILITY_CLASS int vclass = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.ACCESS_LEVEL)); if(vclass != CalendarContract.Events.ACCESS_DEFAULT && vclass != CalendarContract.Events.ACCESS_CONFIDENTIAL) { if(vclass == CalendarContract.Events.ACCESS_PRIVATE) { event.setAccessClass(new Property(LEGACY_ACCESS_LEVEL_STRING_PRIVATE)); //legacy Funambol value } else if(vclass == CalendarContract.Events.ACCESS_PUBLIC) { event.setAccessClass(new Property(LEGACY_ACCESS_LEVEL_STRING_PUBLIC)); //legacy Funambol value } } // Load REMINDER int hasRem = cursor.getInt(cursor.getColumnIndexOrThrow(CalendarContract.Events.HAS_ALARM)); if (hasRem == 1) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "This event has an alarm associated"); } String fields[] = { CalendarContract.Reminders.MINUTES }; String whereClause = CalendarContract.Reminders.EVENT_ID + " = " + key; Cursor rems = resolver.query(CalendarContract.Reminders.CONTENT_URI, fields, whereClause, null, null); try { if (rems != null && rems.moveToFirst()) { int mins = rems.getInt(rems.getColumnIndexOrThrow(CalendarContract.Reminders.MINUTES)); Reminder rem = new Reminder(); rem.setMinutes(mins); rem.setActive(true); event.setReminder(rem); } else { Log.error(TAG_LOG, "Internal error: cannot find reminder for: " + key); } if (rems != null) { if(rems.moveToNext()) { Log.error(TAG_LOG, "Only one reminder is currently supported, ignoring the others"); } } } finally { if (rems != null) { rems.close(); } } } // Load recurrence String rrule = cursor.getString(cursor.getColumnIndexOrThrow(CalendarContract.Events.RRULE)); if (rrule != null && rrule.length() > 0) { try { String exdate = cursor.getString(cursor.getColumnIndexOrThrow(CalendarContract.Events.EXDATE)); String exrule = cursor.getString(cursor.getColumnIndexOrThrow(CalendarContract.Events.EXRULE)); String rdate = cursor.getString(cursor.getColumnIndexOrThrow(CalendarContract.Events.RDATE)); RecurrencePattern rp = createRecurrencePattern(dtstart, rrule, exdate, exrule, rdate, tz, allday); if (rp == null) { Log.error(TAG_LOG, "Cannot load recurrence"); } else { event.setRecurrencePattern(rp); } } catch (Exception e) { Log.error(TAG_LOG, "Cannot load recurrence", e); } } cal.setEvent(event); } /** * Fills a new ContentValues objects with all the given Event's properties * @param event the event to be used to fill the ContentValue object * @return ContentValues the filled ContenValues object. * @throws IOException if anything went wrong accessing the calendar db */ private ContentValues createEventContentValues(Event event) throws IOException { CalendarAppSyncSourceConfig config = (CalendarAppSyncSourceConfig)appSource.getConfig(); long calendarId = config.getCalendarId(); if (calendarId == -1) { throw new IOException("Cannot use undefined calendar"); } ContentValues cv = new ContentValues(); // Put String properties putStringProperty(CalendarContract.Events.TITLE, event.getSummary(), cv); putStringProperty(CalendarContract.Events.DESCRIPTION, event.getDescription(), cv); putStringProperty(CalendarContract.Events.EVENT_LOCATION, event.getLocation(), cv); // Put date properties PropertyWithTimeZone start = event.getDtStart(); PropertyWithTimeZone end = event.getDtEnd(); boolean allDay = false; if(putAllDay(event, cv)) { // We must take the event TZ into account for this to work allDay = true; } putDateTimeProperty(CalendarContract.Events.DTSTART, start, cv, allDay, false); // Android requires that we set DURATION or DTEND for all events Property duration = event.getDuration(); if (!Property.isEmptyProperty(duration)) { putStringProperty(CalendarContract.Events.DURATION, duration, cv); } else if (!Property.isEmptyProperty(end)) { // For rucurring events we must save the duration instead of the dtend if(event.getRecurrencePattern() != null) { // This piece of code computes the duration given a dtstart and // dtend. Probably redundant, do do not set it. if (!Property.isEmptyProperty(start)) { java.util.Calendar startCal = DateUtil.parseDateTime( start.getPropertyValueAsString()); java.util.Calendar endCal = DateUtil.parseDateTime( end.getPropertyValueAsString()); long endMillis = endCal.getTimeInMillis(); long startMillis = startCal.getTimeInMillis(); long d = endMillis - startMillis; int seconds = (int)(d / (1000)); // Duration should be formatted as 'P<seconds>S' StringBuffer newDuration = new StringBuffer(10); newDuration.append("P"); newDuration.append(seconds); newDuration.append("S"); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Setting duration to: " + newDuration); } putStringProperty(CalendarContract.Events.DURATION, new Property( newDuration.toString()), cv); } else { // Should never happen // Use a default DURATION of 1 in this case putStringProperty(CalendarContract.Events.DURATION, new Property("P1D"), cv); } } else { putDateTimeProperty(CalendarContract.Events.DTEND, end, cv, allDay, true); } } else { // Use a default DURATION of 1 in this case putStringProperty(CalendarContract.Events.DURATION, new Property("P1D"), cv); } // Put Timezone putTimeZone(event.getDtStart(), cv, allDay); // Put visibility class property putVisibilityClass(event.getAccessClass(), cv); // Put constant values cv.put(CalendarContract.Events.HAS_ATTENDEE_DATA, 1); // Set the hasAlarm property Reminder rem = event.getReminder(); cv.put(CalendarContract.Events.HAS_ALARM, rem != null); try { putRecurrence(event, cv); } catch (Exception e) { Log.error(TAG_LOG, "Cannot convert recurrence rule", e); throw new IOException("Cannot write recurrence rule"); } // Put Calendar references cv.put(CalendarContract.Events.CALENDAR_ID, calendarId); return cv; } /** * Put a String property to the given ContentValues. * @param column the culumn to be written * @param property the property to be written into the column * @param cv the content values related to the property */ private void putStringProperty(String column, Property property, ContentValues cv) { if(property != null) { String value = property.getPropertyValueAsString(); if(value != null) { value = StringUtil.replaceAll(value, "\r\n", "\n"); value = StringUtil.replaceAll(value, "\r", "\n"); cv.put(column, value); } } } /** * Put a date time property to the given ContentValues. * @param column the culumn to be written * @param property the property to be written into the column * @param cv the content values related to the property * @param allday tells that the given date is an all day * @param addOneDay add one day in milliseconds to the given date */ private void putDateTimeProperty(String column, PropertyWithTimeZone property, ContentValues cv, boolean allDay, boolean addOneDay) { if (property != null) { if (allDay) { String date = property.getPropertyValueAsString(); try { TimeZone tz = null; if (property.getTimeZone() != null) { tz = TimeZone.getTimeZone(property.getTimeZone()); } date = TimeUtils.convertUTCDateToLocal(date, tz); } catch(Exception ex) { Log.error(TAG_LOG, "Cannot convert to local time", ex); } // Android dislike events all day with a date whose hour/min/sec // are not zero (the calendar app crashes). For this reason we // remove any time info int tIdx = date.indexOf("T"); if (tIdx != -1) { date = date.substring(0, tIdx); } // TimeZone for all day properties must be UTC property = new PropertyWithTimeZone(date, "UTC"); } String value = property.getPropertyValueAsString(); if(value != null) { long time = CalendarUtils.getLocalDateTime(value, property.getTimeZone()); if(allDay && addOneDay) { time += CalendarUtils.DAY_FACTOR; } cv.put(column, time); } } } /** * Put the allday property to the given ContentValues. * @param event the event that contains the all day property * @param cv the content values related to the event */ private boolean putAllDay(Event event, ContentValues cv) { int allday = event.isAllDay() ? 1 : 0; cv.put(CalendarContract.Events.ALL_DAY, allday); return event.isAllDay(); } /** * Put the timezone property to the given ContentValues. * @param property the TZ to be set * @param cv the contentValues where to put the TZ */ private void putTimeZone(PropertyWithTimeZone property, ContentValues cv, boolean allDay) { if(property != null) { String tz = allDay ? "UTC" : property.getTimeZone(); if(!StringUtil.isNullOrEmpty(tz)) { cv.put(CalendarContract.Events.EVENT_TIMEZONE, tz); } } } /** * Put the visibility class property to the given ContentValues. * @param property the visibility property container * @param cv the Content value to be updated */ private void putVisibilityClass(Property property, ContentValues cv) { if(property != null) { String vclass = property.getPropertyValueAsString(); if(!StringUtil.isNullOrEmpty(vclass)) { if(LEGACY_ACCESS_LEVEL_STRING_PRIVATE.equals(vclass)) { cv.put(CalendarContract.Events.ACCESS_LEVEL, CalendarContract.Events.ACCESS_PRIVATE); } else if(LEGACY_ACCESS_LEVEL_STRING_PUBLIC.equals(vclass)) { cv.put(CalendarContract.Events.ACCESS_LEVEL, CalendarContract.Events.ACCESS_PUBLIC); } } } } private void addReminders(Calendar item, long eventId) throws IOException { // Create a new entry in the Reminders table if necessary Reminder rem = item.getEvent().getReminder(); if (rem != null) { int mins = -1; if (rem.getMinutes() > 0) { mins = rem.getMinutes(); } else if (rem.getTime() != null) { Log.error(TAG_LOG, "Reminder as absloute value not implemented yet"); /* TODO FIXME String time = rem.getTime(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Reminder time: " + time); } try { Date remDate = DateFormat.getDateInstance().parse(time); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "remDate=" + remDate.toString()); } } catch (Exception e) { Log.error(TAG_LOG, "Cannot parse reminder date, ignoring reminder"); } */ } if (mins != -1) { // We need to specify the time as minutes before the start Uri uri = asSyncAdapter(CalendarContract.Reminders.CONTENT_URI, this.accountName, this.accountType); ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(uri); // When inserting we shall use a back reference, while on update we have the event id if (eventId == -1) { builder.withValueBackReference(CalendarContract.Reminders.EVENT_ID, lastEventBackRef); } else { builder.withValue(CalendarContract.Reminders.EVENT_ID, eventId); } builder.withValue(CalendarContract.Reminders.MINUTES, mins); builder.withValue(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_DEFAULT); ops.add(builder.build()); } } } private void deleteRemindersForEvent(long itemId, boolean resetHasAlarm) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Deleting reminders for item: " + itemId); } // TODO FIXME: if we don't sync all of the reminders, we shall remove // only the first one, which is the one we sync... Uri uri = asSyncAdapter(CalendarContract.Reminders.CONTENT_URI, this.accountName, this.accountType); uri = ContentUris.withAppendedId(uri, itemId); ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete(uri); ops.add(builder.build()); if (resetHasAlarm) { // TODO: reset the has alarm in the original item } } private void putRecurrence(Event event, ContentValues cv) throws ConverterException { // We basically need to transform a vCal rec rule into an iCal rec rule // This can be done in different ways. One possibility was to used the // VCalendarConverter and the VComponentWriter, but this would not give // us any ability to modify the generated RRULE to fix issues. For this // reason that method has been discarded even if implementation wise it // would have been simpler. if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Saving recurrence"); } RecurrencePattern rp = event.getRecurrencePattern(); if (rp != null) { StringBuffer result = new StringBuffer(60); // Estimate 60 is needed String typeDesc = rp.getTypeDesc(); if (typeDesc != null) { result.append("FREQ="); if ("D".equals(typeDesc)) { result.append("DAILY"); } else if ("W".equals(typeDesc)) { result.append("WEEKLY"); } else if ("YM".equals(typeDesc) || "YD".equals(typeDesc)) { result.append("YEARLY"); } else if ("MP".equals(typeDesc) || "MD".equals(typeDesc)) { // This ia by position recurrence result.append("MONTHLY"); } int interval = rp.getInterval(); if(interval > 1) { result.append(";INTERVAL=").append(interval); } } int count = rp.getOccurrences(); if (count > 0 && rp.isNoEndDate()) { result.append(";COUNT=").append(count); } if (!rp.isNoEndDate() && rp.getEndDatePattern() != null && !rp.getEndDatePattern().equals("")) { rp = fixEndDatePattern(rp, true); String enddate = rp.getEndDatePattern(); result.append(";UNTIL=").append(enddate); } if ("W".equals(typeDesc)) { StringBuffer days = new StringBuffer(); for (int i=0; i<rp.getDayOfWeek().size(); i++) { if (days.length() > 0) { days.append(","); } days.append(rp.getDayOfWeek().get(i)); } if (days.length() > 0) { result.append(";BYDAY=").append(days.toString()); } } else if ("MD".equals(typeDesc)) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "getDayOfMonth=" + rp.getDayOfMonth()); } result.append(";BYMONTHDAY=").append(rp.getDayOfMonth()); } else if ("MP".equals(typeDesc)) { int instance = rp.getInstance(); short mask = rp.getDayOfWeekMask(); addDaysOfWeek(result, instance, mask); } else if ("YM".equals(typeDesc)) { short monthOfYear = rp.getMonthOfYear(); if (monthOfYear > 0) { result.append(";BYMONTH=").append(monthOfYear); } int instance = rp.getInstance(); short mask = rp.getDayOfWeekMask(); addDaysOfWeek(result, instance, mask); } else if ("YD".equals(typeDesc)) { // This is not supported by the calendar model } // Add the RRULE field String rule = result.toString(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Setting rrule in event to: " + rule); } cv.put(CalendarContract.Events.RRULE, rule); // Add the EXDATE and RDATE fields List<ExceptionToRecurrenceRule> exceptions = rp.getExceptions(); StringBuffer exdate = new StringBuffer(); StringBuffer rdate = new StringBuffer(); for(ExceptionToRecurrenceRule ex : exceptions) { String date = ex.getDate(); if(ex.isAddition()) { if(rdate.length() > 0) { rdate.append(','); } rdate.append(date); } else { if(exdate.length() > 0) { exdate.append(','); } exdate.append(date); } } if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Setting exdate in event to: " + exdate.toString()); } cv.put(CalendarContract.Events.EXDATE, exdate.toString()); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Setting rdate in event to: " + rdate.toString()); } cv.put(CalendarContract.Events.RDATE, rdate.toString()); } } private void addDaysOfWeek(StringBuffer result, int instance, short mask) { StringBuffer daysOfWeek = new StringBuffer(); if ((mask & RecurrencePattern.DAY_OF_WEEK_SUNDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "SU"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_MONDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "MO"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_TUESDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "TU"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_WEDNESDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "WE"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_THURSDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "TH"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_FRIDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "FR"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_SATURDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "SA"); } if (daysOfWeek.length() > 0) { result.append(";BYDAY=").append(daysOfWeek.toString()); } } private void addDayOfWeek(StringBuffer dayOfWeek, int instance, String day) { if (dayOfWeek.length() > 0) { dayOfWeek.append(","); } if (instance != 0) { dayOfWeek.append(instance); } dayOfWeek.append(day); } private RecurrencePattern createRecurrencePattern(String dtstart, String rrule, String exdate, String exrule, String rdate, String tz, boolean allday) throws Exception { // We must parse an ICalendar recurrence StringBuffer event = new StringBuffer(); event.append("BEGIN:VCALENDAR\r\n") .append("VERSION:2.0\r\n") .append("BEGIN:VEVENT\r\n") .append("DTSTART:").append(dtstart).append("\r\n"); event.append("RRULE:").append(rrule).append("\r\n"); if (exdate != null) { event.append("EXDATE:").append(exdate).append("\r\n"); } if (exrule != null) { event.append("EXRULE:").append(exrule).append("\r\n"); } if (rdate != null) { event.append("RDATE:").append(rdate).append("\r\n"); } event.append("END:VEVENT\r\n") .append("END:VCALENDAR\r\n"); ByteArrayInputStream buffer = new ByteArrayInputStream(event.toString().getBytes()); VCalendar vcalendar = new VCalendar(); ICalendarSyntaxParserListenerImpl listener = new ICalendarSyntaxParserListenerImpl(vcalendar); ICalendarSyntaxParser parser = new ICalendarSyntaxParser(buffer); parser.setListener(listener); parser.parse(); vcalendar.addProperty("VERSION", "2.0"); // In the iCalendar event we synthetized, we did not specify any // vtimezone. In this case the converter will consider the event in the // timezone supplied in the constructor. For this reason we create the // converter in the timezone of the vcal event TimeZone tZone; if (tz != null) { tZone = TimeZone.getTimeZone(tz); } else { // Use the device default TZ in this case tZone = TimeZone.getDefault(); } VCalendarConverter vcf = getConverter(tZone, allday); Event e = vcf.vcalendar2calendar(vcalendar).getEvent(); RecurrencePattern rp = e.getRecurrencePattern(); return fixEndDatePattern(rp, false); } /** * Fix the end date in the recurrence pattern. * Android saves the end date pattern as the first occurence to exclude * minus 1 second. According to the vCalendar specification the end date * "Controls when a repeating event terminates. The enddate is the last * time an event can occur." * So if the end date we read from the database is in the form: * 20101010T105959Z we should retrieve the last occurence previous to * that one. */ private RecurrencePattern fixEndDatePattern(RecurrencePattern rp, boolean incoming) { if(rp != null) { String enddate = rp.getEndDatePattern(); if((enddate != null) && ((!incoming && (enddate.endsWith("59") || enddate.endsWith("59Z"))) || incoming)) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Fixing end date in recurrence field"); } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Old end date: " + enddate); } String tz = rp.getTimeZone(); Time endDateTime = new Time(tz != null ? tz : "UTC"); endDateTime.parse(enddate); if(incoming) { endDateTime.second--; } else { endDateTime.second++; } switch(rp.getTypeId()) { case RecurrencePattern.TYPE_MONTHLY: if(incoming) { endDateTime.month++; } else { endDateTime.month--; } break; case RecurrencePattern.TYPE_YEARLY: if(incoming) { endDateTime.year++; } else { endDateTime.year--; } break; case RecurrencePattern.TYPE_DAILY: default: if(incoming) { endDateTime.monthDay++; } else { endDateTime.monthDay--; } break; } endDateTime.normalize(false); // Do not ignore DST // We cannot use the format2445 method because we found a bug // where certain dates are not properly formatted, resulting in // invalid dates that are later rejected by the calendar // provider long t = endDateTime.toMillis(false); enddate = DateUtil.formatDateTimeUTC(t); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "New end date: " + enddate); } rp.setEndDatePattern(enddate); } } return rp; } private VCalendarConverter getConverter(TimeZone tZone, boolean allday) { if (allday) { // For all day events we should use the device's default timezone in // order to convert the pattern end date to the related local time. return new VCalendarConverter(TimeZone.getDefault(), "UTF-8", false); } else { return new VCalendarConverter(tZone, "UTF-8", false); } } /** * Adds a parameter which tells Android NOT to mark modified entries as (sync-)dirty * @param uri * @param account * @param accountType * @return */ public static Uri asSyncAdapter(Uri uri, String account, String accountType) { return uri.buildUpon() .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,"true") .appendQueryParameter(Calendars.ACCOUNT_NAME, account) .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); } @Override protected String getAuthority() { return CalendarContract.AUTHORITY; } }