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