diff options
Diffstat (limited to 'core/java/android/pim')
-rw-r--r-- | core/java/android/pim/EventRecurrence.java | 892 | ||||
-rw-r--r-- | core/java/android/pim/ICalendar.java | 660 | ||||
-rw-r--r-- | core/java/android/pim/RecurrenceSet.java | 511 |
3 files changed, 0 insertions, 2063 deletions
diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java deleted file mode 100644 index 128b697..0000000 --- a/core/java/android/pim/EventRecurrence.java +++ /dev/null @@ -1,892 +0,0 @@ -/* - * 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 android.pim; - -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/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java deleted file mode 100644 index 58c5c63..0000000 --- a/core/java/android/pim/ICalendar.java +++ /dev/null @@ -1,660 +0,0 @@ -/* - * 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 android.pim; - -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/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java deleted file mode 100644 index b7fb320..0000000 --- a/core/java/android/pim/RecurrenceSet.java +++ /dev/null @@ -1,511 +0,0 @@ -/* - * 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 android.pim; - -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(); - } -} |