diff options
author | Andy McFadden <fadden@android.com> | 2011-07-07 15:30:40 -0700 |
---|---|---|
committer | Andy McFadden <fadden@android.com> | 2011-07-08 16:26:38 -0700 |
commit | 0d3524562e330e74f150a17c4dc4dd66a0faae46 (patch) | |
tree | 48c2961c3b9a19297a536b946018f4c920a23482 | |
parent | 8fa82ef0bb1129284d7ed5032eca0a25a1aa94e6 (diff) | |
download | calendar-0d3524562e330e74f150a17c4dc4dd66a0faae46.tar.gz |
Relocate common Calendar classes
Move some classes from android.pim to com.android.calendarcommon.
Bug 4575374
Change-Id: I0da19545253f0bc887d2c284414f372ad740e946
-rw-r--r-- | Android.mk | 24 | ||||
-rw-r--r-- | src/com/android/calendarcommon/EventRecurrence.java | 892 | ||||
-rw-r--r-- | src/com/android/calendarcommon/ICalendar.java | 660 | ||||
-rw-r--r-- | src/com/android/calendarcommon/RecurrenceSet.java | 511 | ||||
-rw-r--r-- | tests/Android.mk | 24 | ||||
-rw-r--r-- | tests/AndroidManifest.xml | 28 | ||||
-rw-r--r-- | tests/src/com/android/calendarcommon/EventRecurrenceTest.java | 754 | ||||
-rw-r--r-- | tests/src/com/android/calendarcommon/RecurrenceSetTest.java | 83 |
8 files changed, 2976 insertions, 0 deletions
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)); + } +} |