1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.calendarcommon2;
18 
19 import android.text.TextUtils;
20 import android.text.format.Time;
21 import android.util.Log;
22 import android.util.TimeFormatException;
23 
24 import java.util.Calendar;
25 import java.util.HashMap;
26 
27 /**
28  * Event recurrence utility functions.
29  */
30 public class EventRecurrence {
31     private static String TAG = "EventRecur";
32 
33     public static final int SECONDLY = 1;
34     public static final int MINUTELY = 2;
35     public static final int HOURLY = 3;
36     public static final int DAILY = 4;
37     public static final int WEEKLY = 5;
38     public static final int MONTHLY = 6;
39     public static final int YEARLY = 7;
40 
41     public static final int SU = 0x00010000;
42     public static final int MO = 0x00020000;
43     public static final int TU = 0x00040000;
44     public static final int WE = 0x00080000;
45     public static final int TH = 0x00100000;
46     public static final int FR = 0x00200000;
47     public static final int SA = 0x00400000;
48 
49     public Time      startDate;     // set by setStartDate(), not parse()
50 
51     public int       freq;          // SECONDLY, MINUTELY, etc.
52     public String    until;
53     public int       count;
54     public int       interval;
55     public int       wkst;          // SU, MO, TU, etc.
56 
57     /* lists with zero entries may be null references */
58     public int[]     bysecond;
59     public int       bysecondCount;
60     public int[]     byminute;
61     public int       byminuteCount;
62     public int[]     byhour;
63     public int       byhourCount;
64     public int[]     byday;
65     public int[]     bydayNum;
66     public int       bydayCount;
67     public int[]     bymonthday;
68     public int       bymonthdayCount;
69     public int[]     byyearday;
70     public int       byyeardayCount;
71     public int[]     byweekno;
72     public int       byweeknoCount;
73     public int[]     bymonth;
74     public int       bymonthCount;
75     public int[]     bysetpos;
76     public int       bysetposCount;
77 
78     /** maps a part string to a parser object */
79     private static HashMap<String,PartParser> sParsePartMap;
80     static {
81         sParsePartMap = new HashMap<String,PartParser>();
82         sParsePartMap.put("FREQ", new ParseFreq());
83         sParsePartMap.put("UNTIL", new ParseUntil());
84         sParsePartMap.put("COUNT", new ParseCount());
85         sParsePartMap.put("INTERVAL", new ParseInterval());
86         sParsePartMap.put("BYSECOND", new ParseBySecond());
87         sParsePartMap.put("BYMINUTE", new ParseByMinute());
88         sParsePartMap.put("BYHOUR", new ParseByHour());
89         sParsePartMap.put("BYDAY", new ParseByDay());
90         sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay());
91         sParsePartMap.put("BYYEARDAY", new ParseByYearDay());
92         sParsePartMap.put("BYWEEKNO", new ParseByWeekNo());
93         sParsePartMap.put("BYMONTH", new ParseByMonth());
94         sParsePartMap.put("BYSETPOS", new ParseBySetPos());
95         sParsePartMap.put("WKST", new ParseWkst());
96     }
97 
98     /* values for bit vector that keeps track of what we have already seen */
99     private static final int PARSED_FREQ = 1 << 0;
100     private static final int PARSED_UNTIL = 1 << 1;
101     private static final int PARSED_COUNT = 1 << 2;
102     private static final int PARSED_INTERVAL = 1 << 3;
103     private static final int PARSED_BYSECOND = 1 << 4;
104     private static final int PARSED_BYMINUTE = 1 << 5;
105     private static final int PARSED_BYHOUR = 1 << 6;
106     private static final int PARSED_BYDAY = 1 << 7;
107     private static final int PARSED_BYMONTHDAY = 1 << 8;
108     private static final int PARSED_BYYEARDAY = 1 << 9;
109     private static final int PARSED_BYWEEKNO = 1 << 10;
110     private static final int PARSED_BYMONTH = 1 << 11;
111     private static final int PARSED_BYSETPOS = 1 << 12;
112     private static final int PARSED_WKST = 1 << 13;
113 
114     /** maps a FREQ value to an integer constant */
115     private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>();
116     static {
117         sParseFreqMap.put("SECONDLY", SECONDLY);
118         sParseFreqMap.put("MINUTELY", MINUTELY);
119         sParseFreqMap.put("HOURLY", HOURLY);
120         sParseFreqMap.put("DAILY", DAILY);
121         sParseFreqMap.put("WEEKLY", WEEKLY);
122         sParseFreqMap.put("MONTHLY", MONTHLY);
123         sParseFreqMap.put("YEARLY", YEARLY);
124     }
125 
126     /** maps a two-character weekday string to an integer constant */
127     private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>();
128     static {
129         sParseWeekdayMap.put("SU", SU);
130         sParseWeekdayMap.put("MO", MO);
131         sParseWeekdayMap.put("TU", TU);
132         sParseWeekdayMap.put("WE", WE);
133         sParseWeekdayMap.put("TH", TH);
134         sParseWeekdayMap.put("FR", FR);
135         sParseWeekdayMap.put("SA", SA);
136     }
137 
138     /** If set, allow lower-case recurrence rule strings.  Minor performance impact. */
139     private static final boolean ALLOW_LOWER_CASE = true;
140 
141     /** If set, validate the value of UNTIL parts.  Minor performance impact. */
142     private static final boolean VALIDATE_UNTIL = false;
143 
144     /** If set, require that only one of {UNTIL,COUNT} is present.  Breaks compat w/ old parser. */
145     private static final boolean ONLY_ONE_UNTIL_COUNT = false;
146 
147 
148     /**
149      * Thrown when a recurrence string provided can not be parsed according
150      * to RFC2445.
151      */
152     public static class InvalidFormatException extends RuntimeException {
InvalidFormatException(String s)153         InvalidFormatException(String s) {
154             super(s);
155         }
156     }
157 
158 
setStartDate(Time date)159     public void setStartDate(Time date) {
160         startDate = date;
161     }
162 
163     /**
164      * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc.
165      * constants.  btw, I think we should switch to those here too, to
166      * get rid of this function, if possible.
167      */
calendarDay2Day(int day)168     public static int calendarDay2Day(int day)
169     {
170         switch (day)
171         {
172             case Calendar.SUNDAY:
173                 return SU;
174             case Calendar.MONDAY:
175                 return MO;
176             case Calendar.TUESDAY:
177                 return TU;
178             case Calendar.WEDNESDAY:
179                 return WE;
180             case Calendar.THURSDAY:
181                 return TH;
182             case Calendar.FRIDAY:
183                 return FR;
184             case Calendar.SATURDAY:
185                 return SA;
186             default:
187                 throw new RuntimeException("bad day of week: " + day);
188         }
189     }
190 
timeDay2Day(int day)191     public static int timeDay2Day(int day)
192     {
193         switch (day)
194         {
195             case Time.SUNDAY:
196                 return SU;
197             case Time.MONDAY:
198                 return MO;
199             case Time.TUESDAY:
200                 return TU;
201             case Time.WEDNESDAY:
202                 return WE;
203             case Time.THURSDAY:
204                 return TH;
205             case Time.FRIDAY:
206                 return FR;
207             case Time.SATURDAY:
208                 return SA;
209             default:
210                 throw new RuntimeException("bad day of week: " + day);
211         }
212     }
day2TimeDay(int day)213     public static int day2TimeDay(int day)
214     {
215         switch (day)
216         {
217             case SU:
218                 return Time.SUNDAY;
219             case MO:
220                 return Time.MONDAY;
221             case TU:
222                 return Time.TUESDAY;
223             case WE:
224                 return Time.WEDNESDAY;
225             case TH:
226                 return Time.THURSDAY;
227             case FR:
228                 return Time.FRIDAY;
229             case SA:
230                 return Time.SATURDAY;
231             default:
232                 throw new RuntimeException("bad day of week: " + day);
233         }
234     }
235 
236     /**
237      * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY
238      * constants.  btw, I think we should switch to those here too, to
239      * get rid of this function, if possible.
240      */
day2CalendarDay(int day)241     public static int day2CalendarDay(int day)
242     {
243         switch (day)
244         {
245             case SU:
246                 return Calendar.SUNDAY;
247             case MO:
248                 return Calendar.MONDAY;
249             case TU:
250                 return Calendar.TUESDAY;
251             case WE:
252                 return Calendar.WEDNESDAY;
253             case TH:
254                 return Calendar.THURSDAY;
255             case FR:
256                 return Calendar.FRIDAY;
257             case SA:
258                 return Calendar.SATURDAY;
259             default:
260                 throw new RuntimeException("bad day of week: " + day);
261         }
262     }
263 
264     /**
265      * Converts one of the internal day constants (SU, MO, etc.) to the
266      * two-letter string representing that constant.
267      *
268      * @param day one the internal constants SU, MO, etc.
269      * @return the two-letter string for the day ("SU", "MO", etc.)
270      *
271      * @throws IllegalArgumentException Thrown if the day argument is not one of
272      * the defined day constants.
273      */
day2String(int day)274     private static String day2String(int day) {
275         switch (day) {
276         case SU:
277             return "SU";
278         case MO:
279             return "MO";
280         case TU:
281             return "TU";
282         case WE:
283             return "WE";
284         case TH:
285             return "TH";
286         case FR:
287             return "FR";
288         case SA:
289             return "SA";
290         default:
291             throw new IllegalArgumentException("bad day argument: " + day);
292         }
293     }
294 
appendNumbers(StringBuilder s, String label, int count, int[] values)295     private static void appendNumbers(StringBuilder s, String label,
296                                         int count, int[] values)
297     {
298         if (count > 0) {
299             s.append(label);
300             count--;
301             for (int i=0; i<count; i++) {
302                 s.append(values[i]);
303                 s.append(",");
304             }
305             s.append(values[count]);
306         }
307     }
308 
appendByDay(StringBuilder s, int i)309     private void appendByDay(StringBuilder s, int i)
310     {
311         int n = this.bydayNum[i];
312         if (n != 0) {
313             s.append(n);
314         }
315 
316         String str = day2String(this.byday[i]);
317         s.append(str);
318     }
319 
320     @Override
toString()321     public String toString()
322     {
323         StringBuilder s = new StringBuilder();
324 
325         s.append("FREQ=");
326         switch (this.freq)
327         {
328             case SECONDLY:
329                 s.append("SECONDLY");
330                 break;
331             case MINUTELY:
332                 s.append("MINUTELY");
333                 break;
334             case HOURLY:
335                 s.append("HOURLY");
336                 break;
337             case DAILY:
338                 s.append("DAILY");
339                 break;
340             case WEEKLY:
341                 s.append("WEEKLY");
342                 break;
343             case MONTHLY:
344                 s.append("MONTHLY");
345                 break;
346             case YEARLY:
347                 s.append("YEARLY");
348                 break;
349         }
350 
351         if (!TextUtils.isEmpty(this.until)) {
352             s.append(";UNTIL=");
353             s.append(until);
354         }
355 
356         if (this.count != 0) {
357             s.append(";COUNT=");
358             s.append(this.count);
359         }
360 
361         if (this.interval != 0) {
362             s.append(";INTERVAL=");
363             s.append(this.interval);
364         }
365 
366         if (this.wkst != 0) {
367             s.append(";WKST=");
368             s.append(day2String(this.wkst));
369         }
370 
371         appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond);
372         appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute);
373         appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour);
374 
375         // day
376         int count = this.bydayCount;
377         if (count > 0) {
378             s.append(";BYDAY=");
379             count--;
380             for (int i=0; i<count; i++) {
381                 appendByDay(s, i);
382                 s.append(",");
383             }
384             appendByDay(s, count);
385         }
386 
387         appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday);
388         appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday);
389         appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno);
390         appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth);
391         appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos);
392 
393         return s.toString();
394     }
395 
repeatsOnEveryWeekDay()396     public boolean repeatsOnEveryWeekDay() {
397         if (this.freq != WEEKLY) {
398             return false;
399         }
400 
401         int count = this.bydayCount;
402         if (count != 5) {
403             return false;
404         }
405 
406         for (int i = 0 ; i < count ; i++) {
407             int day = byday[i];
408             if (day == SU || day == SA) {
409                 return false;
410             }
411         }
412 
413         return true;
414     }
415 
416     /**
417      * Determines whether this rule specifies a simple monthly rule by weekday, such as
418      * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month).
419      * <p>
420      * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month),
421      * will cause "false" to be returned.
422      * <p>
423      * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every
424      * month) will cause "false" to be returned.  (Note these are usually expressed as
425      * WEEKLY rules, and hence are uncommon.)
426      *
427      * @return true if this rule is of the appropriate form
428      */
repeatsMonthlyOnDayCount()429     public boolean repeatsMonthlyOnDayCount() {
430         if (this.freq != MONTHLY) {
431             return false;
432         }
433 
434         if (bydayCount != 1 || bymonthdayCount != 0) {
435             return false;
436         }
437 
438         if (bydayNum[0] <= 0) {
439             return false;
440         }
441 
442         return true;
443     }
444 
445     /**
446      * Determines whether two integer arrays contain identical elements.
447      * <p>
448      * The native implementation over-allocated the arrays (and may have stuff left over from
449      * a previous run), so we can't just check the arrays -- the separately-maintained count
450      * field also matters.  We assume that a null array will have a count of zero, and that the
451      * array can hold as many elements as the associated count indicates.
452      * <p>
453      * TODO: replace this with Arrays.equals() when the old parser goes away.
454      */
arraysEqual(int[] array1, int count1, int[] array2, int count2)455     private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) {
456         if (count1 != count2) {
457             return false;
458         }
459 
460         for (int i = 0; i < count1; i++) {
461             if (array1[i] != array2[i])
462                 return false;
463         }
464 
465         return true;
466     }
467 
468     @Override
equals(Object obj)469     public boolean equals(Object obj) {
470         if (this == obj) {
471             return true;
472         }
473         if (!(obj instanceof EventRecurrence)) {
474             return false;
475         }
476 
477         EventRecurrence er = (EventRecurrence) obj;
478         return  (startDate == null ?
479                         er.startDate == null : Time.compare(startDate, er.startDate) == 0) &&
480                 freq == er.freq &&
481                 (until == null ? er.until == null : until.equals(er.until)) &&
482                 count == er.count &&
483                 interval == er.interval &&
484                 wkst == er.wkst &&
485                 arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) &&
486                 arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) &&
487                 arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) &&
488                 arraysEqual(byday, bydayCount, er.byday, er.bydayCount) &&
489                 arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) &&
490                 arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) &&
491                 arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) &&
492                 arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) &&
493                 arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) &&
494                 arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount);
495     }
496 
hashCode()497     @Override public int hashCode() {
498         // We overrode equals, so we must override hashCode().  Nobody seems to need this though.
499         throw new UnsupportedOperationException();
500     }
501 
502     /**
503      * Resets parser-modified fields to their initial state.  Does not alter startDate.
504      * <p>
505      * The original parser always set all of the "count" fields, "wkst", and "until",
506      * essentially allowing the same object to be used multiple times by calling parse().
507      * It's unclear whether this behavior was intentional.  For now, be paranoid and
508      * preserve the existing behavior by resetting the fields.
509      * <p>
510      * We don't need to touch the integer arrays; they will either be ignored or
511      * overwritten.  The "startDate" field is not set by the parser, so we ignore it here.
512      */
resetFields()513     private void resetFields() {
514         until = null;
515         freq = count = interval = bysecondCount = byminuteCount = byhourCount =
516             bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount =
517             bysetposCount = 0;
518     }
519 
520     /**
521      * Parses an rfc2445 recurrence rule string into its component pieces.  Attempting to parse
522      * malformed input will result in an EventRecurrence.InvalidFormatException.
523      *
524      * @param recur The recurrence rule to parse (in un-folded form).
525      */
parse(String recur)526     public void parse(String recur) {
527         /*
528          * From RFC 2445 section 4.3.10:
529          *
530          * recur = "FREQ"=freq *(
531          *       ; either UNTIL or COUNT may appear in a 'recur',
532          *       ; but UNTIL and COUNT MUST NOT occur in the same 'recur'
533          *
534          *       ( ";" "UNTIL" "=" enddate ) /
535          *       ( ";" "COUNT" "=" 1*DIGIT ) /
536          *
537          *       ; the rest of these keywords are optional,
538          *       ; but MUST NOT occur more than once
539          *
540          *       ( ";" "INTERVAL" "=" 1*DIGIT )          /
541          *       ( ";" "BYSECOND" "=" byseclist )        /
542          *       ( ";" "BYMINUTE" "=" byminlist )        /
543          *       ( ";" "BYHOUR" "=" byhrlist )           /
544          *       ( ";" "BYDAY" "=" bywdaylist )          /
545          *       ( ";" "BYMONTHDAY" "=" bymodaylist )    /
546          *       ( ";" "BYYEARDAY" "=" byyrdaylist )     /
547          *       ( ";" "BYWEEKNO" "=" bywknolist )       /
548          *       ( ";" "BYMONTH" "=" bymolist )          /
549          *       ( ";" "BYSETPOS" "=" bysplist )         /
550          *       ( ";" "WKST" "=" weekday )              /
551          *       ( ";" x-name "=" text )
552          *       )
553          *
554          *  The rule parts are not ordered in any particular sequence.
555          *
556          * Examples:
557          *   FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
558          *   FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
559          *
560          * Strategy:
561          * (1) Split the string at ';' boundaries to get an array of rule "parts".
562          * (2) For each part, find substrings for left/right sides of '=' (name/value).
563          * (3) Call a <name>-specific parsing function to parse the <value> into an
564          *     output field.
565          *
566          * By keeping track of which names we've seen in a bit vector, we can verify the
567          * constraints indicated above (FREQ appears first, none of them appear more than once --
568          * though x-[name] would require special treatment), and we have either UNTIL or COUNT
569          * but not both.
570          *
571          * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must
572          * be handled in a case-insensitive fashion, but case may be significant for other
573          * properties.  We don't have any case-sensitive values in RRULE, except possibly
574          * for the custom "X-" properties, but we ignore those anyway.  Thus, we can trivially
575          * convert the entire string to upper case and then use simple comparisons.
576          *
577          * Differences from previous version:
578          * - allows lower-case property and enumeration values [optional]
579          * - enforces that FREQ appears first
580          * - enforces that only one of UNTIL and COUNT may be specified
581          * - allows (but ignores) X-* parts
582          * - improved validation on various values (e.g. UNTIL timestamps)
583          * - error messages are more specific
584          *
585          * TODO: enforce additional constraints listed in RFC 5545, notably the "N/A" entries
586          * in section 3.3.10.  For example, if FREQ=WEEKLY, we should reject a rule that
587          * includes a BYMONTHDAY part.
588          */
589 
590         /* TODO: replace with "if (freq != 0) throw" if nothing requires this */
591         resetFields();
592 
593         int parseFlags = 0;
594         String[] parts;
595         if (ALLOW_LOWER_CASE) {
596             parts = recur.toUpperCase().split(";");
597         } else {
598             parts = recur.split(";");
599         }
600         for (String part : parts) {
601             // allow empty part (e.g., double semicolon ";;")
602             if (TextUtils.isEmpty(part)) {
603                 continue;
604             }
605             int equalIndex = part.indexOf('=');
606             if (equalIndex <= 0) {
607                 /* no '=' or no LHS */
608                 throw new InvalidFormatException("Missing LHS in " + part);
609             }
610 
611             String lhs = part.substring(0, equalIndex);
612             String rhs = part.substring(equalIndex + 1);
613             if (rhs.length() == 0) {
614                 throw new InvalidFormatException("Missing RHS in " + part);
615             }
616 
617             /*
618              * In lieu of a "switch" statement that allows string arguments, we use a
619              * map from strings to parsing functions.
620              */
621             PartParser parser = sParsePartMap.get(lhs);
622             if (parser == null) {
623                 if (lhs.startsWith("X-")) {
624                     //Log.d(TAG, "Ignoring custom part " + lhs);
625                     continue;
626                 }
627                 throw new InvalidFormatException("Couldn't find parser for " + lhs);
628             } else {
629                 int flag = parser.parsePart(rhs, this);
630                 if ((parseFlags & flag) != 0) {
631                     throw new InvalidFormatException("Part " + lhs + " was specified twice");
632                 }
633                 parseFlags |= flag;
634             }
635         }
636 
637         // If not specified, week starts on Monday.
638         if ((parseFlags & PARSED_WKST) == 0) {
639             wkst = MO;
640         }
641 
642         // FREQ is mandatory.
643         if ((parseFlags & PARSED_FREQ) == 0) {
644             throw new InvalidFormatException("Must specify a FREQ value");
645         }
646 
647         // Can't have both UNTIL and COUNT.
648         if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) {
649             if (ONLY_ONE_UNTIL_COUNT) {
650                 throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur);
651             } else {
652                 Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur);
653             }
654         }
655     }
656 
657     /**
658      * Base class for the RRULE part parsers.
659      */
660     abstract static class PartParser {
661         /**
662          * Parses a single part.
663          *
664          * @param value The right-hand-side of the part.
665          * @param er The EventRecurrence into which the result is stored.
666          * @return A bit value indicating which part was parsed.
667          */
parsePart(String value, EventRecurrence er)668         public abstract int parsePart(String value, EventRecurrence er);
669 
670         /**
671          * Parses an integer, with range-checking.
672          *
673          * @param str The string to parse.
674          * @param minVal Minimum allowed value.
675          * @param maxVal Maximum allowed value.
676          * @param allowZero Is 0 allowed?
677          * @return The parsed value.
678          */
parseIntRange(String str, int minVal, int maxVal, boolean allowZero)679         public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) {
680             try {
681                 if (str.charAt(0) == '+') {
682                     // Integer.parseInt does not allow a leading '+', so skip it manually.
683                     str = str.substring(1);
684                 }
685                 int val = Integer.parseInt(str);
686                 if (val < minVal || val > maxVal || (val == 0 && !allowZero)) {
687                     throw new InvalidFormatException("Integer value out of range: " + str);
688                 }
689                 return val;
690             } catch (NumberFormatException nfe) {
691                 throw new InvalidFormatException("Invalid integer value: " + str);
692             }
693         }
694 
695         /**
696          * Parses a comma-separated list of integers, with range-checking.
697          *
698          * @param listStr The string to parse.
699          * @param minVal Minimum allowed value.
700          * @param maxVal Maximum allowed value.
701          * @param allowZero Is 0 allowed?
702          * @return A new array with values, sized to hold the exact number of elements.
703          */
parseNumberList(String listStr, int minVal, int maxVal, boolean allowZero)704         public static int[] parseNumberList(String listStr, int minVal, int maxVal,
705                 boolean allowZero) {
706             int[] values;
707 
708             if (listStr.indexOf(",") < 0) {
709                 // Common case: only one entry, skip split() overhead.
710                 values = new int[1];
711                 values[0] = parseIntRange(listStr, minVal, maxVal, allowZero);
712             } else {
713                 String[] valueStrs = listStr.split(",");
714                 int len = valueStrs.length;
715                 values = new int[len];
716                 for (int i = 0; i < len; i++) {
717                     values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero);
718                 }
719             }
720             return values;
721         }
722    }
723 
724     /** parses FREQ={SECONDLY,MINUTELY,...} */
725     private static class ParseFreq extends PartParser {
parsePart(String value, EventRecurrence er)726         @Override public int parsePart(String value, EventRecurrence er) {
727             Integer freq = sParseFreqMap.get(value);
728             if (freq == null) {
729                 throw new InvalidFormatException("Invalid FREQ value: " + value);
730             }
731             er.freq = freq;
732             return PARSED_FREQ;
733         }
734     }
735     /** parses UNTIL=enddate, e.g. "19970829T021400" */
736     private static class ParseUntil extends PartParser {
parsePart(String value, EventRecurrence er)737         @Override public int parsePart(String value, EventRecurrence er) {
738             if (VALIDATE_UNTIL) {
739                 try {
740                     // Parse the time to validate it.  The result isn't retained.
741                     Time until = new Time();
742                     until.parse(value);
743                 } catch (TimeFormatException tfe) {
744                     throw new InvalidFormatException("Invalid UNTIL value: " + value);
745                 }
746             }
747             er.until = value;
748             return PARSED_UNTIL;
749         }
750     }
751     /** parses COUNT=[non-negative-integer] */
752     private static class ParseCount extends PartParser {
parsePart(String value, EventRecurrence er)753         @Override public int parsePart(String value, EventRecurrence er) {
754             er.count = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
755             if (er.count < 0) {
756                 Log.d(TAG, "Invalid Count. Forcing COUNT to 1 from " + value);
757                 er.count = 1; // invalid count. assume one time recurrence.
758             }
759             return PARSED_COUNT;
760         }
761     }
762     /** parses INTERVAL=[non-negative-integer] */
763     private static class ParseInterval extends PartParser {
parsePart(String value, EventRecurrence er)764         @Override public int parsePart(String value, EventRecurrence er) {
765             er.interval = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
766             if (er.interval < 1) {
767                 Log.d(TAG, "Invalid Interval. Forcing INTERVAL to 1 from " + value);
768                 er.interval = 1;
769             }
770             return PARSED_INTERVAL;
771         }
772     }
773     /** parses BYSECOND=byseclist */
774     private static class ParseBySecond extends PartParser {
parsePart(String value, EventRecurrence er)775         @Override public int parsePart(String value, EventRecurrence er) {
776             int[] bysecond = parseNumberList(value, 0, 59, true);
777             er.bysecond = bysecond;
778             er.bysecondCount = bysecond.length;
779             return PARSED_BYSECOND;
780         }
781     }
782     /** parses BYMINUTE=byminlist */
783     private static class ParseByMinute extends PartParser {
parsePart(String value, EventRecurrence er)784         @Override public int parsePart(String value, EventRecurrence er) {
785             int[] byminute = parseNumberList(value, 0, 59, true);
786             er.byminute = byminute;
787             er.byminuteCount = byminute.length;
788             return PARSED_BYMINUTE;
789         }
790     }
791     /** parses BYHOUR=byhrlist */
792     private static class ParseByHour extends PartParser {
parsePart(String value, EventRecurrence er)793         @Override public int parsePart(String value, EventRecurrence er) {
794             int[] byhour = parseNumberList(value, 0, 23, true);
795             er.byhour = byhour;
796             er.byhourCount = byhour.length;
797             return PARSED_BYHOUR;
798         }
799     }
800     /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */
801     private static class ParseByDay extends PartParser {
parsePart(String value, EventRecurrence er)802         @Override public int parsePart(String value, EventRecurrence er) {
803             int[] byday;
804             int[] bydayNum;
805             int bydayCount;
806 
807             if (value.indexOf(",") < 0) {
808                 /* only one entry, skip split() overhead */
809                 bydayCount = 1;
810                 byday = new int[1];
811                 bydayNum = new int[1];
812                 parseWday(value, byday, bydayNum, 0);
813             } else {
814                 String[] wdays = value.split(",");
815                 int len = wdays.length;
816                 bydayCount = len;
817                 byday = new int[len];
818                 bydayNum = new int[len];
819                 for (int i = 0; i < len; i++) {
820                     parseWday(wdays[i], byday, bydayNum, i);
821                 }
822             }
823             er.byday = byday;
824             er.bydayNum = bydayNum;
825             er.bydayCount = bydayCount;
826             return PARSED_BYDAY;
827         }
828 
829         /** parses [int]weekday, putting the pieces into parallel array entries */
parseWday(String str, int[] byday, int[] bydayNum, int index)830         private static void parseWday(String str, int[] byday, int[] bydayNum, int index) {
831             int wdayStrStart = str.length() - 2;
832             String wdayStr;
833 
834             if (wdayStrStart > 0) {
835                 /* number is included; parse it out and advance to weekday */
836                 String numPart = str.substring(0, wdayStrStart);
837                 int num = parseIntRange(numPart, -53, 53, false);
838                 bydayNum[index] = num;
839                 wdayStr = str.substring(wdayStrStart);
840             } else {
841                 /* just the weekday string */
842                 wdayStr = str;
843             }
844             Integer wday = sParseWeekdayMap.get(wdayStr);
845             if (wday == null) {
846                 throw new InvalidFormatException("Invalid BYDAY value: " + str);
847             }
848             byday[index] = wday;
849         }
850     }
851     /** parses BYMONTHDAY=bymodaylist */
852     private static class ParseByMonthDay extends PartParser {
parsePart(String value, EventRecurrence er)853         @Override public int parsePart(String value, EventRecurrence er) {
854             int[] bymonthday = parseNumberList(value, -31, 31, false);
855             er.bymonthday = bymonthday;
856             er.bymonthdayCount = bymonthday.length;
857             return PARSED_BYMONTHDAY;
858         }
859     }
860     /** parses BYYEARDAY=byyrdaylist */
861     private static class ParseByYearDay extends PartParser {
parsePart(String value, EventRecurrence er)862         @Override public int parsePart(String value, EventRecurrence er) {
863             int[] byyearday = parseNumberList(value, -366, 366, false);
864             er.byyearday = byyearday;
865             er.byyeardayCount = byyearday.length;
866             return PARSED_BYYEARDAY;
867         }
868     }
869     /** parses BYWEEKNO=bywknolist */
870     private static class ParseByWeekNo extends PartParser {
parsePart(String value, EventRecurrence er)871         @Override public int parsePart(String value, EventRecurrence er) {
872             int[] byweekno = parseNumberList(value, -53, 53, false);
873             er.byweekno = byweekno;
874             er.byweeknoCount = byweekno.length;
875             return PARSED_BYWEEKNO;
876         }
877     }
878     /** parses BYMONTH=bymolist */
879     private static class ParseByMonth extends PartParser {
parsePart(String value, EventRecurrence er)880         @Override public int parsePart(String value, EventRecurrence er) {
881             int[] bymonth = parseNumberList(value, 1, 12, false);
882             er.bymonth = bymonth;
883             er.bymonthCount = bymonth.length;
884             return PARSED_BYMONTH;
885         }
886     }
887     /** parses BYSETPOS=bysplist */
888     private static class ParseBySetPos extends PartParser {
parsePart(String value, EventRecurrence er)889         @Override public int parsePart(String value, EventRecurrence er) {
890             int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
891             er.bysetpos = bysetpos;
892             er.bysetposCount = bysetpos.length;
893             return PARSED_BYSETPOS;
894         }
895     }
896     /** parses WKST={SU,MO,...} */
897     private static class ParseWkst extends PartParser {
parsePart(String value, EventRecurrence er)898         @Override public int parsePart(String value, EventRecurrence er) {
899             Integer wkst = sParseWeekdayMap.get(value);
900             if (wkst == null) {
901                 throw new InvalidFormatException("Invalid WKST value: " + value);
902             }
903             er.wkst = wkst;
904             return PARSED_WKST;
905         }
906     }
907 }
908