diff options
Diffstat (limited to 'src/com/android/calendarcommon2/RecurrenceSet.java')
-rw-r--r-- | src/com/android/calendarcommon2/RecurrenceSet.java | 526 |
1 files changed, 526 insertions, 0 deletions
diff --git a/src/com/android/calendarcommon2/RecurrenceSet.java b/src/com/android/calendarcommon2/RecurrenceSet.java new file mode 100644 index 0000000..9ee0ae9 --- /dev/null +++ b/src/com/android/calendarcommon2/RecurrenceSet.java @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.calendarcommon2; + +import android.content.ContentValues; +import android.database.Cursor; +import android.provider.CalendarContract; +import android.text.TextUtils; +import android.text.format.Time; +import android.util.Log; +import android.util.TimeFormatException; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Basic information about a recurrence, following RFC 2445 Section 4.8.5. + * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties. + */ +public class RecurrenceSet { + + private final static String TAG = "RecurrenceSet"; + + private final static String RULE_SEPARATOR = "\n"; + private final static String FOLDING_SEPARATOR = "\n "; + + // TODO: make these final? + public EventRecurrence[] rrules = null; + public long[] rdates = null; + public EventRecurrence[] exrules = null; + public long[] exdates = null; + + /** + * Creates a new RecurrenceSet from information stored in the + * events table in the CalendarProvider. + * @param values The values retrieved from the Events table. + */ + public RecurrenceSet(ContentValues values) + throws EventRecurrence.InvalidFormatException { + String rruleStr = values.getAsString(CalendarContract.Events.RRULE); + String rdateStr = values.getAsString(CalendarContract.Events.RDATE); + String exruleStr = values.getAsString(CalendarContract.Events.EXRULE); + String exdateStr = values.getAsString(CalendarContract.Events.EXDATE); + init(rruleStr, rdateStr, exruleStr, exdateStr); + } + + /** + * Creates a new RecurrenceSet from information stored in a database + * {@link Cursor} pointing to the events table in the + * CalendarProvider. The cursor must contain the RRULE, RDATE, EXRULE, + * and EXDATE columns. + * + * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE + * columns. + */ + public RecurrenceSet(Cursor cursor) + throws EventRecurrence.InvalidFormatException { + int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE); + int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE); + int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE); + int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE); + String rruleStr = cursor.getString(rruleColumn); + String rdateStr = cursor.getString(rdateColumn); + String exruleStr = cursor.getString(exruleColumn); + String exdateStr = cursor.getString(exdateColumn); + init(rruleStr, rdateStr, exruleStr, exdateStr); + } + + public RecurrenceSet(String rruleStr, String rdateStr, + String exruleStr, String exdateStr) + throws EventRecurrence.InvalidFormatException { + init(rruleStr, rdateStr, exruleStr, exdateStr); + } + + private void init(String rruleStr, String rdateStr, + String exruleStr, String exdateStr) + throws EventRecurrence.InvalidFormatException { + if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) { + + if (!TextUtils.isEmpty(rruleStr)) { + String[] rruleStrs = rruleStr.split(RULE_SEPARATOR); + rrules = new EventRecurrence[rruleStrs.length]; + for (int i = 0; i < rruleStrs.length; ++i) { + EventRecurrence rrule = new EventRecurrence(); + rrule.parse(rruleStrs[i]); + rrules[i] = rrule; + } + } + + if (!TextUtils.isEmpty(rdateStr)) { + rdates = parseRecurrenceDates(rdateStr); + } + + if (!TextUtils.isEmpty(exruleStr)) { + String[] exruleStrs = exruleStr.split(RULE_SEPARATOR); + exrules = new EventRecurrence[exruleStrs.length]; + for (int i = 0; i < exruleStrs.length; ++i) { + EventRecurrence exrule = new EventRecurrence(); + exrule.parse(exruleStr); + exrules[i] = exrule; + } + } + + if (!TextUtils.isEmpty(exdateStr)) { + exdates = parseRecurrenceDates(exdateStr); + } + } + } + + /** + * Returns whether or not a recurrence is defined in this RecurrenceSet. + * @return Whether or not a recurrence is defined in this RecurrenceSet. + */ + public boolean hasRecurrence() { + return (rrules != null || rdates != null); + } + + /** + * Parses the provided RDATE or EXDATE string into an array of longs + * representing each date/time in the recurrence. + * @param recurrence The recurrence to be parsed. + * @return The list of date/times. + */ + public static long[] parseRecurrenceDates(String recurrence) + throws EventRecurrence.InvalidFormatException{ + // TODO: use "local" time as the default. will need to handle times + // that end in "z" (UTC time) explicitly at that point. + String tz = Time.TIMEZONE_UTC; + int tzidx = recurrence.indexOf(";"); + if (tzidx != -1) { + tz = recurrence.substring(0, tzidx); + recurrence = recurrence.substring(tzidx + 1); + } + Time time = new Time(tz); + String[] rawDates = recurrence.split(","); + int n = rawDates.length; + long[] dates = new long[n]; + for (int i = 0; i<n; ++i) { + // The timezone is updated to UTC if the time string specified 'Z'. + try { + time.parse(rawDates[i]); + } catch (TimeFormatException e) { + throw new EventRecurrence.InvalidFormatException( + "TimeFormatException thrown when parsing time " + rawDates[i] + + " in recurrence " + recurrence); + + } + dates[i] = time.toMillis(false /* use isDst */); + time.timezone = tz; + } + return dates; + } + + /** + * Populates the database map of values with the appropriate RRULE, RDATE, + * EXRULE, and EXDATE values extracted from the parsed iCalendar component. + * @param component The iCalendar component containing the desired + * recurrence specification. + * @param values The db values that should be updated. + * @return true if the component contained the necessary information + * to specify a recurrence. The required fields are DTSTART, + * one of DTEND/DURATION, and one of RRULE/RDATE. Returns false if + * there was an error, including if the date is out of range. + */ + public static boolean populateContentValues(ICalendar.Component component, + ContentValues values) { + try { + ICalendar.Property dtstartProperty = + component.getFirstProperty("DTSTART"); + String dtstart = dtstartProperty.getValue(); + ICalendar.Parameter tzidParam = + dtstartProperty.getFirstParameter("TZID"); + // NOTE: the timezone may be null, if this is a floating time. + String tzid = tzidParam == null ? null : tzidParam.value; + Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid); + boolean inUtc = start.parse(dtstart); + boolean allDay = start.allDay; + + // We force TimeZone to UTC for "all day recurring events" as the server is sending no + // TimeZone in DTSTART for them + if (inUtc || allDay) { + tzid = Time.TIMEZONE_UTC; + } + + String duration = computeDuration(start, component); + String rrule = flattenProperties(component, "RRULE"); + String rdate = extractDates(component.getFirstProperty("RDATE")); + String exrule = flattenProperties(component, "EXRULE"); + String exdate = extractDates(component.getFirstProperty("EXDATE")); + + if ((TextUtils.isEmpty(dtstart))|| + (TextUtils.isEmpty(duration))|| + ((TextUtils.isEmpty(rrule))&& + (TextUtils.isEmpty(rdate)))) { + if (false) { + Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, " + + "or RRULE/RDATE: " + + component.toString()); + } + return false; + } + + if (allDay) { + start.timezone = Time.TIMEZONE_UTC; + } + long millis = start.toMillis(false /* use isDst */); + values.put(CalendarContract.Events.DTSTART, millis); + if (millis == -1) { + if (false) { + Log.d(TAG, "DTSTART is out of range: " + component.toString()); + } + return false; + } + + values.put(CalendarContract.Events.RRULE, rrule); + values.put(CalendarContract.Events.RDATE, rdate); + values.put(CalendarContract.Events.EXRULE, exrule); + values.put(CalendarContract.Events.EXDATE, exdate); + values.put(CalendarContract.Events.EVENT_TIMEZONE, tzid); + values.put(CalendarContract.Events.DURATION, duration); + values.put(CalendarContract.Events.ALL_DAY, allDay ? 1 : 0); + return true; + } catch (TimeFormatException e) { + // Something is wrong with the format of this event + Log.i(TAG,"Failed to parse event: " + component.toString()); + return false; + } + } + + // This can be removed when the old CalendarSyncAdapter is removed. + public static boolean populateComponent(Cursor cursor, + ICalendar.Component component) { + + int dtstartColumn = cursor.getColumnIndex(CalendarContract.Events.DTSTART); + int durationColumn = cursor.getColumnIndex(CalendarContract.Events.DURATION); + int tzidColumn = cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE); + int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE); + int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE); + int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE); + int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE); + int allDayColumn = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY); + + + long dtstart = -1; + if (!cursor.isNull(dtstartColumn)) { + dtstart = cursor.getLong(dtstartColumn); + } + String duration = cursor.getString(durationColumn); + String tzid = cursor.getString(tzidColumn); + String rruleStr = cursor.getString(rruleColumn); + String rdateStr = cursor.getString(rdateColumn); + String exruleStr = cursor.getString(exruleColumn); + String exdateStr = cursor.getString(exdateColumn); + boolean allDay = cursor.getInt(allDayColumn) == 1; + + if ((dtstart == -1) || + (TextUtils.isEmpty(duration))|| + ((TextUtils.isEmpty(rruleStr))&& + (TextUtils.isEmpty(rdateStr)))) { + // no recurrence. + return false; + } + + ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); + Time dtstartTime = null; + if (!TextUtils.isEmpty(tzid)) { + if (!allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); + } + dtstartTime = new Time(tzid); + } else { + // use the "floating" timezone + dtstartTime = new Time(Time.TIMEZONE_UTC); + } + + dtstartTime.set(dtstart); + // make sure the time is printed just as a date, if all day. + // TODO: android.pim.Time really should take care of this for us. + if (allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); + dtstartTime.allDay = true; + dtstartTime.hour = 0; + dtstartTime.minute = 0; + dtstartTime.second = 0; + } + + dtstartProp.setValue(dtstartTime.format2445()); + component.addProperty(dtstartProp); + ICalendar.Property durationProp = new ICalendar.Property("DURATION"); + durationProp.setValue(duration); + component.addProperty(durationProp); + + addPropertiesForRuleStr(component, "RRULE", rruleStr); + addPropertyForDateStr(component, "RDATE", rdateStr); + addPropertiesForRuleStr(component, "EXRULE", exruleStr); + addPropertyForDateStr(component, "EXDATE", exdateStr); + return true; + } + +public static boolean populateComponent(ContentValues values, + ICalendar.Component component) { + long dtstart = -1; + if (values.containsKey(CalendarContract.Events.DTSTART)) { + dtstart = values.getAsLong(CalendarContract.Events.DTSTART); + } + final String duration = values.getAsString(CalendarContract.Events.DURATION); + final String tzid = values.getAsString(CalendarContract.Events.EVENT_TIMEZONE); + final String rruleStr = values.getAsString(CalendarContract.Events.RRULE); + final String rdateStr = values.getAsString(CalendarContract.Events.RDATE); + final String exruleStr = values.getAsString(CalendarContract.Events.EXRULE); + final String exdateStr = values.getAsString(CalendarContract.Events.EXDATE); + final Integer allDayInteger = values.getAsInteger(CalendarContract.Events.ALL_DAY); + final boolean allDay = (null != allDayInteger) ? (allDayInteger == 1) : false; + + if ((dtstart == -1) || + (TextUtils.isEmpty(duration))|| + ((TextUtils.isEmpty(rruleStr))&& + (TextUtils.isEmpty(rdateStr)))) { + // no recurrence. + return false; + } + + ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); + Time dtstartTime = null; + if (!TextUtils.isEmpty(tzid)) { + if (!allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); + } + dtstartTime = new Time(tzid); + } else { + // use the "floating" timezone + dtstartTime = new Time(Time.TIMEZONE_UTC); + } + + dtstartTime.set(dtstart); + // make sure the time is printed just as a date, if all day. + // TODO: android.pim.Time really should take care of this for us. + if (allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); + dtstartTime.allDay = true; + dtstartTime.hour = 0; + dtstartTime.minute = 0; + dtstartTime.second = 0; + } + + dtstartProp.setValue(dtstartTime.format2445()); + component.addProperty(dtstartProp); + ICalendar.Property durationProp = new ICalendar.Property("DURATION"); + durationProp.setValue(duration); + component.addProperty(durationProp); + + addPropertiesForRuleStr(component, "RRULE", rruleStr); + addPropertyForDateStr(component, "RDATE", rdateStr); + addPropertiesForRuleStr(component, "EXRULE", exruleStr); + addPropertyForDateStr(component, "EXDATE", exdateStr); + return true; + } + + public static void addPropertiesForRuleStr(ICalendar.Component component, + String propertyName, + String ruleStr) { + if (TextUtils.isEmpty(ruleStr)) { + return; + } + String[] rrules = getRuleStrings(ruleStr); + for (String rrule : rrules) { + ICalendar.Property prop = new ICalendar.Property(propertyName); + prop.setValue(rrule); + component.addProperty(prop); + } + } + + private static String[] getRuleStrings(String ruleStr) { + if (null == ruleStr) { + return new String[0]; + } + String unfoldedRuleStr = unfold(ruleStr); + String[] split = unfoldedRuleStr.split(RULE_SEPARATOR); + int count = split.length; + for (int n = 0; n < count; n++) { + split[n] = fold(split[n]); + } + return split; + } + + + private static final Pattern IGNORABLE_ICAL_WHITESPACE_RE = + Pattern.compile("(?:\\r\\n?|\\n)[ \t]"); + + private static final Pattern FOLD_RE = Pattern.compile(".{75}"); + + /** + * fold and unfolds ical content lines as per RFC 2445 section 4.1. + * + * <h3>4.1 Content Lines</h3> + * + * <p>The iCalendar object is organized into individual lines of text, called + * content lines. Content lines are delimited by a line break, which is a CRLF + * sequence (US-ASCII decimal 13, followed by US-ASCII decimal 10). + * + * <p>Lines of text SHOULD NOT be longer than 75 octets, excluding the line + * break. Long content lines SHOULD be split into a multiple line + * representations using a line "folding" technique. That is, a long line can + * be split between any two characters by inserting a CRLF immediately + * followed by a single linear white space character (i.e., SPACE, US-ASCII + * decimal 32 or HTAB, US-ASCII decimal 9). Any sequence of CRLF followed + * immediately by a single linear white space character is ignored (i.e., + * removed) when processing the content type. + */ + public static String fold(String unfoldedIcalContent) { + return FOLD_RE.matcher(unfoldedIcalContent).replaceAll("$0\r\n "); + } + + public static String unfold(String foldedIcalContent) { + return IGNORABLE_ICAL_WHITESPACE_RE.matcher( + foldedIcalContent).replaceAll(""); + } + + public static void addPropertyForDateStr(ICalendar.Component component, + String propertyName, + String dateStr) { + if (TextUtils.isEmpty(dateStr)) { + return; + } + + ICalendar.Property prop = new ICalendar.Property(propertyName); + String tz = null; + int tzidx = dateStr.indexOf(";"); + if (tzidx != -1) { + tz = dateStr.substring(0, tzidx); + dateStr = dateStr.substring(tzidx + 1); + } + if (!TextUtils.isEmpty(tz)) { + prop.addParameter(new ICalendar.Parameter("TZID", tz)); + } + prop.setValue(dateStr); + component.addProperty(prop); + } + + private static String computeDuration(Time start, + ICalendar.Component component) { + // see if a duration is defined + ICalendar.Property durationProperty = + component.getFirstProperty("DURATION"); + if (durationProperty != null) { + // just return the duration + return durationProperty.getValue(); + } + + // must compute a duration from the DTEND + ICalendar.Property dtendProperty = + component.getFirstProperty("DTEND"); + if (dtendProperty == null) { + // no DURATION, no DTEND: 0 second duration + return "+P0S"; + } + ICalendar.Parameter endTzidParameter = + dtendProperty.getFirstParameter("TZID"); + String endTzid = (endTzidParameter == null) + ? start.timezone : endTzidParameter.value; + + Time end = new Time(endTzid); + end.parse(dtendProperty.getValue()); + long durationMillis = end.toMillis(false /* use isDst */) + - start.toMillis(false /* use isDst */); + long durationSeconds = (durationMillis / 1000); + if (start.allDay && (durationSeconds % 86400) == 0) { + return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S + } else { + return "P" + durationSeconds + "S"; + } + } + + private static String flattenProperties(ICalendar.Component component, + String name) { + List<ICalendar.Property> properties = component.getProperties(name); + if (properties == null || properties.isEmpty()) { + return null; + } + + if (properties.size() == 1) { + return properties.get(0).getValue(); + } + + StringBuilder sb = new StringBuilder(); + + boolean first = true; + for (ICalendar.Property property : component.getProperties(name)) { + if (first) { + first = false; + } else { + // TODO: use commas. our RECUR parsing should handle that + // anyway. + sb.append(RULE_SEPARATOR); + } + sb.append(property.getValue()); + } + return sb.toString(); + } + + private static String extractDates(ICalendar.Property recurrence) { + if (recurrence == null) { + return null; + } + ICalendar.Parameter tzidParam = + recurrence.getFirstParameter("TZID"); + if (tzidParam != null) { + return tzidParam.value + ";" + recurrence.getValue(); + } + return recurrence.getValue(); + } +} |