/* * 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.calendarcommon; 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; i4.1 Content Lines * *

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). * *

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 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(); } }