aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/calendarcommon2/RecurrenceSet.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/calendarcommon2/RecurrenceSet.java')
-rw-r--r--src/com/android/calendarcommon2/RecurrenceSet.java526
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();
+ }
+}