RecurrenceSet.java revision 0d3524562e330e74f150a17c4dc4dd66a0faae46
10d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden/*
20d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Copyright (C) 2007 The Android Open Source Project
30d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *
40d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Licensed under the Apache License, Version 2.0 (the "License");
50d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * you may not use this file except in compliance with the License.
60d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * You may obtain a copy of the License at
70d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *
80d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *      http://www.apache.org/licenses/LICENSE-2.0
90d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *
100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Unless required by applicable law or agreed to in writing, software
110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * distributed under the License is distributed on an "AS IS" BASIS,
120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * See the License for the specific language governing permissions and
140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * limitations under the License.
150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden */
160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenpackage com.android.calendarcommon;
180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.content.ContentValues;
200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.database.Cursor;
210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.provider.CalendarContract;
220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.text.TextUtils;
230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.text.format.Time;
240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.util.Log;
250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.List;
270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.regex.Pattern;
280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden/**
300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Basic information about a recurrence, following RFC 2445 Section 4.8.5.
310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties.
320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden */
330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenpublic class RecurrenceSet {
340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private final static String TAG = "CalendarProvider";
360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private final static String RULE_SEPARATOR = "\n";
380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private final static String FOLDING_SEPARATOR = "\n ";
390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // TODO: make these final?
410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public EventRecurrence[] rrules = null;
420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public long[] rdates = null;
430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public EventRecurrence[] exrules = null;
440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public long[] exdates = null;
450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Creates a new RecurrenceSet from information stored in the
480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * events table in the CalendarProvider.
490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param values The values retrieved from the Events table.
500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public RecurrenceSet(ContentValues values)
520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws EventRecurrence.InvalidFormatException {
530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rruleStr = values.getAsString(CalendarContract.Events.RRULE);
540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rdateStr = values.getAsString(CalendarContract.Events.RDATE);
550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exruleStr = values.getAsString(CalendarContract.Events.EXRULE);
560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exdateStr = values.getAsString(CalendarContract.Events.EXDATE);
570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        init(rruleStr, rdateStr, exruleStr, exdateStr);
580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Creates a new RecurrenceSet from information stored in a database
620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * {@link Cursor} pointing to the events table in the
630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * CalendarProvider.  The cursor must contain the RRULE, RDATE, EXRULE,
640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * and EXDATE columns.
650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     *
660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE
670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * columns.
680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public RecurrenceSet(Cursor cursor)
700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws EventRecurrence.InvalidFormatException {
710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE);
720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE);
730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE);
740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE);
750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rruleStr = cursor.getString(rruleColumn);
760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rdateStr = cursor.getString(rdateColumn);
770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exruleStr = cursor.getString(exruleColumn);
780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exdateStr = cursor.getString(exdateColumn);
790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        init(rruleStr, rdateStr, exruleStr, exdateStr);
800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public RecurrenceSet(String rruleStr, String rdateStr,
830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                  String exruleStr, String exdateStr)
840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws EventRecurrence.InvalidFormatException {
850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        init(rruleStr, rdateStr, exruleStr, exdateStr);
860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private void init(String rruleStr, String rdateStr,
890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                      String exruleStr, String exdateStr)
900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws EventRecurrence.InvalidFormatException {
910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) {
920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!TextUtils.isEmpty(rruleStr)) {
940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                String[] rruleStrs = rruleStr.split(RULE_SEPARATOR);
950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                rrules = new EventRecurrence[rruleStrs.length];
960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                for (int i = 0; i < rruleStrs.length; ++i) {
970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    EventRecurrence rrule = new EventRecurrence();
980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    rrule.parse(rruleStrs[i]);
990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    rrules[i] = rrule;
1000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
1010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!TextUtils.isEmpty(rdateStr)) {
1040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                rdates = parseRecurrenceDates(rdateStr);
1050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!TextUtils.isEmpty(exruleStr)) {
1080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                String[] exruleStrs = exruleStr.split(RULE_SEPARATOR);
1090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                exrules = new EventRecurrence[exruleStrs.length];
1100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                for (int i = 0; i < exruleStrs.length; ++i) {
1110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    EventRecurrence exrule = new EventRecurrence();
1120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    exrule.parse(exruleStr);
1130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    exrules[i] = exrule;
1140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
1150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!TextUtils.isEmpty(exdateStr)) {
1180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                exdates = parseRecurrenceDates(exdateStr);
1190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
1220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
1240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Returns whether or not a recurrence is defined in this RecurrenceSet.
1250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return Whether or not a recurrence is defined in this RecurrenceSet.
1260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
1270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public boolean hasRecurrence() {
1280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return (rrules != null || rdates != null);
1290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
1300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
1320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses the provided RDATE or EXDATE string into an array of longs
1330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * representing each date/time in the recurrence.
1340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param recurrence The recurrence to be parsed.
1350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return The list of date/times.
1360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
1370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static long[] parseRecurrenceDates(String recurrence) {
1380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // TODO: use "local" time as the default.  will need to handle times
1390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // that end in "z" (UTC time) explicitly at that point.
1400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String tz = Time.TIMEZONE_UTC;
1410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int tzidx = recurrence.indexOf(";");
1420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (tzidx != -1) {
1430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            tz = recurrence.substring(0, tzidx);
1440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            recurrence = recurrence.substring(tzidx + 1);
1450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Time time = new Time(tz);
1470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String[] rawDates = recurrence.split(",");
1480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int n = rawDates.length;
1490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        long[] dates = new long[n];
1500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        for (int i = 0; i<n; ++i) {
1510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // The timezone is updated to UTC if the time string specified 'Z'.
1520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            time.parse(rawDates[i]);
1530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dates[i] = time.toMillis(false /* use isDst */);
1540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            time.timezone = tz;
1550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return dates;
1570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
1580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
1600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Populates the database map of values with the appropriate RRULE, RDATE,
1610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * EXRULE, and EXDATE values extracted from the parsed iCalendar component.
1620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param component The iCalendar component containing the desired
1630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * recurrence specification.
1640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param values The db values that should be updated.
1650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return true if the component contained the necessary information
1660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * to specify a recurrence.  The required fields are DTSTART,
1670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * one of DTEND/DURATION, and one of RRULE/RDATE.  Returns false if
1680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * there was an error, including if the date is out of range.
1690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
1700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static boolean populateContentValues(ICalendar.Component component,
1710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ContentValues values) {
1720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property dtstartProperty =
1730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                component.getFirstProperty("DTSTART");
1740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String dtstart = dtstartProperty.getValue();
1750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Parameter tzidParam =
1760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                dtstartProperty.getFirstParameter("TZID");
1770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // NOTE: the timezone may be null, if this is a floating time.
1780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String tzid = tzidParam == null ? null : tzidParam.value;
1790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid);
1800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        boolean inUtc = start.parse(dtstart);
1810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        boolean allDay = start.allDay;
1820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // We force TimeZone to UTC for "all day recurring events" as the server is sending no
1840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // TimeZone in DTSTART for them
1850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (inUtc || allDay) {
1860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            tzid = Time.TIMEZONE_UTC;
1870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String duration = computeDuration(start, component);
1900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rrule = flattenProperties(component, "RRULE");
1910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rdate = extractDates(component.getFirstProperty("RDATE"));
1920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exrule = flattenProperties(component, "EXRULE");
1930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exdate = extractDates(component.getFirstProperty("EXDATE"));
1940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if ((TextUtils.isEmpty(dtstart))||
1960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                (TextUtils.isEmpty(duration))||
1970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                ((TextUtils.isEmpty(rrule))&&
1980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                        (TextUtils.isEmpty(rdate)))) {
1990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (false) {
2000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, "
2010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                + "or RRULE/RDATE: "
2020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                + component.toString());
2030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
2040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return false;
2050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (allDay) {
2080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            start.timezone = Time.TIMEZONE_UTC;
2090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        long millis = start.toMillis(false /* use isDst */);
2110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.DTSTART, millis);
2120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (millis == -1) {
2130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (false) {
2140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                Log.d(TAG, "DTSTART is out of range: " + component.toString());
2150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
2160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return false;
2170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.RRULE, rrule);
2200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.RDATE, rdate);
2210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.EXRULE, exrule);
2220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.EXDATE, exdate);
2230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.EVENT_TIMEZONE, tzid);
2240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.DURATION, duration);
2250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        values.put(CalendarContract.Events.ALL_DAY, allDay ? 1 : 0);
2260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return true;
2270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
2280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // This can be removed when the old CalendarSyncAdapter is removed.
2300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static boolean populateComponent(Cursor cursor,
2310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                            ICalendar.Component component) {
2320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int dtstartColumn = cursor.getColumnIndex(CalendarContract.Events.DTSTART);
2340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int durationColumn = cursor.getColumnIndex(CalendarContract.Events.DURATION);
2350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int tzidColumn = cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE);
2360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE);
2370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE);
2380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE);
2390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE);
2400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int allDayColumn = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY);
2410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        long dtstart = -1;
2440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (!cursor.isNull(dtstartColumn)) {
2450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstart = cursor.getLong(dtstartColumn);
2460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String duration = cursor.getString(durationColumn);
2480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String tzid = cursor.getString(tzidColumn);
2490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rruleStr = cursor.getString(rruleColumn);
2500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rdateStr = cursor.getString(rdateColumn);
2510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exruleStr = cursor.getString(exruleColumn);
2520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exdateStr = cursor.getString(exdateColumn);
2530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        boolean allDay = cursor.getInt(allDayColumn) == 1;
2540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if ((dtstart == -1) ||
2560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            (TextUtils.isEmpty(duration))||
2570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ((TextUtils.isEmpty(rruleStr))&&
2580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                (TextUtils.isEmpty(rdateStr)))) {
2590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // no recurrence.
2600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return false;
2610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
2640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Time dtstartTime = null;
2650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (!TextUtils.isEmpty(tzid)) {
2660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!allDay) {
2670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
2680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
2690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime = new Time(tzid);
2700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        } else {
2710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // use the "floating" timezone
2720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime = new Time(Time.TIMEZONE_UTC);
2730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        dtstartTime.set(dtstart);
2760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // make sure the time is printed just as a date, if all day.
2770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // TODO: android.pim.Time really should take care of this for us.
2780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (allDay) {
2790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
2800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.allDay = true;
2810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.hour = 0;
2820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.minute = 0;
2830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.second = 0;
2840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        dtstartProp.setValue(dtstartTime.format2445());
2870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        component.addProperty(dtstartProp);
2880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property durationProp = new ICalendar.Property("DURATION");
2890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        durationProp.setValue(duration);
2900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        component.addProperty(durationProp);
2910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertiesForRuleStr(component, "RRULE", rruleStr);
2930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertyForDateStr(component, "RDATE", rdateStr);
2940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertiesForRuleStr(component, "EXRULE", exruleStr);
2950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertyForDateStr(component, "EXDATE", exdateStr);
2960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return true;
2970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
2980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenpublic static boolean populateComponent(ContentValues values,
3000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                            ICalendar.Component component) {
3010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        long dtstart = -1;
3020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (values.containsKey(CalendarContract.Events.DTSTART)) {
3030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstart = values.getAsLong(CalendarContract.Events.DTSTART);
3040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String duration = values.getAsString(CalendarContract.Events.DURATION);
3060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String tzid = values.getAsString(CalendarContract.Events.EVENT_TIMEZONE);
3070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rruleStr = values.getAsString(CalendarContract.Events.RRULE);
3080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String rdateStr = values.getAsString(CalendarContract.Events.RDATE);
3090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exruleStr = values.getAsString(CalendarContract.Events.EXRULE);
3100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String exdateStr = values.getAsString(CalendarContract.Events.EXDATE);
3110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Integer allDayInteger = values.getAsInteger(CalendarContract.Events.ALL_DAY);
3120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        boolean allDay = (null != allDayInteger) ? (allDayInteger == 1) : false;
3130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if ((dtstart == -1) ||
3150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            (TextUtils.isEmpty(duration))||
3160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ((TextUtils.isEmpty(rruleStr))&&
3170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                (TextUtils.isEmpty(rdateStr)))) {
3180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // no recurrence.
3190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return false;
3200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
3230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Time dtstartTime = null;
3240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (!TextUtils.isEmpty(tzid)) {
3250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!allDay) {
3260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
3270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
3280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime = new Time(tzid);
3290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        } else {
3300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // use the "floating" timezone
3310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime = new Time(Time.TIMEZONE_UTC);
3320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        dtstartTime.set(dtstart);
3350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // make sure the time is printed just as a date, if all day.
3360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // TODO: android.pim.Time really should take care of this for us.
3370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (allDay) {
3380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
3390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.allDay = true;
3400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.hour = 0;
3410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.minute = 0;
3420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dtstartTime.second = 0;
3430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        dtstartProp.setValue(dtstartTime.format2445());
3460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        component.addProperty(dtstartProp);
3470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property durationProp = new ICalendar.Property("DURATION");
3480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        durationProp.setValue(duration);
3490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        component.addProperty(durationProp);
3500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertiesForRuleStr(component, "RRULE", rruleStr);
3520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertyForDateStr(component, "RDATE", rdateStr);
3530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertiesForRuleStr(component, "EXRULE", exruleStr);
3540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        addPropertyForDateStr(component, "EXDATE", exdateStr);
3550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return true;
3560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
3570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static void addPropertiesForRuleStr(ICalendar.Component component,
3590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                                String propertyName,
3600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                                String ruleStr) {
3610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (TextUtils.isEmpty(ruleStr)) {
3620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return;
3630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String[] rrules = getRuleStrings(ruleStr);
3650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        for (String rrule : rrules) {
3660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ICalendar.Property prop = new ICalendar.Property(propertyName);
3670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            prop.setValue(rrule);
3680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            component.addProperty(prop);
3690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
3710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static String[] getRuleStrings(String ruleStr) {
3730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (null == ruleStr) {
3740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return new String[0];
3750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String unfoldedRuleStr = unfold(ruleStr);
3770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String[] split = unfoldedRuleStr.split(RULE_SEPARATOR);
3780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int count = split.length;
3790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        for (int n = 0; n < count; n++) {
3800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            split[n] = fold(split[n]);
3810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return split;
3830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
3840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static final Pattern IGNORABLE_ICAL_WHITESPACE_RE =
3870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            Pattern.compile("(?:\\r\\n?|\\n)[ \t]");
3880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static final Pattern FOLD_RE = Pattern.compile(".{75}");
3900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
3920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * fold and unfolds ical content lines as per RFC 2445 section 4.1.
3930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    *
3940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * <h3>4.1 Content Lines</h3>
3950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    *
3960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * <p>The iCalendar object is organized into individual lines of text, called
3970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * content lines. Content lines are delimited by a line break, which is a CRLF
3980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * sequence (US-ASCII decimal 13, followed by US-ASCII decimal 10).
3990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    *
4000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * <p>Lines of text SHOULD NOT be longer than 75 octets, excluding the line
4010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * break. Long content lines SHOULD be split into a multiple line
4020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * representations using a line "folding" technique. That is, a long line can
4030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * be split between any two characters by inserting a CRLF immediately
4040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * followed by a single linear white space character (i.e., SPACE, US-ASCII
4050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * decimal 32 or HTAB, US-ASCII decimal 9). Any sequence of CRLF followed
4060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * immediately by a single linear white space character is ignored (i.e.,
4070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    * removed) when processing the content type.
4080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    */
4090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static String fold(String unfoldedIcalContent) {
4100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return FOLD_RE.matcher(unfoldedIcalContent).replaceAll("$0\r\n ");
4110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static String unfold(String foldedIcalContent) {
4140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return IGNORABLE_ICAL_WHITESPACE_RE.matcher(
4150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            foldedIcalContent).replaceAll("");
4160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static void addPropertyForDateStr(ICalendar.Component component,
4190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                              String propertyName,
4200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                              String dateStr) {
4210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (TextUtils.isEmpty(dateStr)) {
4220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return;
4230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property prop = new ICalendar.Property(propertyName);
4260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String tz = null;
4270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int tzidx = dateStr.indexOf(";");
4280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (tzidx != -1) {
4290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            tz = dateStr.substring(0, tzidx);
4300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            dateStr = dateStr.substring(tzidx + 1);
4310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (!TextUtils.isEmpty(tz)) {
4330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            prop.addParameter(new ICalendar.Parameter("TZID", tz));
4340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        prop.setValue(dateStr);
4360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        component.addProperty(prop);
4370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static String computeDuration(Time start,
4400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                          ICalendar.Component component) {
4410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // see if a duration is defined
4420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property durationProperty =
4430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                component.getFirstProperty("DURATION");
4440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (durationProperty != null) {
4450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // just return the duration
4460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return durationProperty.getValue();
4470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // must compute a duration from the DTEND
4500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Property dtendProperty =
4510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                component.getFirstProperty("DTEND");
4520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (dtendProperty == null) {
4530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // no DURATION, no DTEND: 0 second duration
4540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return "+P0S";
4550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Parameter endTzidParameter =
4570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                dtendProperty.getFirstParameter("TZID");
4580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String endTzid = (endTzidParameter == null)
4590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                ? start.timezone : endTzidParameter.value;
4600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Time end = new Time(endTzid);
4620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        end.parse(dtendProperty.getValue());
4630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        long durationMillis = end.toMillis(false /* use isDst */)
4640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                - start.toMillis(false /* use isDst */);
4650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        long durationSeconds = (durationMillis / 1000);
4660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (start.allDay && (durationSeconds % 86400) == 0) {
4670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S
4680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        } else {
4690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return "P" + durationSeconds + "S";
4700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static String flattenProperties(ICalendar.Component component,
4740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                            String name) {
4750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        List<ICalendar.Property> properties = component.getProperties(name);
4760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (properties == null || properties.isEmpty()) {
4770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return null;
4780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (properties.size() == 1) {
4810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return properties.get(0).getValue();
4820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        StringBuilder sb = new StringBuilder();
4850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        boolean first = true;
4870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        for (ICalendar.Property property : component.getProperties(name)) {
4880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (first) {
4890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                first = false;
4900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            } else {
4910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // TODO: use commas.  our RECUR parsing should handle that
4920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // anyway.
4930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                sb.append(RULE_SEPARATOR);
4940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
4950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(property.getValue());
4960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return sb.toString();
4980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
5000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static String extractDates(ICalendar.Property recurrence) {
5010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (recurrence == null) {
5020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return null;
5030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
5040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ICalendar.Parameter tzidParam =
5050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                recurrence.getFirstParameter("TZID");
5060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (tzidParam != null) {
5070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return tzidParam.value + ";" + recurrence.getValue();
5080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
5090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return recurrence.getValue();
5100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
5110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden}
512