Relocate common Calendar classes

Move some classes from android.pim to com.android.calendarcommon.

Bug 4575374

Change-Id: I0da19545253f0bc887d2c284414f372ad740e946
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..ffa7606
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,24 @@
+# Copyright (C) 2011 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := calendar-common
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Build the test package
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/src/com/android/calendarcommon/EventRecurrence.java b/src/com/android/calendarcommon/EventRecurrence.java
new file mode 100644
index 0000000..fa5d47c
--- /dev/null
+++ b/src/com/android/calendarcommon/EventRecurrence.java
@@ -0,0 +1,892 @@
+/*
+ * Copyright (C) 2006 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.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.util.TimeFormatException;
+
+import java.util.Calendar;
+import java.util.HashMap;
+
+/**
+ * Event recurrence utility functions.
+ */
+public class EventRecurrence {
+    private static String TAG = "EventRecur";
+
+    public static final int SECONDLY = 1;
+    public static final int MINUTELY = 2;
+    public static final int HOURLY = 3;
+    public static final int DAILY = 4;
+    public static final int WEEKLY = 5;
+    public static final int MONTHLY = 6;
+    public static final int YEARLY = 7;
+
+    public static final int SU = 0x00010000;
+    public static final int MO = 0x00020000;
+    public static final int TU = 0x00040000;
+    public static final int WE = 0x00080000;
+    public static final int TH = 0x00100000;
+    public static final int FR = 0x00200000;
+    public static final int SA = 0x00400000;
+
+    public Time      startDate;     // set by setStartDate(), not parse()
+
+    public int       freq;          // SECONDLY, MINUTELY, etc.
+    public String    until;
+    public int       count;
+    public int       interval;
+    public int       wkst;          // SU, MO, TU, etc.
+
+    /* lists with zero entries may be null references */
+    public int[]     bysecond;
+    public int       bysecondCount;
+    public int[]     byminute;
+    public int       byminuteCount;
+    public int[]     byhour;
+    public int       byhourCount;
+    public int[]     byday;
+    public int[]     bydayNum;
+    public int       bydayCount;
+    public int[]     bymonthday;
+    public int       bymonthdayCount;
+    public int[]     byyearday;
+    public int       byyeardayCount;
+    public int[]     byweekno;
+    public int       byweeknoCount;
+    public int[]     bymonth;
+    public int       bymonthCount;
+    public int[]     bysetpos;
+    public int       bysetposCount;
+
+    /** maps a part string to a parser object */
+    private static HashMap<String,PartParser> sParsePartMap;
+    static {
+        sParsePartMap = new HashMap<String,PartParser>();
+        sParsePartMap.put("FREQ", new ParseFreq());
+        sParsePartMap.put("UNTIL", new ParseUntil());
+        sParsePartMap.put("COUNT", new ParseCount());
+        sParsePartMap.put("INTERVAL", new ParseInterval());
+        sParsePartMap.put("BYSECOND", new ParseBySecond());
+        sParsePartMap.put("BYMINUTE", new ParseByMinute());
+        sParsePartMap.put("BYHOUR", new ParseByHour());
+        sParsePartMap.put("BYDAY", new ParseByDay());
+        sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay());
+        sParsePartMap.put("BYYEARDAY", new ParseByYearDay());
+        sParsePartMap.put("BYWEEKNO", new ParseByWeekNo());
+        sParsePartMap.put("BYMONTH", new ParseByMonth());
+        sParsePartMap.put("BYSETPOS", new ParseBySetPos());
+        sParsePartMap.put("WKST", new ParseWkst());
+    }
+
+    /* values for bit vector that keeps track of what we have already seen */
+    private static final int PARSED_FREQ = 1 << 0;
+    private static final int PARSED_UNTIL = 1 << 1;
+    private static final int PARSED_COUNT = 1 << 2;
+    private static final int PARSED_INTERVAL = 1 << 3;
+    private static final int PARSED_BYSECOND = 1 << 4;
+    private static final int PARSED_BYMINUTE = 1 << 5;
+    private static final int PARSED_BYHOUR = 1 << 6;
+    private static final int PARSED_BYDAY = 1 << 7;
+    private static final int PARSED_BYMONTHDAY = 1 << 8;
+    private static final int PARSED_BYYEARDAY = 1 << 9;
+    private static final int PARSED_BYWEEKNO = 1 << 10;
+    private static final int PARSED_BYMONTH = 1 << 11;
+    private static final int PARSED_BYSETPOS = 1 << 12;
+    private static final int PARSED_WKST = 1 << 13;
+
+    /** maps a FREQ value to an integer constant */
+    private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>();
+    static {
+        sParseFreqMap.put("SECONDLY", SECONDLY);
+        sParseFreqMap.put("MINUTELY", MINUTELY);
+        sParseFreqMap.put("HOURLY", HOURLY);
+        sParseFreqMap.put("DAILY", DAILY);
+        sParseFreqMap.put("WEEKLY", WEEKLY);
+        sParseFreqMap.put("MONTHLY", MONTHLY);
+        sParseFreqMap.put("YEARLY", YEARLY);
+    }
+
+    /** maps a two-character weekday string to an integer constant */
+    private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>();
+    static {
+        sParseWeekdayMap.put("SU", SU);
+        sParseWeekdayMap.put("MO", MO);
+        sParseWeekdayMap.put("TU", TU);
+        sParseWeekdayMap.put("WE", WE);
+        sParseWeekdayMap.put("TH", TH);
+        sParseWeekdayMap.put("FR", FR);
+        sParseWeekdayMap.put("SA", SA);
+    }
+
+    /** If set, allow lower-case recurrence rule strings.  Minor performance impact. */
+    private static final boolean ALLOW_LOWER_CASE = false;
+
+    /** If set, validate the value of UNTIL parts.  Minor performance impact. */
+    private static final boolean VALIDATE_UNTIL = false;
+
+    /** If set, require that only one of {UNTIL,COUNT} is present.  Breaks compat w/ old parser. */
+    private static final boolean ONLY_ONE_UNTIL_COUNT = false;
+
+
+    /**
+     * Thrown when a recurrence string provided can not be parsed according
+     * to RFC2445.
+     */
+    public static class InvalidFormatException extends RuntimeException {
+        InvalidFormatException(String s) {
+            super(s);
+        }
+    }
+
+
+    public void setStartDate(Time date) {
+        startDate = date;
+    }
+
+    /**
+     * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc.
+     * constants.  btw, I think we should switch to those here too, to
+     * get rid of this function, if possible.
+     */
+    public static int calendarDay2Day(int day)
+    {
+        switch (day)
+        {
+            case Calendar.SUNDAY:
+                return SU;
+            case Calendar.MONDAY:
+                return MO;
+            case Calendar.TUESDAY:
+                return TU;
+            case Calendar.WEDNESDAY:
+                return WE;
+            case Calendar.THURSDAY:
+                return TH;
+            case Calendar.FRIDAY:
+                return FR;
+            case Calendar.SATURDAY:
+                return SA;
+            default:
+                throw new RuntimeException("bad day of week: " + day);
+        }
+    }
+
+    public static int timeDay2Day(int day)
+    {
+        switch (day)
+        {
+            case Time.SUNDAY:
+                return SU;
+            case Time.MONDAY:
+                return MO;
+            case Time.TUESDAY:
+                return TU;
+            case Time.WEDNESDAY:
+                return WE;
+            case Time.THURSDAY:
+                return TH;
+            case Time.FRIDAY:
+                return FR;
+            case Time.SATURDAY:
+                return SA;
+            default:
+                throw new RuntimeException("bad day of week: " + day);
+        }
+    }
+    public static int day2TimeDay(int day)
+    {
+        switch (day)
+        {
+            case SU:
+                return Time.SUNDAY;
+            case MO:
+                return Time.MONDAY;
+            case TU:
+                return Time.TUESDAY;
+            case WE:
+                return Time.WEDNESDAY;
+            case TH:
+                return Time.THURSDAY;
+            case FR:
+                return Time.FRIDAY;
+            case SA:
+                return Time.SATURDAY;
+            default:
+                throw new RuntimeException("bad day of week: " + day);
+        }
+    }
+
+    /**
+     * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY
+     * constants.  btw, I think we should switch to those here too, to
+     * get rid of this function, if possible.
+     */
+    public static int day2CalendarDay(int day)
+    {
+        switch (day)
+        {
+            case SU:
+                return Calendar.SUNDAY;
+            case MO:
+                return Calendar.MONDAY;
+            case TU:
+                return Calendar.TUESDAY;
+            case WE:
+                return Calendar.WEDNESDAY;
+            case TH:
+                return Calendar.THURSDAY;
+            case FR:
+                return Calendar.FRIDAY;
+            case SA:
+                return Calendar.SATURDAY;
+            default:
+                throw new RuntimeException("bad day of week: " + day);
+        }
+    }
+
+    /**
+     * Converts one of the internal day constants (SU, MO, etc.) to the
+     * two-letter string representing that constant.
+     *
+     * @param day one the internal constants SU, MO, etc.
+     * @return the two-letter string for the day ("SU", "MO", etc.)
+     *
+     * @throws IllegalArgumentException Thrown if the day argument is not one of
+     * the defined day constants.
+     */
+    private static String day2String(int day) {
+        switch (day) {
+        case SU:
+            return "SU";
+        case MO:
+            return "MO";
+        case TU:
+            return "TU";
+        case WE:
+            return "WE";
+        case TH:
+            return "TH";
+        case FR:
+            return "FR";
+        case SA:
+            return "SA";
+        default:
+            throw new IllegalArgumentException("bad day argument: " + day);
+        }
+    }
+
+    private static void appendNumbers(StringBuilder s, String label,
+                                        int count, int[] values)
+    {
+        if (count > 0) {
+            s.append(label);
+            count--;
+            for (int i=0; i<count; i++) {
+                s.append(values[i]);
+                s.append(",");
+            }
+            s.append(values[count]);
+        }
+    }
+
+    private void appendByDay(StringBuilder s, int i)
+    {
+        int n = this.bydayNum[i];
+        if (n != 0) {
+            s.append(n);
+        }
+
+        String str = day2String(this.byday[i]);
+        s.append(str);
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder s = new StringBuilder();
+
+        s.append("FREQ=");
+        switch (this.freq)
+        {
+            case SECONDLY:
+                s.append("SECONDLY");
+                break;
+            case MINUTELY:
+                s.append("MINUTELY");
+                break;
+            case HOURLY:
+                s.append("HOURLY");
+                break;
+            case DAILY:
+                s.append("DAILY");
+                break;
+            case WEEKLY:
+                s.append("WEEKLY");
+                break;
+            case MONTHLY:
+                s.append("MONTHLY");
+                break;
+            case YEARLY:
+                s.append("YEARLY");
+                break;
+        }
+
+        if (!TextUtils.isEmpty(this.until)) {
+            s.append(";UNTIL=");
+            s.append(until);
+        }
+
+        if (this.count != 0) {
+            s.append(";COUNT=");
+            s.append(this.count);
+        }
+
+        if (this.interval != 0) {
+            s.append(";INTERVAL=");
+            s.append(this.interval);
+        }
+
+        if (this.wkst != 0) {
+            s.append(";WKST=");
+            s.append(day2String(this.wkst));
+        }
+
+        appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond);
+        appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute);
+        appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour);
+
+        // day
+        int count = this.bydayCount;
+        if (count > 0) {
+            s.append(";BYDAY=");
+            count--;
+            for (int i=0; i<count; i++) {
+                appendByDay(s, i);
+                s.append(",");
+            }
+            appendByDay(s, count);
+        }
+
+        appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday);
+        appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday);
+        appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno);
+        appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth);
+        appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos);
+
+        return s.toString();
+    }
+
+    public boolean repeatsOnEveryWeekDay() {
+        if (this.freq != WEEKLY) {
+            return false;
+        }
+
+        int count = this.bydayCount;
+        if (count != 5) {
+            return false;
+        }
+
+        for (int i = 0 ; i < count ; i++) {
+            int day = byday[i];
+            if (day == SU || day == SA) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Determines whether this rule specifies a simple monthly rule by weekday, such as
+     * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month).
+     * <p>
+     * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month),
+     * will cause "false" to be returned.
+     * <p>
+     * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every
+     * month) will cause "false" to be returned.  (Note these are usually expressed as
+     * WEEKLY rules, and hence are uncommon.)
+     *
+     * @return true if this rule is of the appropriate form
+     */
+    public boolean repeatsMonthlyOnDayCount() {
+        if (this.freq != MONTHLY) {
+            return false;
+        }
+
+        if (bydayCount != 1 || bymonthdayCount != 0) {
+            return false;
+        }
+
+        if (bydayNum[0] <= 0) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Determines whether two integer arrays contain identical elements.
+     * <p>
+     * The native implementation over-allocated the arrays (and may have stuff left over from
+     * a previous run), so we can't just check the arrays -- the separately-maintained count
+     * field also matters.  We assume that a null array will have a count of zero, and that the
+     * array can hold as many elements as the associated count indicates.
+     * <p>
+     * TODO: replace this with Arrays.equals() when the old parser goes away.
+     */
+    private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) {
+        if (count1 != count2) {
+            return false;
+        }
+
+        for (int i = 0; i < count1; i++) {
+            if (array1[i] != array2[i])
+                return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof EventRecurrence)) {
+            return false;
+        }
+
+        EventRecurrence er = (EventRecurrence) obj;
+        return  (startDate == null ?
+                        er.startDate == null : Time.compare(startDate, er.startDate) == 0) &&
+                freq == er.freq &&
+                (until == null ? er.until == null : until.equals(er.until)) &&
+                count == er.count &&
+                interval == er.interval &&
+                wkst == er.wkst &&
+                arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) &&
+                arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) &&
+                arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) &&
+                arraysEqual(byday, bydayCount, er.byday, er.bydayCount) &&
+                arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) &&
+                arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) &&
+                arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) &&
+                arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) &&
+                arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) &&
+                arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount);
+    }
+
+    @Override public int hashCode() {
+        // We overrode equals, so we must override hashCode().  Nobody seems to need this though.
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Resets parser-modified fields to their initial state.  Does not alter startDate.
+     * <p>
+     * The original parser always set all of the "count" fields, "wkst", and "until",
+     * essentially allowing the same object to be used multiple times by calling parse().
+     * It's unclear whether this behavior was intentional.  For now, be paranoid and
+     * preserve the existing behavior by resetting the fields.
+     * <p>
+     * We don't need to touch the integer arrays; they will either be ignored or
+     * overwritten.  The "startDate" field is not set by the parser, so we ignore it here.
+     */
+    private void resetFields() {
+        until = null;
+        freq = count = interval = bysecondCount = byminuteCount = byhourCount =
+            bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount =
+            bysetposCount = 0;
+    }
+
+    /**
+     * Parses an rfc2445 recurrence rule string into its component pieces.  Attempting to parse
+     * malformed input will result in an EventRecurrence.InvalidFormatException.
+     *
+     * @param recur The recurrence rule to parse (in un-folded form).
+     */
+    public void parse(String recur) {
+        /*
+         * From RFC 2445 section 4.3.10:
+         *
+         * recur = "FREQ"=freq *(
+         *       ; either UNTIL or COUNT may appear in a 'recur',
+         *       ; but UNTIL and COUNT MUST NOT occur in the same 'recur'
+         *
+         *       ( ";" "UNTIL" "=" enddate ) /
+         *       ( ";" "COUNT" "=" 1*DIGIT ) /
+         *
+         *       ; the rest of these keywords are optional,
+         *       ; but MUST NOT occur more than once
+         *
+         *       ( ";" "INTERVAL" "=" 1*DIGIT )          /
+         *       ( ";" "BYSECOND" "=" byseclist )        /
+         *       ( ";" "BYMINUTE" "=" byminlist )        /
+         *       ( ";" "BYHOUR" "=" byhrlist )           /
+         *       ( ";" "BYDAY" "=" bywdaylist )          /
+         *       ( ";" "BYMONTHDAY" "=" bymodaylist )    /
+         *       ( ";" "BYYEARDAY" "=" byyrdaylist )     /
+         *       ( ";" "BYWEEKNO" "=" bywknolist )       /
+         *       ( ";" "BYMONTH" "=" bymolist )          /
+         *       ( ";" "BYSETPOS" "=" bysplist )         /
+         *       ( ";" "WKST" "=" weekday )              /
+         *       ( ";" x-name "=" text )
+         *       )
+         *
+         * Examples:
+         *   FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
+         *   FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
+         *
+         * Strategy:
+         * (1) Split the string at ';' boundaries to get an array of rule "parts".
+         * (2) For each part, find substrings for left/right sides of '=' (name/value).
+         * (3) Call a <name>-specific parsing function to parse the <value> into an
+         *     output field.
+         *
+         * By keeping track of which names we've seen in a bit vector, we can verify the
+         * constraints indicated above (FREQ appears first, none of them appear more than once --
+         * though x-[name] would require special treatment), and we have either UNTIL or COUNT
+         * but not both.
+         *
+         * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must
+         * be handled in a case-insensitive fashion, but case may be significant for other
+         * properties.  We don't have any case-sensitive values in RRULE, except possibly
+         * for the custom "X-" properties, but we ignore those anyway.  Thus, we can trivially
+         * convert the entire string to upper case and then use simple comparisons.
+         *
+         * Differences from previous version:
+         * - allows lower-case property and enumeration values [optional]
+         * - enforces that FREQ appears first
+         * - enforces that only one of UNTIL and COUNT may be specified
+         * - allows (but ignores) X-* parts
+         * - improved validation on various values (e.g. UNTIL timestamps)
+         * - error messages are more specific
+         */
+
+        /* TODO: replace with "if (freq != 0) throw" if nothing requires this */
+        resetFields();
+
+        int parseFlags = 0;
+        String[] parts;
+        if (ALLOW_LOWER_CASE) {
+            parts = recur.toUpperCase().split(";");
+        } else {
+            parts = recur.split(";");
+        }
+        for (String part : parts) {
+            int equalIndex = part.indexOf('=');
+            if (equalIndex <= 0) {
+                /* no '=' or no LHS */
+                throw new InvalidFormatException("Missing LHS in " + part);
+            }
+
+            String lhs = part.substring(0, equalIndex);
+            String rhs = part.substring(equalIndex + 1);
+            if (rhs.length() == 0) {
+                throw new InvalidFormatException("Missing RHS in " + part);
+            }
+
+            /*
+             * In lieu of a "switch" statement that allows string arguments, we use a
+             * map from strings to parsing functions.
+             */
+            PartParser parser = sParsePartMap.get(lhs);
+            if (parser == null) {
+                if (lhs.startsWith("X-")) {
+                    //Log.d(TAG, "Ignoring custom part " + lhs);
+                    continue;
+                }
+                throw new InvalidFormatException("Couldn't find parser for " + lhs);
+            } else {
+                int flag = parser.parsePart(rhs, this);
+                if ((parseFlags & flag) != 0) {
+                    throw new InvalidFormatException("Part " + lhs + " was specified twice");
+                }
+                if (parseFlags == 0 && flag != PARSED_FREQ) {
+                    throw new InvalidFormatException("FREQ must be specified first");
+                }
+                parseFlags |= flag;
+            }
+        }
+
+        // If not specified, week starts on Monday.
+        if ((parseFlags & PARSED_WKST) == 0) {
+            wkst = MO;
+        }
+
+        // FREQ is mandatory.
+        if ((parseFlags & PARSED_FREQ) == 0) {
+            throw new InvalidFormatException("Must specify a FREQ value");
+        }
+
+        // Can't have both UNTIL and COUNT.
+        if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) {
+            if (ONLY_ONE_UNTIL_COUNT) {
+                throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur);
+            } else {
+                Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur);
+            }
+        }
+    }
+
+    /**
+     * Base class for the RRULE part parsers.
+     */
+    abstract static class PartParser {
+        /**
+         * Parses a single part.
+         *
+         * @param value The right-hand-side of the part.
+         * @param er The EventRecurrence into which the result is stored.
+         * @return A bit value indicating which part was parsed.
+         */
+        public abstract int parsePart(String value, EventRecurrence er);
+
+        /**
+         * Parses an integer, with range-checking.
+         *
+         * @param str The string to parse.
+         * @param minVal Minimum allowed value.
+         * @param maxVal Maximum allowed value.
+         * @param allowZero Is 0 allowed?
+         * @return The parsed value.
+         */
+        public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) {
+            try {
+                if (str.charAt(0) == '+') {
+                    // Integer.parseInt does not allow a leading '+', so skip it manually.
+                    str = str.substring(1);
+                }
+                int val = Integer.parseInt(str);
+                if (val < minVal || val > maxVal || (val == 0 && !allowZero)) {
+                    throw new InvalidFormatException("Integer value out of range: " + str);
+                }
+                return val;
+            } catch (NumberFormatException nfe) {
+                throw new InvalidFormatException("Invalid integer value: " + str);
+            }
+        }
+
+        /**
+         * Parses a comma-separated list of integers, with range-checking.
+         *
+         * @param listStr The string to parse.
+         * @param minVal Minimum allowed value.
+         * @param maxVal Maximum allowed value.
+         * @param allowZero Is 0 allowed?
+         * @return A new array with values, sized to hold the exact number of elements.
+         */
+        public static int[] parseNumberList(String listStr, int minVal, int maxVal,
+                boolean allowZero) {
+            int[] values;
+
+            if (listStr.indexOf(",") < 0) {
+                // Common case: only one entry, skip split() overhead.
+                values = new int[1];
+                values[0] = parseIntRange(listStr, minVal, maxVal, allowZero);
+            } else {
+                String[] valueStrs = listStr.split(",");
+                int len = valueStrs.length;
+                values = new int[len];
+                for (int i = 0; i < len; i++) {
+                    values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero);
+                }
+            }
+            return values;
+        }
+   }
+
+    /** parses FREQ={SECONDLY,MINUTELY,...} */
+    private static class ParseFreq extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            Integer freq = sParseFreqMap.get(value);
+            if (freq == null) {
+                throw new InvalidFormatException("Invalid FREQ value: " + value);
+            }
+            er.freq = freq;
+            return PARSED_FREQ;
+        }
+    }
+    /** parses UNTIL=enddate, e.g. "19970829T021400" */
+    private static class ParseUntil extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            if (VALIDATE_UNTIL) {
+                try {
+                    // Parse the time to validate it.  The result isn't retained.
+                    Time until = new Time();
+                    until.parse(value);
+                } catch (TimeFormatException tfe) {
+                    throw new InvalidFormatException("Invalid UNTIL value: " + value);
+                }
+            }
+            er.until = value;
+            return PARSED_UNTIL;
+        }
+    }
+    /** parses COUNT=[non-negative-integer] */
+    private static class ParseCount extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            er.count = parseIntRange(value, 0, Integer.MAX_VALUE, true);
+            return PARSED_COUNT;
+        }
+    }
+    /** parses INTERVAL=[non-negative-integer] */
+    private static class ParseInterval extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            er.interval = parseIntRange(value, 1, Integer.MAX_VALUE, false);
+            return PARSED_INTERVAL;
+        }
+    }
+    /** parses BYSECOND=byseclist */
+    private static class ParseBySecond extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] bysecond = parseNumberList(value, 0, 59, true);
+            er.bysecond = bysecond;
+            er.bysecondCount = bysecond.length;
+            return PARSED_BYSECOND;
+        }
+    }
+    /** parses BYMINUTE=byminlist */
+    private static class ParseByMinute extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] byminute = parseNumberList(value, 0, 59, true);
+            er.byminute = byminute;
+            er.byminuteCount = byminute.length;
+            return PARSED_BYMINUTE;
+        }
+    }
+    /** parses BYHOUR=byhrlist */
+    private static class ParseByHour extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] byhour = parseNumberList(value, 0, 23, true);
+            er.byhour = byhour;
+            er.byhourCount = byhour.length;
+            return PARSED_BYHOUR;
+        }
+    }
+    /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */
+    private static class ParseByDay extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] byday;
+            int[] bydayNum;
+            int bydayCount;
+
+            if (value.indexOf(",") < 0) {
+                /* only one entry, skip split() overhead */
+                bydayCount = 1;
+                byday = new int[1];
+                bydayNum = new int[1];
+                parseWday(value, byday, bydayNum, 0);
+            } else {
+                String[] wdays = value.split(",");
+                int len = wdays.length;
+                bydayCount = len;
+                byday = new int[len];
+                bydayNum = new int[len];
+                for (int i = 0; i < len; i++) {
+                    parseWday(wdays[i], byday, bydayNum, i);
+                }
+            }
+            er.byday = byday;
+            er.bydayNum = bydayNum;
+            er.bydayCount = bydayCount;
+            return PARSED_BYDAY;
+        }
+
+        /** parses [int]weekday, putting the pieces into parallel array entries */
+        private static void parseWday(String str, int[] byday, int[] bydayNum, int index) {
+            int wdayStrStart = str.length() - 2;
+            String wdayStr;
+
+            if (wdayStrStart > 0) {
+                /* number is included; parse it out and advance to weekday */
+                String numPart = str.substring(0, wdayStrStart);
+                int num = parseIntRange(numPart, -53, 53, false);
+                bydayNum[index] = num;
+                wdayStr = str.substring(wdayStrStart);
+            } else {
+                /* just the weekday string */
+                wdayStr = str;
+            }
+            Integer wday = sParseWeekdayMap.get(wdayStr);
+            if (wday == null) {
+                throw new InvalidFormatException("Invalid BYDAY value: " + str);
+            }
+            byday[index] = wday;
+        }
+    }
+    /** parses BYMONTHDAY=bymodaylist */
+    private static class ParseByMonthDay extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] bymonthday = parseNumberList(value, -31, 31, false);
+            er.bymonthday = bymonthday;
+            er.bymonthdayCount = bymonthday.length;
+            return PARSED_BYMONTHDAY;
+        }
+    }
+    /** parses BYYEARDAY=byyrdaylist */
+    private static class ParseByYearDay extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] byyearday = parseNumberList(value, -366, 366, false);
+            er.byyearday = byyearday;
+            er.byyeardayCount = byyearday.length;
+            return PARSED_BYYEARDAY;
+        }
+    }
+    /** parses BYWEEKNO=bywknolist */
+    private static class ParseByWeekNo extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] byweekno = parseNumberList(value, -53, 53, false);
+            er.byweekno = byweekno;
+            er.byweeknoCount = byweekno.length;
+            return PARSED_BYWEEKNO;
+        }
+    }
+    /** parses BYMONTH=bymolist */
+    private static class ParseByMonth extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] bymonth = parseNumberList(value, 1, 12, false);
+            er.bymonth = bymonth;
+            er.bymonthCount = bymonth.length;
+            return PARSED_BYMONTH;
+        }
+    }
+    /** parses BYSETPOS=bysplist */
+    private static class ParseBySetPos extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
+            er.bysetpos = bysetpos;
+            er.bysetposCount = bysetpos.length;
+            return PARSED_BYSETPOS;
+        }
+    }
+    /** parses WKST={SU,MO,...} */
+    private static class ParseWkst extends PartParser {
+        @Override public int parsePart(String value, EventRecurrence er) {
+            Integer wkst = sParseWeekdayMap.get(value);
+            if (wkst == null) {
+                throw new InvalidFormatException("Invalid WKST value: " + value);
+            }
+            er.wkst = wkst;
+            return PARSED_WKST;
+        }
+    }
+}
diff --git a/src/com/android/calendarcommon/ICalendar.java b/src/com/android/calendarcommon/ICalendar.java
new file mode 100644
index 0000000..ab77ed8
--- /dev/null
+++ b/src/com/android/calendarcommon/ICalendar.java
@@ -0,0 +1,660 @@
+/*
+ * 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.util.Log;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.ArrayList;
+
+/**
+ * Parses RFC 2445 iCalendar objects.
+ */
+public class ICalendar {
+
+    private static final String TAG = "Sync";
+
+    // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM
+    // components, by type field or by subclass?  subclass would allow us to
+    // enforce grammars.
+
+    /**
+     * Exception thrown when an iCalendar object has invalid syntax.
+     */
+    public static class FormatException extends Exception {
+        public FormatException() {
+            super();
+        }
+
+        public FormatException(String msg) {
+            super(msg);
+        }
+
+        public FormatException(String msg, Throwable cause) {
+            super(msg, cause);
+        }
+    }
+
+    /**
+     * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY,
+     * VTIMEZONE, VALARM).
+     */
+    public static class Component {
+
+        // components
+        private static final String BEGIN = "BEGIN";
+        private static final String END = "END";
+        private static final String NEWLINE = "\n";
+        public static final String VCALENDAR = "VCALENDAR";
+        public static final String VEVENT = "VEVENT";
+        public static final String VTODO = "VTODO";
+        public static final String VJOURNAL = "VJOURNAL";
+        public static final String VFREEBUSY = "VFREEBUSY";
+        public static final String VTIMEZONE = "VTIMEZONE";
+        public static final String VALARM = "VALARM";
+
+        private final String mName;
+        private final Component mParent; // see if we can get rid of this
+        private LinkedList<Component> mChildren = null;
+        private final LinkedHashMap<String, ArrayList<Property>> mPropsMap =
+                new LinkedHashMap<String, ArrayList<Property>>();
+
+        /**
+         * Creates a new component with the provided name.
+         * @param name The name of the component.
+         */
+        public Component(String name, Component parent) {
+            mName = name;
+            mParent = parent;
+        }
+
+        /**
+         * Returns the name of the component.
+         * @return The name of the component.
+         */
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Returns the parent of this component.
+         * @return The parent of this component.
+         */
+        public Component getParent() {
+            return mParent;
+        }
+
+        /**
+         * Helper that lazily gets/creates the list of children.
+         * @return The list of children.
+         */
+        protected LinkedList<Component> getOrCreateChildren() {
+            if (mChildren == null) {
+                mChildren = new LinkedList<Component>();
+            }
+            return mChildren;
+        }
+
+        /**
+         * Adds a child component to this component.
+         * @param child The child component.
+         */
+        public void addChild(Component child) {
+            getOrCreateChildren().add(child);
+        }
+
+        /**
+         * Returns a list of the Component children of this component.  May be
+         * null, if there are no children.
+         *
+         * @return A list of the children.
+         */
+        public List<Component> getComponents() {
+            return mChildren;
+        }
+
+        /**
+         * Adds a Property to this component.
+         * @param prop
+         */
+        public void addProperty(Property prop) {
+            String name= prop.getName();
+            ArrayList<Property> props = mPropsMap.get(name);
+            if (props == null) {
+                props = new ArrayList<Property>();
+                mPropsMap.put(name, props);
+            }
+            props.add(prop);
+        }
+
+        /**
+         * Returns a set of the property names within this component.
+         * @return A set of property names within this component.
+         */
+        public Set<String> getPropertyNames() {
+            return mPropsMap.keySet();
+        }
+
+        /**
+         * Returns a list of properties with the specified name.  Returns null
+         * if there are no such properties.
+         * @param name The name of the property that should be returned.
+         * @return A list of properties with the requested name.
+         */
+        public List<Property> getProperties(String name) {
+            return mPropsMap.get(name);
+        }
+
+        /**
+         * Returns the first property with the specified name.  Returns null
+         * if there is no such property.
+         * @param name The name of the property that should be returned.
+         * @return The first property with the specified name.
+         */
+        public Property getFirstProperty(String name) {
+            List<Property> props = mPropsMap.get(name);
+            if (props == null || props.size() == 0) {
+                return null;
+            }
+            return props.get(0);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            toString(sb);
+            sb.append(NEWLINE);
+            return sb.toString();
+        }
+
+        /**
+         * Helper method that appends this component to a StringBuilder.  The
+         * caller is responsible for appending a newline at the end of the
+         * component.
+         */
+        public void toString(StringBuilder sb) {
+            sb.append(BEGIN);
+            sb.append(":");
+            sb.append(mName);
+            sb.append(NEWLINE);
+
+            // append the properties
+            for (String propertyName : getPropertyNames()) {
+                for (Property property : getProperties(propertyName)) {
+                    property.toString(sb);
+                    sb.append(NEWLINE);
+                }
+            }
+
+            // append the sub-components
+            if (mChildren != null) {
+                for (Component component : mChildren) {
+                    component.toString(sb);
+                    sb.append(NEWLINE);
+                }
+            }
+
+            sb.append(END);
+            sb.append(":");
+            sb.append(mName);
+        }
+    }
+
+    /**
+     * A property within an iCalendar component (e.g., DTSTART, DTEND, etc.,
+     * within a VEVENT).
+     */
+    public static class Property {
+        // properties
+        // TODO: do we want to list these here?  the complete list is long.
+        public static final String DTSTART = "DTSTART";
+        public static final String DTEND = "DTEND";
+        public static final String DURATION = "DURATION";
+        public static final String RRULE = "RRULE";
+        public static final String RDATE = "RDATE";
+        public static final String EXRULE = "EXRULE";
+        public static final String EXDATE = "EXDATE";
+        // ... need to add more.
+        
+        private final String mName;
+        private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap =
+                new LinkedHashMap<String, ArrayList<Parameter>>();
+        private String mValue; // TODO: make this final?
+
+        /**
+         * Creates a new property with the provided name.
+         * @param name The name of the property.
+         */
+        public Property(String name) {
+            mName = name;
+        }
+
+        /**
+         * Creates a new property with the provided name and value.
+         * @param name The name of the property.
+         * @param value The value of the property.
+         */
+        public Property(String name, String value) {
+            mName = name;
+            mValue = value;
+        }
+
+        /**
+         * Returns the name of the property.
+         * @return The name of the property.
+         */
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Returns the value of this property.
+         * @return The value of this property.
+         */
+        public String getValue() {
+            return mValue;
+        }
+
+        /**
+         * Sets the value of this property.
+         * @param value The desired value for this property.
+         */
+        public void setValue(String value) {
+            mValue = value;
+        }        
+
+        /**
+         * Adds a {@link Parameter} to this property.
+         * @param param The parameter that should be added.
+         */
+        public void addParameter(Parameter param) {
+            ArrayList<Parameter> params = mParamsMap.get(param.name);
+            if (params == null) {
+                params = new ArrayList<Parameter>();
+                mParamsMap.put(param.name, params);
+            }
+            params.add(param);
+        }
+
+        /**
+         * Returns the set of parameter names for this property.
+         * @return The set of parameter names for this property.
+         */
+        public Set<String> getParameterNames() {
+            return mParamsMap.keySet();
+        }
+
+        /**
+         * Returns the list of parameters with the specified name.  May return
+         * null if there are no such parameters.
+         * @param name The name of the parameters that should be returned.
+         * @return The list of parameters with the specified name.
+         */
+        public List<Parameter> getParameters(String name) {
+            return mParamsMap.get(name);
+        }
+
+        /**
+         * Returns the first parameter with the specified name.  May return
+         * nll if there is no such parameter.
+         * @param name The name of the parameter that should be returned.
+         * @return The first parameter with the specified name.
+         */
+        public Parameter getFirstParameter(String name) {
+            ArrayList<Parameter> params = mParamsMap.get(name);
+            if (params == null || params.size() == 0) {
+                return null;
+            }
+            return params.get(0);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            toString(sb);
+            return sb.toString();
+        }
+
+        /**
+         * Helper method that appends this property to a StringBuilder.  The
+         * caller is responsible for appending a newline after this property.
+         */
+        public void toString(StringBuilder sb) {
+            sb.append(mName);
+            Set<String> parameterNames = getParameterNames();
+            for (String parameterName : parameterNames) {
+                for (Parameter param : getParameters(parameterName)) {
+                    sb.append(";");
+                    param.toString(sb);
+                }
+            }
+            sb.append(":");
+            sb.append(mValue);
+        }
+    }
+
+    /**
+     * A parameter defined for an iCalendar property.
+     */
+    // TODO: make this a proper class rather than a struct?
+    public static class Parameter {
+        public String name;
+        public String value;
+
+        /**
+         * Creates a new empty parameter.
+         */
+        public Parameter() {
+        }
+
+        /**
+         * Creates a new parameter with the specified name and value.
+         * @param name The name of the parameter.
+         * @param value The value of the parameter.
+         */
+        public Parameter(String name, String value) {
+            this.name = name;
+            this.value = value;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            toString(sb);
+            return sb.toString();
+        }
+
+        /**
+         * Helper method that appends this parameter to a StringBuilder.
+         */
+        public void toString(StringBuilder sb) {
+            sb.append(name);
+            sb.append("=");
+            sb.append(value);
+        }
+    }
+
+    private static final class ParserState {
+        // public int lineNumber = 0;
+        public String line; // TODO: just point to original text
+        public int index;
+    }
+
+    // use factory method
+    private ICalendar() {
+    }
+
+    // TODO: get rid of this -- handle all of the parsing in one pass through
+    // the text.
+    private static String normalizeText(String text) {
+        // it's supposed to be \r\n, but not everyone does that
+        text = text.replaceAll("\r\n", "\n");
+        text = text.replaceAll("\r", "\n");
+
+        // we deal with line folding, by replacing all "\n " strings
+        // with nothing.  The RFC specifies "\r\n " to be folded, but
+        // we handle "\n " and "\r " too because we can get those.
+        text = text.replaceAll("\n ", "");
+
+        return text;
+    }
+
+    /**
+     * Parses text into an iCalendar component.  Parses into the provided
+     * component, if not null, or parses into a new component.  In the latter
+     * case, expects a BEGIN as the first line.  Returns the provided or newly
+     * created top-level component.
+     */
+    // TODO: use an index into the text, so we can make this a recursive
+    // function?
+    private static Component parseComponentImpl(Component component,
+                                                String text)
+            throws FormatException {
+        Component current = component;
+        ParserState state = new ParserState();
+        state.index = 0;
+
+        // split into lines
+        String[] lines = text.split("\n");
+
+        // each line is of the format:
+        // name *(";" param) ":" value
+        for (String line : lines) {
+            try {
+                current = parseLine(line, state, current);
+                // if the provided component was null, we will return the root
+                // NOTE: in this case, if the first line is not a BEGIN, a
+                // FormatException will get thrown.   
+                if (component == null) {
+                    component = current;
+                }
+            } catch (FormatException fe) {
+                if (false) {
+                    Log.v(TAG, "Cannot parse " + line, fe);
+                }
+                // for now, we ignore the parse error.  Google Calendar seems
+                // to be emitting some misformatted iCalendar objects.
+            }
+            continue;
+        }
+        return component;
+    }
+
+    /**
+     * Parses a line into the provided component.  Creates a new component if
+     * the line is a BEGIN, adding the newly created component to the provided
+     * parent.  Returns whatever component is the current one (to which new
+     * properties will be added) in the parse.
+     */
+    private static Component parseLine(String line, ParserState state,
+                                       Component component)
+            throws FormatException {
+        state.line = line;
+        int len = state.line.length();
+
+        // grab the name
+        char c = 0;
+        for (state.index = 0; state.index < len; ++state.index) {
+            c = line.charAt(state.index);
+            if (c == ';' || c == ':') {
+                break;
+            }
+        }
+        String name = line.substring(0, state.index);
+
+        if (component == null) {
+            if (!Component.BEGIN.equals(name)) {
+                throw new FormatException("Expected BEGIN");
+            }
+        }
+
+        Property property;
+        if (Component.BEGIN.equals(name)) {
+            // start a new component
+            String componentName = extractValue(state);
+            Component child = new Component(componentName, component);
+            if (component != null) {
+                component.addChild(child);
+            }
+            return child;
+        } else if (Component.END.equals(name)) {
+            // finish the current component
+            String componentName = extractValue(state);
+            if (component == null ||
+                    !componentName.equals(component.getName())) {
+                throw new FormatException("Unexpected END " + componentName);
+            }
+            return component.getParent();
+        } else {
+            property = new Property(name);
+        }
+
+        if (c == ';') {
+            Parameter parameter = null;
+            while ((parameter = extractParameter(state)) != null) {
+                property.addParameter(parameter);
+            }
+        }
+        String value = extractValue(state);
+        property.setValue(value);
+        component.addProperty(property);
+        return component;
+    }
+
+    /**
+     * Extracts the value ":..." on the current line.  The first character must
+     * be a ':'.
+     */
+    private static String extractValue(ParserState state)
+            throws FormatException {
+        String line = state.line;
+        if (state.index >= line.length() || line.charAt(state.index) != ':') {
+            throw new FormatException("Expected ':' before end of line in "
+                    + line);
+        }
+        String value = line.substring(state.index + 1);
+        state.index = line.length() - 1;
+        return value;
+    }
+
+    /**
+     * Extracts the next parameter from the line, if any.  If there are no more
+     * parameters, returns null.
+     */
+    private static Parameter extractParameter(ParserState state)
+            throws FormatException {
+        String text = state.line;
+        int len = text.length();
+        Parameter parameter = null;
+        int startIndex = -1;
+        int equalIndex = -1;
+        while (state.index < len) {
+            char c = text.charAt(state.index);
+            if (c == ':') {
+                if (parameter != null) {
+                    if (equalIndex == -1) {
+                        throw new FormatException("Expected '=' within "
+                                + "parameter in " + text);
+                    }
+                    parameter.value = text.substring(equalIndex + 1,
+                                                     state.index);
+                }
+                return parameter; // may be null
+            } else if (c == ';') {
+                if (parameter != null) {
+                    if (equalIndex == -1) {
+                        throw new FormatException("Expected '=' within "
+                                + "parameter in " + text);
+                    }
+                    parameter.value = text.substring(equalIndex + 1,
+                                                     state.index);
+                    return parameter;
+                } else {
+                    parameter = new Parameter();
+                    startIndex = state.index;
+                }
+            } else if (c == '=') {
+                equalIndex = state.index;
+                if ((parameter == null) || (startIndex == -1)) {
+                    throw new FormatException("Expected ';' before '=' in "
+                            + text);
+                }
+                parameter.name = text.substring(startIndex + 1, equalIndex);
+            } else if (c == '"') {
+                if (parameter == null) {
+                    throw new FormatException("Expected parameter before '\"' in " + text);
+                }
+                if (equalIndex == -1) {
+                    throw new FormatException("Expected '=' within parameter in " + text);
+                }
+                if (state.index > equalIndex + 1) {
+                    throw new FormatException("Parameter value cannot contain a '\"' in " + text);
+                }
+                final int endQuote = text.indexOf('"', state.index + 1);
+                if (endQuote < 0) {
+                    throw new FormatException("Expected closing '\"' in " + text);
+                }
+                parameter.value = text.substring(state.index + 1, endQuote);
+                state.index = endQuote + 1;
+                return parameter;
+            }
+            ++state.index;
+        }
+        throw new FormatException("Expected ':' before end of line in " + text);
+    }
+
+    /**
+     * Parses the provided text into an iCalendar object.  The top-level
+     * component must be of type VCALENDAR.
+     * @param text The text to be parsed.
+     * @return The top-level VCALENDAR component.
+     * @throws FormatException Thrown if the text could not be parsed into an
+     * iCalendar VCALENDAR object.
+     */
+    public static Component parseCalendar(String text) throws FormatException {
+        Component calendar = parseComponent(null, text);
+        if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) {
+            throw new FormatException("Expected " + Component.VCALENDAR);
+        }
+        return calendar;
+    }
+
+    /**
+     * Parses the provided text into an iCalendar event.  The top-level
+     * component must be of type VEVENT.
+     * @param text The text to be parsed.
+     * @return The top-level VEVENT component.
+     * @throws FormatException Thrown if the text could not be parsed into an
+     * iCalendar VEVENT.
+     */
+    public static Component parseEvent(String text) throws FormatException {
+        Component event = parseComponent(null, text);
+        if (event == null || !Component.VEVENT.equals(event.getName())) {
+            throw new FormatException("Expected " + Component.VEVENT);
+        }
+        return event;
+    }
+
+    /**
+     * Parses the provided text into an iCalendar component.
+     * @param text The text to be parsed.
+     * @return The top-level component.
+     * @throws FormatException Thrown if the text could not be parsed into an
+     * iCalendar component.
+     */
+    public static Component parseComponent(String text) throws FormatException {
+        return parseComponent(null, text);
+    }
+
+    /**
+     * Parses the provided text, adding to the provided component.
+     * @param component The component to which the parsed iCalendar data should
+     * be added.
+     * @param text The text to be parsed.
+     * @return The top-level component.
+     * @throws FormatException Thrown if the text could not be parsed as an
+     * iCalendar object.
+     */
+    public static Component parseComponent(Component component, String text)
+        throws FormatException {
+        text = normalizeText(text);
+        return parseComponentImpl(component, text);
+    }
+}
diff --git a/src/com/android/calendarcommon/RecurrenceSet.java b/src/com/android/calendarcommon/RecurrenceSet.java
new file mode 100644
index 0000000..2c6200f
--- /dev/null
+++ b/src/com/android/calendarcommon/RecurrenceSet.java
@@ -0,0 +1,511 @@
+/*
+ * 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 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 = "CalendarProvider";
+
+    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) {
+        // 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'.
+            time.parse(rawDates[i]);
+            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) {
+        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;
+    }
+
+    // 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);
+        }
+        String duration = values.getAsString(CalendarContract.Events.DURATION);
+        String tzid = values.getAsString(CalendarContract.Events.EVENT_TIMEZONE);
+        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);
+        Integer allDayInteger = values.getAsInteger(CalendarContract.Events.ALL_DAY);
+        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;
+    }
+
+    private 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("");
+    }
+
+    private 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();
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..414081a
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,24 @@
+# Copyright (C) 2011 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := CalendarCommonTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := calendar-common
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..ab1240b
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.calendarcommon.tests"
+        android:sharedUserId="com.android.uid.test">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.calendarcommon.tests"
+            android:label="TestRunner letting users run CalendarCommon Library Tests" />
+</manifest>
diff --git a/tests/src/com/android/calendarcommon/EventRecurrenceTest.java b/tests/src/com/android/calendarcommon/EventRecurrenceTest.java
new file mode 100644
index 0000000..3450389
--- /dev/null
+++ b/tests/src/com/android/calendarcommon/EventRecurrenceTest.java
@@ -0,0 +1,754 @@
+/*
+ * Copyright (C) 2006 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 com.android.calendarcommon.EventRecurrence.InvalidFormatException;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+/**
+ * Test android.pim.EventRecurrence.
+ *
+ * adb shell am instrument -w -e class android.pim.EventRecurrenceTest \
+ *   com.android.frameworks.coretests/android.test.InstrumentationTestRunner
+ */
+public class EventRecurrenceTest extends TestCase {
+
+    @SmallTest
+    public void test0() throws Exception {
+        verifyRecurType("FREQ=SECONDLY",
+                /* int freq */         EventRecurrence.SECONDLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test1() throws Exception {
+        verifyRecurType("FREQ=MINUTELY",
+                /* int freq */         EventRecurrence.MINUTELY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test2() throws Exception {
+        verifyRecurType("FREQ=HOURLY",
+                /* int freq */         EventRecurrence.HOURLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test3() throws Exception {
+        verifyRecurType("FREQ=DAILY",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test4() throws Exception {
+        verifyRecurType("FREQ=WEEKLY",
+                /* int freq */         EventRecurrence.WEEKLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test5() throws Exception {
+        verifyRecurType("FREQ=MONTHLY",
+                /* int freq */         EventRecurrence.MONTHLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test6() throws Exception {
+        verifyRecurType("FREQ=YEARLY",
+                /* int freq */         EventRecurrence.YEARLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test7() throws Exception {
+        // with an until
+        verifyRecurType("FREQ=DAILY;UNTIL=112233T223344Z",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     "112233T223344Z",
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test8() throws Exception {
+        // with a count
+        verifyRecurType("FREQ=DAILY;COUNT=334",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        334,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test9() throws Exception {
+        // with a count
+        verifyRecurType("FREQ=DAILY;INTERVAL=5000",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     5000,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @SmallTest
+    public void test10() throws Exception {
+        // verifyRecurType all of the BY* ones with one element
+        verifyRecurType("FREQ=DAILY"
+                + ";BYSECOND=0"
+                + ";BYMINUTE=1"
+                + ";BYHOUR=2"
+                + ";BYMONTHDAY=30"
+                + ";BYYEARDAY=300"
+                + ";BYWEEKNO=53"
+                + ";BYMONTH=12"
+                + ";BYSETPOS=-15"
+                + ";WKST=SU",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   new int[]{0},
+                /* int[] byminute */   new int[]{1},
+                /* int[] byhour */     new int[]{2},
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ new int[]{30},
+                /* int[] byyearday */  new int[]{300},
+                /* int[] byweekno */   new int[]{53},
+                /* int[] bymonth */    new int[]{12},
+                /* int[] bysetpos */   new int[]{-15},
+                /* int wkst */         EventRecurrence.SU
+        );
+    }
+
+    @SmallTest
+    public void test11() throws Exception {
+        // verifyRecurType all of the BY* ones with one element
+        verifyRecurType("FREQ=DAILY"
+                + ";BYSECOND=0,30,59"
+                + ";BYMINUTE=0,41,59"
+                + ";BYHOUR=0,4,23"
+                + ";BYMONTHDAY=-31,-1,1,31"
+                + ";BYYEARDAY=-366,-1,1,366"
+                + ";BYWEEKNO=-53,-1,1,53"
+                + ";BYMONTH=1,12"
+                + ";BYSETPOS=1,2,3,4,500,10000"
+                + ";WKST=SU",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   new int[]{0, 30, 59},
+                /* int[] byminute */   new int[]{0, 41, 59},
+                /* int[] byhour */     new int[]{0, 4, 23},
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ new int[]{-31, -1, 1, 31},
+                /* int[] byyearday */  new int[]{-366, -1, 1, 366},
+                /* int[] byweekno */   new int[]{-53, -1, 1, 53},
+                /* int[] bymonth */    new int[]{1, 12},
+                /* int[] bysetpos */   new int[]{1, 2, 3, 4, 500, 10000},
+                /* int wkst */         EventRecurrence.SU
+        );
+    }
+
+    private static class Check {
+        Check(String k, int... v) {
+            key = k;
+            values = v;
+        }
+
+        String key;
+        int[] values;
+    }
+
+    // this is a negative verifyRecurType case to verifyRecurType the range of the numbers accepted
+    @SmallTest
+    public void test12() throws Exception {
+        Check[] checks = new Check[]{
+                new Check("BYSECOND", -100, -1, 60, 100),
+                new Check("BYMINUTE", -100, -1, 60, 100),
+                new Check("BYHOUR", -100, -1, 24, 100),
+                new Check("BYMONTHDAY", -100, -32, 0, 32, 100),
+                new Check("BYYEARDAY", -400, -367, 0, 367, 400),
+                new Check("BYWEEKNO", -100, -54, 0, 54, 100),
+                new Check("BYMONTH", -100, -5, 0, 13, 100)
+        };
+
+        for (Check ck : checks) {
+            for (int n : ck.values) {
+                String recur = "FREQ=DAILY;" + ck.key + "=" + n;
+                try {
+                    EventRecurrence er = new EventRecurrence();
+                    er.parse(recur);
+                    fail("Negative verifyRecurType failed. "
+                            + " parse failed to throw an exception for '"
+                            + recur + "'");
+                } catch (EventRecurrence.InvalidFormatException e) {
+                    // expected
+                }
+            }
+        }
+    }
+
+    // verifyRecurType BYDAY
+    @SmallTest
+    public void test13() throws Exception {
+        verifyRecurType("FREQ=DAILY;BYDAY=1SU,-2MO,+33TU,WE,TH,FR,SA",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      new int[] {
+                        EventRecurrence.SU,
+                        EventRecurrence.MO,
+                        EventRecurrence.TU,
+                        EventRecurrence.WE,
+                        EventRecurrence.TH,
+                        EventRecurrence.FR,
+                        EventRecurrence.SA
+                },
+                /* int[] bydayNum */   new int[]{1, -2, 33, 0, 0, 0, 0},
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    @Suppress
+    // Repro bug #2331761 - this should fail because of the last comma into BYDAY
+    public void test14() throws Exception {
+        verifyRecurType("FREQ=WEEKLY;WKST=MO;UNTIL=20100129T130000Z;INTERVAL=1;BYDAY=MO,TU,WE,",
+                /* int freq */         EventRecurrence.WEEKLY,
+                /* String until */     "20100129T130000Z",
+                /* int count */        0,
+                /* int interval */     1,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      new int[] {
+                        EventRecurrence.MO,
+                        EventRecurrence.TU,
+                        EventRecurrence.WE,
+                },
+                /* int[] bydayNum */   new int[]{0, 0, 0},
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    // This test should pass
+    public void test15() throws Exception {
+        verifyRecurType("FREQ=WEEKLY;WKST=MO;UNTIL=20100129T130000Z;INTERVAL=1;"
+                + "BYDAY=MO,TU,WE,TH,FR,SA,SU",
+                /* int freq */         EventRecurrence.WEEKLY,
+                /* String until */     "20100129T130000Z",
+                /* int count */        0,
+                /* int interval */     1,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      new int[] {
+                        EventRecurrence.MO,
+                        EventRecurrence.TU,
+                        EventRecurrence.WE,
+                        EventRecurrence.TH,
+                        EventRecurrence.FR,
+                        EventRecurrence.SA,
+                        EventRecurrence.SU
+                },
+                /* int[] bydayNum */   new int[]{0, 0, 0, 0, 0, 0, 0},
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    // Sample coming from RFC2445
+    public void test16() throws Exception {
+        verifyRecurType("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1",
+                /* int freq */         EventRecurrence.MONTHLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      new int[] {
+                        EventRecurrence.MO,
+                        EventRecurrence.TU,
+                        EventRecurrence.WE,
+                        EventRecurrence.TH,
+                        EventRecurrence.FR
+                },
+                /* int[] bydayNum */   new int[] {0, 0, 0, 0, 0},
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   new int[] { -1 },
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    // Sample coming from RFC2445
+    public void test17() throws Exception {
+        verifyRecurType("FREQ=DAILY;COUNT=10;INTERVAL=2",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        10,
+                /* int interval */     2,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    // Sample coming from RFC2445
+    public void test18() throws Exception {
+        verifyRecurType("FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10",
+                /* int freq */         EventRecurrence.YEARLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      new int[] {
+                        EventRecurrence.SU
+                },
+                /* int[] bydayNum */   new int[] { -1 },
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    new int[] { 10 },
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    // Sample coming from bug #1640517
+    public void test19() throws Exception {
+        verifyRecurType("FREQ=YEARLY;BYMONTH=3;BYDAY=TH",
+                /* int freq */         EventRecurrence.YEARLY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      new int[] {
+                        EventRecurrence.TH
+                },
+                /* int[] bydayNum */   new int[] { 0 },
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    new int[] { 3 },
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    // for your copying pleasure
+    public void fakeTestXX() throws Exception {
+        verifyRecurType("FREQ=DAILY;",
+                /* int freq */         EventRecurrence.DAILY,
+                /* String until */     null,
+                /* int count */        0,
+                /* int interval */     0,
+                /* int[] bysecond */   null,
+                /* int[] byminute */   null,
+                /* int[] byhour */     null,
+                /* int[] byday */      null,
+                /* int[] bydayNum */   null,
+                /* int[] bymonthday */ null,
+                /* int[] byyearday */  null,
+                /* int[] byweekno */   null,
+                /* int[] bymonth */    null,
+                /* int[] bysetpos */   null,
+                /* int wkst */         EventRecurrence.MO
+        );
+    }
+
+    private static void cmp(int vlen, int[] v, int[] correct, String name) {
+        if ((correct == null && v != null)
+                || (correct != null && v == null)) {
+            throw new RuntimeException("One is null, one isn't for " + name
+                    + ": correct=" + Arrays.toString(correct)
+                    + " actual=" + Arrays.toString(v));
+        }
+        if ((correct == null && vlen != 0)
+                || (vlen != (correct == null ? 0 : correct.length))) {
+            throw new RuntimeException("Reported length mismatch for " + name
+                    + ": correct=" + ((correct == null) ? "null" : correct.length)
+                    + " actual=" + vlen);
+        }
+        if (correct == null) {
+            return;
+        }
+        if (v.length < correct.length) {
+            throw new RuntimeException("Array length mismatch for " + name
+                    + ": correct=" + Arrays.toString(correct)
+                    + " actual=" + Arrays.toString(v));
+        }
+        for (int i = 0; i < correct.length; i++) {
+            if (v[i] != correct[i]) {
+                throw new RuntimeException("Array value mismatch for " + name
+                        + ": correct=" + Arrays.toString(correct)
+                        + " actual=" + Arrays.toString(v));
+            }
+        }
+    }
+
+    private static boolean eq(String a, String b) {
+        if ((a == null && b != null) || (a != null && b == null)) {
+            return false;
+        } else {
+            return a == b || a.equals(b);
+        }
+    }
+
+    private static void verifyRecurType(String recur,
+            int freq, String until, int count, int interval,
+            int[] bysecond, int[] byminute, int[] byhour,
+            int[] byday, int[] bydayNum, int[] bymonthday,
+            int[] byyearday, int[] byweekno, int[] bymonth,
+            int[] bysetpos, int wkst) {
+        EventRecurrence eventRecurrence = new EventRecurrence();
+        eventRecurrence.parse(recur);
+        if (eventRecurrence.freq != freq
+                || !eq(eventRecurrence.until, until)
+                || eventRecurrence.count != count
+                || eventRecurrence.interval != interval
+                || eventRecurrence.wkst != wkst) {
+            System.out.println("Error... got:");
+            print(eventRecurrence);
+            System.out.println("expected:");
+            System.out.println("{");
+            System.out.println("    freq=" + freq);
+            System.out.println("    until=" + until);
+            System.out.println("    count=" + count);
+            System.out.println("    interval=" + interval);
+            System.out.println("    wkst=" + wkst);
+            System.out.println("    bysecond=" + Arrays.toString(bysecond));
+            System.out.println("    byminute=" + Arrays.toString(byminute));
+            System.out.println("    byhour=" + Arrays.toString(byhour));
+            System.out.println("    byday=" + Arrays.toString(byday));
+            System.out.println("    bydayNum=" + Arrays.toString(bydayNum));
+            System.out.println("    bymonthday=" + Arrays.toString(bymonthday));
+            System.out.println("    byyearday=" + Arrays.toString(byyearday));
+            System.out.println("    byweekno=" + Arrays.toString(byweekno));
+            System.out.println("    bymonth=" + Arrays.toString(bymonth));
+            System.out.println("    bysetpos=" + Arrays.toString(bysetpos));
+            System.out.println("}");
+            throw new RuntimeException("Mismatch in fields");
+        }
+        cmp(eventRecurrence.bysecondCount, eventRecurrence.bysecond, bysecond, "bysecond");
+        cmp(eventRecurrence.byminuteCount, eventRecurrence.byminute, byminute, "byminute");
+        cmp(eventRecurrence.byhourCount, eventRecurrence.byhour, byhour, "byhour");
+        cmp(eventRecurrence.bydayCount, eventRecurrence.byday, byday, "byday");
+        cmp(eventRecurrence.bydayCount, eventRecurrence.bydayNum, bydayNum, "bydayNum");
+        cmp(eventRecurrence.bymonthdayCount, eventRecurrence.bymonthday, bymonthday, "bymonthday");
+        cmp(eventRecurrence.byyeardayCount, eventRecurrence.byyearday, byyearday, "byyearday");
+        cmp(eventRecurrence.byweeknoCount, eventRecurrence.byweekno, byweekno, "byweekno");
+        cmp(eventRecurrence.bymonthCount, eventRecurrence.bymonth, bymonth, "bymonth");
+        cmp(eventRecurrence.bysetposCount, eventRecurrence.bysetpos, bysetpos, "bysetpos");
+    }
+
+    private static void print(EventRecurrence er) {
+        System.out.println("{");
+        System.out.println("    freq=" + er.freq);
+        System.out.println("    until=" + er.until);
+        System.out.println("    count=" + er.count);
+        System.out.println("    interval=" + er.interval);
+        System.out.println("    wkst=" + er.wkst);
+        System.out.println("    bysecond=" + Arrays.toString(er.bysecond));
+        System.out.println("    bysecondCount=" + er.bysecondCount);
+        System.out.println("    byminute=" + Arrays.toString(er.byminute));
+        System.out.println("    byminuteCount=" + er.byminuteCount);
+        System.out.println("    byhour=" + Arrays.toString(er.byhour));
+        System.out.println("    byhourCount=" + er.byhourCount);
+        System.out.println("    byday=" + Arrays.toString(er.byday));
+        System.out.println("    bydayNum=" + Arrays.toString(er.bydayNum));
+        System.out.println("    bydayCount=" + er.bydayCount);
+        System.out.println("    bymonthday=" + Arrays.toString(er.bymonthday));
+        System.out.println("    bymonthdayCount=" + er.bymonthdayCount);
+        System.out.println("    byyearday=" + Arrays.toString(er.byyearday));
+        System.out.println("    byyeardayCount=" + er.byyeardayCount);
+        System.out.println("    byweekno=" + Arrays.toString(er.byweekno));
+        System.out.println("    byweeknoCount=" + er.byweeknoCount);
+        System.out.println("    bymonth=" + Arrays.toString(er.bymonth));
+        System.out.println("    bymonthCount=" + er.bymonthCount);
+        System.out.println("    bysetpos=" + Arrays.toString(er.bysetpos));
+        System.out.println("    bysetposCount=" + er.bysetposCount);
+        System.out.println("}");
+    }
+
+
+    /** A list of valid rules.  The parser must accept these. */
+    private static final String[] GOOD_RRULES = {
+        /* extracted wholesale from from RFC 2445 section 4.8.5.4 */
+        "FREQ=DAILY;COUNT=10",
+        "FREQ=DAILY;UNTIL=19971224T000000Z",
+        "FREQ=DAILY;INTERVAL=2",
+        "FREQ=DAILY;INTERVAL=10;COUNT=5",
+        "FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA",
+        "FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1",
+        "FREQ=WEEKLY;COUNT=10",
+        "FREQ=WEEKLY;UNTIL=19971224T000000Z",
+        "FREQ=WEEKLY;INTERVAL=2;WKST=SU",
+        "FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH",
+        "FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH",
+        "FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR",
+        "FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH",
+        "FREQ=MONTHLY;COUNT=10;BYDAY=1FR",
+        "FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR",
+        "FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU",
+        "FREQ=MONTHLY;COUNT=6;BYDAY=-2MO",
+        "FREQ=MONTHLY;BYMONTHDAY=-3",
+        "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15",
+        "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1",
+        "FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15",
+        "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU",
+        "FREQ=YEARLY;COUNT=10;BYMONTH=6,7",
+        "FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3",
+        "FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200",
+        "FREQ=YEARLY;BYDAY=20MO",
+        "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO",
+        "FREQ=YEARLY;BYMONTH=3;BYDAY=TH",
+        "FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8",
+        "FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13",
+        "FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13",
+        "FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8",
+        "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3",
+        "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2",
+        "FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z",
+        "FREQ=MINUTELY;INTERVAL=15;COUNT=6",
+        "FREQ=MINUTELY;INTERVAL=90;COUNT=4",
+        "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40",
+        "FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16",
+        "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO",
+        "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU",
+        /* a few more */
+        "FREQ=SECONDLY;BYSECOND=0,15,59",
+        "FREQ=MINUTELY;BYMINUTE=0,15,59",
+        "FREQ=HOURLY;BYHOUR=+0,+15,+23",
+        "FREQ=DAILY;X-WHATEVER=blah",                       // fails on old parser
+        //"freq=daily;wkst=su",                               // fails on old parser
+    };
+
+    /** The parser must reject these. */
+    private static final String[] BAD_RRULES = {
+        "INTERVAL=4;FREQ=YEARLY",                           // FREQ must come first
+        "FREQ=MONTHLY;FREQ=MONTHLY",                        // can't specify twice
+        "FREQ=MONTHLY;COUNT=1;COUNT=1",                     // can't specify twice
+        "FREQ=SECONDLY;BYSECOND=60",                        // range
+        "FREQ=MINUTELY;BYMINUTE=-1",                        // range
+        "FREQ=HOURLY;BYHOUR=24",                            // range
+        "FREQ=YEARLY;BYMONTHDAY=0",                         // zero not valid
+        //"FREQ=YEARLY;COUNT=1;UNTIL=12345",                  // can't have both COUNT and UNTIL
+        //"FREQ=DAILY;UNTIL=19970829T021400e",                // invalid date
+    };
+
+    /**
+     * Simple test of good/bad rules.
+     */
+    @SmallTest
+    public void testBasicParse() {
+        for (String rule : GOOD_RRULES) {
+            EventRecurrence recur = new EventRecurrence();
+            recur.parse(rule);
+        }
+
+        for (String rule : BAD_RRULES) {
+            EventRecurrence recur = new EventRecurrence();
+            boolean didThrow = false;
+
+            try {
+                recur.parse(rule);
+            } catch (InvalidFormatException ife) {
+                didThrow = true;
+            }
+
+            assertTrue("Expected throw on " + rule, didThrow);
+        }
+    }
+}
diff --git a/tests/src/com/android/calendarcommon/RecurrenceSetTest.java b/tests/src/com/android/calendarcommon/RecurrenceSetTest.java
new file mode 100644
index 0000000..8382db8
--- /dev/null
+++ b/tests/src/com/android/calendarcommon/RecurrenceSetTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2009 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 com.android.calendarcommon.ICalendar;
+import com.android.calendarcommon.RecurrenceSet;
+
+import android.content.ContentValues;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+import android.provider.CalendarContract;
+import junit.framework.TestCase;
+
+/**
+ * Test some pim.RecurrenceSet functionality.
+ */
+public class RecurrenceSetTest extends TestCase {
+
+    // Test a recurrence
+    @SmallTest
+    public void testRecurrenceSet0() throws Exception {
+        String recurrence = "DTSTART;TZID=America/New_York:20080221T070000\n"
+                + "DTEND;TZID=America/New_York:20080221T190000\n"
+                + "RRULE:FREQ=DAILY;UNTIL=20080222T000000Z\n"
+                + "EXDATE:20080222T120000Z";
+        verifyPopulateContentValues(recurrence, "FREQ=DAILY;UNTIL=20080222T000000Z", null,
+                null, "20080222T120000Z", 1203595200000L, "America/New_York", "P43200S", 0);
+    }
+
+    // Test 1 day all-day event
+    @SmallTest
+    public void testRecurrenceSet1() throws Exception {
+        String recurrence = "DTSTART;VALUE=DATE:20090821\nDTEND;VALUE=DATE:20090822\n"
+                + "RRULE:FREQ=YEARLY;WKST=SU";
+        verifyPopulateContentValues(recurrence, "FREQ=YEARLY;WKST=SU", null,
+                null, null, 1250812800000L, "UTC", "P1D", 1);
+    }
+
+    // Test 2 day all-day event
+    @SmallTest
+    public void testRecurrenceSet2() throws Exception {
+        String recurrence = "DTSTART;VALUE=DATE:20090821\nDTEND;VALUE=DATE:20090823\n"
+                + "RRULE:FREQ=YEARLY;WKST=SU";
+        verifyPopulateContentValues(recurrence, "FREQ=YEARLY;WKST=SU", null,
+                null, null, 1250812800000L, "UTC",  "P2D", 1);
+    }
+
+    // run populateContentValues and verify the results
+    private void verifyPopulateContentValues(String recurrence, String rrule, String rdate,
+            String exrule, String exdate, long dtstart, String tzid, String duration, int allDay)
+            throws ICalendar.FormatException {
+        ICalendar.Component recurrenceComponent =
+                new ICalendar.Component("DUMMY", null /* parent */);
+        ICalendar.parseComponent(recurrenceComponent, recurrence);
+        ContentValues values = new ContentValues();
+        RecurrenceSet.populateContentValues(recurrenceComponent, values);
+        Log.d("KS", "values " + values);
+
+        assertEquals(rrule, values.get(android.provider.CalendarContract.Events.RRULE));
+        assertEquals(rdate, values.get(android.provider.CalendarContract.Events.RDATE));
+        assertEquals(exrule, values.get(android.provider.CalendarContract.Events.EXRULE));
+        assertEquals(exdate, values.get(android.provider.CalendarContract.Events.EXDATE));
+        assertEquals(dtstart, (long) values.getAsLong(CalendarContract.Events.DTSTART));
+        assertEquals(tzid, values.get(android.provider.CalendarContract.Events.EVENT_TIMEZONE));
+        assertEquals(duration, values.get(android.provider.CalendarContract.Events.DURATION));
+        assertEquals(allDay,
+                (int) values.getAsInteger(android.provider.CalendarContract.Events.ALL_DAY));
+    }
+}