• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* //device/content/providers/pim/RecurrenceProcessor.java
2 **
3 ** Copyright 2006, The Android Open Source Project
4 **
5 ** Licensed under the Apache License, Version 2.0 (the "License");
6 ** you may not use this file except in compliance with the License.
7 ** You may obtain a copy of the License at
8 **
9 **     http://www.apache.org/licenses/LICENSE-2.0
10 **
11 ** Unless required by applicable law or agreed to in writing, software
12 ** distributed under the License is distributed on an "AS IS" BASIS,
13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 ** See the License for the specific language governing permissions and
15 ** limitations under the License.
16 */
17 
18 package com.android.calendarcommon2;
19 
20 import android.util.Log;
21 
22 import java.util.TreeSet;
23 
24 public class RecurrenceProcessor
25 {
26     // these are created once and reused.
27     private Time mIterator = new Time(Time.TIMEZONE_UTC);
28     private Time mUntil = new Time(Time.TIMEZONE_UTC);
29     private StringBuilder mStringBuilder = new StringBuilder();
30     private Time mGenerated = new Time(Time.TIMEZONE_UTC);
31     private DaySet mDays = new DaySet(false);
32     // Give up after this many loops.  This is roughly 1 second of expansion.
33     private static final int MAX_ALLOWED_ITERATIONS = 2000;
34 
RecurrenceProcessor()35     public RecurrenceProcessor()
36     {
37     }
38 
39     private static final String TAG = "RecurrenceProcessor";
40 
41     private static final boolean SPEW = false;
42 
43     /**
44      * Returns the time (millis since epoch) of the last occurrence,
45      * or -1 if the event repeats forever.  If there are no occurrences
46      * (because the exrule or exdates cancel all the occurrences) and the
47      * event does not repeat forever, then 0 is returned.
48      *
49      * This computes a conservative estimate of the last occurrence. That is,
50      * the time of the actual last occurrence might be earlier than the time
51      * returned by this method.
52      *
53      * @param dtstart the time of the first occurrence
54      * @param recur the recurrence
55      * @return an estimate of the time (in UTC milliseconds) of the last
56      * occurrence, which may be greater than the actual last occurrence
57      * @throws DateException
58      */
getLastOccurence(Time dtstart, RecurrenceSet recur)59     public long getLastOccurence(Time dtstart,
60                                  RecurrenceSet recur) throws DateException {
61         return getLastOccurence(dtstart, null /* no limit */, recur);
62     }
63 
64     /**
65      * Returns the time (millis since epoch) of the last occurrence,
66      * or -1 if the event repeats forever.  If there are no occurrences
67      * (because the exrule or exdates cancel all the occurrences) and the
68      * event does not repeat forever, then 0 is returned.
69      *
70      * This computes a conservative estimate of the last occurrence. That is,
71      * the time of the actual last occurrence might be earlier than the time
72      * returned by this method.
73      *
74      * @param dtstart the time of the first occurrence
75      * @param maxtime the max possible time of the last occurrence. null means no limit
76      * @param recur the recurrence
77      * @return an estimate of the time (in UTC milliseconds) of the last
78      * occurrence, which may be greater than the actual last occurrence
79      * @throws DateException
80      */
getLastOccurence(Time dtstart, Time maxtime, RecurrenceSet recur)81     public long getLastOccurence(Time dtstart, Time maxtime,
82                                  RecurrenceSet recur) throws DateException {
83         long lastTime = -1;
84         boolean hasCount = false;
85 
86         // first see if there are any "until"s specified.  if so, use the latest
87         // until / rdate.
88         if (recur.rrules != null) {
89             for (EventRecurrence rrule : recur.rrules) {
90                 if (rrule.count != 0) {
91                     hasCount = true;
92                 } else if (rrule.until != null) {
93                     // according to RFC 2445, until must be in UTC.
94                     mIterator.parse(rrule.until);
95                     long untilTime = mIterator.toMillis();
96                     if (untilTime > lastTime) {
97                         lastTime = untilTime;
98                     }
99                 }
100             }
101             if (lastTime != -1 && recur.rdates != null) {
102                 for (long dt : recur.rdates) {
103                     if (dt > lastTime) {
104                         lastTime = dt;
105                     }
106                 }
107             }
108 
109             // If there were only "until"s and no "count"s, then return the
110             // last "until" date or "rdate".
111             if (lastTime != -1 && !hasCount) {
112                 return lastTime;
113             }
114         } else if (recur.rdates != null &&
115                    recur.exrules == null && recur.exdates == null) {
116             // if there are only rdates, we can just pick the last one.
117             for (long dt : recur.rdates) {
118                 if (dt > lastTime) {
119                     lastTime = dt;
120                 }
121             }
122             return lastTime;
123         }
124 
125         // Expand the complete recurrence if there were any counts specified,
126         // or if there were rdates specified.
127         if (hasCount || recur.rdates != null || maxtime != null) {
128             // The expansion might not contain any dates if the exrule or
129             // exdates cancel all the generated dates.
130             long[] dates = expand(dtstart, recur,
131                     dtstart.toMillis() /* range start */,
132                     (maxtime != null) ? maxtime.toMillis() : -1 /* range end */);
133 
134             // The expansion might not contain any dates if exrule or exdates
135             // cancel all the generated dates.
136             if (dates.length == 0) {
137                 return 0;
138             }
139             return dates[dates.length - 1];
140         }
141         return -1;
142     }
143 
144     /**
145      * a -- list of values
146      * N -- number of values to use in a
147      * v -- value to check for
148      */
listContains(int[] a, int N, int v)149     private static boolean listContains(int[] a, int N, int v)
150     {
151         for (int i=0; i<N; i++) {
152             if (a[i] == v) {
153                 return true;
154             }
155         }
156         return false;
157     }
158 
159     /**
160      * a -- list of values
161      * N -- number of values to use in a
162      * v -- value to check for
163      * max -- if a value in a is negative, add that negative value
164      *        to max and compare that instead; this is how we deal with
165      *        negative numbers being offsets from the end value
166      */
listContains(int[] a, int N, int v, int max)167     private static boolean listContains(int[] a, int N, int v, int max)
168     {
169         for (int i=0; i<N; i++) {
170             int w = a[i];
171             if (w > 0) {
172                 if (w == v) {
173                     return true;
174                 }
175             } else {
176                 max += w; // w is negative
177                 if (max == v) {
178                     return true;
179                 }
180             }
181         }
182         return false;
183     }
184 
185     /**
186      * Filter out the ones for events whose BYxxx rule is for
187      * a period greater than or equal to the period of the FREQ.
188      *
189      * Returns 0 if the event should not be filtered out
190      * Returns something else (a rule number which is useful for debugging)
191      * if the event should not be returned
192      */
filter(EventRecurrence r, Time iterator)193     private static int filter(EventRecurrence r, Time iterator)
194     {
195         boolean found;
196         int freq = r.freq;
197 
198         if (EventRecurrence.MONTHLY >= freq) {
199             // BYMONTH
200             if (r.bymonthCount > 0) {
201                 found = listContains(r.bymonth, r.bymonthCount,
202                         iterator.getMonth() + 1);
203                 if (!found) {
204                     return 1;
205                 }
206             }
207         }
208         if (EventRecurrence.WEEKLY >= freq) {
209             // BYWEEK -- this is just a guess.  I wonder how many events
210             // acutally use BYWEEKNO.
211             if (r.byweeknoCount > 0) {
212                 found = listContains(r.byweekno, r.byweeknoCount,
213                                 iterator.getWeekNumber(),
214                                 iterator.getActualMaximum(Time.WEEK_NUM));
215                 if (!found) {
216                     return 2;
217                 }
218             }
219         }
220         if (EventRecurrence.DAILY >= freq) {
221             // BYYEARDAY
222             if (r.byyeardayCount > 0) {
223                 found = listContains(r.byyearday, r.byyeardayCount,
224                                 iterator.getYearDay(), iterator.getActualMaximum(Time.YEAR_DAY));
225                 if (!found) {
226                     return 3;
227                 }
228             }
229             // BYMONTHDAY
230             if (r.bymonthdayCount > 0 ) {
231                 found = listContains(r.bymonthday, r.bymonthdayCount,
232                                 iterator.getDay(),
233                                 iterator.getActualMaximum(Time.MONTH_DAY));
234                 if (!found) {
235                     return 4;
236                 }
237             }
238             // BYDAY -- when filtering, we ignore the number field, because it
239             // only is meaningful when creating more events.
240 byday:
241             if (r.bydayCount > 0) {
242                 int a[] = r.byday;
243                 int N = r.bydayCount;
244                 int v = EventRecurrence.timeDay2Day(iterator.getWeekDay());
245                 for (int i=0; i<N; i++) {
246                     if (a[i] == v) {
247                         break byday;
248                     }
249                 }
250                 return 5;
251             }
252         }
253         if (EventRecurrence.HOURLY >= freq) {
254             // BYHOUR
255             found = listContains(r.byhour, r.byhourCount,
256                             iterator.getHour(),
257                             iterator.getActualMaximum(Time.HOUR));
258             if (!found) {
259                 return 6;
260             }
261         }
262         if (EventRecurrence.MINUTELY >= freq) {
263             // BYMINUTE
264             found = listContains(r.byminute, r.byminuteCount,
265                             iterator.getMinute(),
266                             iterator.getActualMaximum(Time.MINUTE));
267             if (!found) {
268                 return 7;
269             }
270         }
271         if (EventRecurrence.SECONDLY >= freq) {
272             // BYSECOND
273             found = listContains(r.bysecond, r.bysecondCount,
274                             iterator.getSecond(),
275                             iterator.getActualMaximum(Time.SECOND));
276             if (!found) {
277                 return 8;
278             }
279         }
280 
281         if (r.bysetposCount > 0) {
282 bysetpos:
283             // BYSETPOS - we only handle rules like FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
284             if (freq == EventRecurrence.MONTHLY && r.bydayCount > 0) {
285                 // Check for stuff like BYDAY=1TU
286                 for (int i = r.bydayCount - 1; i >= 0; i--) {
287                     if (r.bydayNum[i] != 0) {
288                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
289                             Log.v(TAG, "BYSETPOS not supported with these rules: " + r);
290                         }
291                         break bysetpos;
292                     }
293                 }
294                 if (!filterMonthlySetPos(r, iterator)) {
295                     // Not allowed, filter it out.
296                     return 9;
297                 }
298             } else {
299                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
300                     Log.v(TAG, "BYSETPOS not supported with these rules: " + r);
301                 }
302             }
303             // BYSETPOS was defined but we don't know how to handle it.  Do no filtering based
304             // on it.
305         }
306 
307         // if we got to here, we didn't filter it out
308         return 0;
309     }
310 
311     /**
312      * Filters out instances that don't match the BYSETPOS clause of a monthly recurrence rule.
313      * This is an awkward and inefficient way to go about it.
314      *
315      * @returns true if this instance should be kept
316      */
filterMonthlySetPos(EventRecurrence r, Time instance)317     private static boolean filterMonthlySetPos(EventRecurrence r, Time instance) {
318         /*
319          * Compute the day of the week for the first day of the month.  "instance" has a
320          * day number and a DotW, so we compute the DotW of the 1st from that.  Note DotW
321          * is 0-6, where 0=SUNDAY.
322          *
323          * The basic calculation is to take the instance's "day of the week" number, subtract
324          * (day of the month - 1) mod 7, and then make sure it's positive.  We can simplify
325          * that with some algebra.
326          */
327         int dotw = (instance.getWeekDay() - instance.getDay() + 36) % 7;
328 
329         /*
330          * The byday[] values are specified as bits, so we can just OR them all
331          * together.
332          */
333         int bydayMask = 0;
334         for (int i = 0; i < r.bydayCount; i++) {
335             bydayMask |= r.byday[i];
336         }
337 
338         /*
339          * Generate a set according to the BYDAY rules.  For each day of the month, determine
340          * if its day of the week is included.  If so, append it to the day set.
341          */
342         int maxDay = instance.getActualMaximum(Time.MONTH_DAY);
343         int daySet[] = new int[maxDay];
344         int daySetLength = 0;
345 
346         for (int md = 1; md <= maxDay; md++) {
347             // For each month day, see if it's part of the set.  (This makes some assumptions
348             // about the exact form of the DotW constants.)
349             int dayBit = EventRecurrence.SU << dotw;
350             if ((bydayMask & dayBit) != 0) {
351                 daySet[daySetLength++] = md;
352             }
353 
354             dotw++;
355             if (dotw == 7)
356                 dotw = 0;
357         }
358 
359         /*
360          * Now walk through the BYSETPOS list and see if the instance is equal to any of the
361          * specified daySet entries.
362          */
363         for (int i = r.bysetposCount - 1; i >= 0; i--) {
364             int index = r.bysetpos[i];
365             if (index > 0) {
366                 if (index > daySetLength) {
367                     continue;  // out of range
368                 }
369                 if (daySet[index-1] == instance.getDay()) {
370                     return true;
371                 }
372             } else if (index < 0) {
373                 if (daySetLength + index < 0) {
374                     continue;  // out of range
375                 }
376                 if (daySet[daySetLength + index] == instance.getDay()) {
377                     return true;
378                 }
379             } else {
380                 // should have been caught by parser
381                 throw new RuntimeException("invalid bysetpos value");
382             }
383         }
384 
385         return false;
386     }
387 
388 
389     private static final int USE_ITERATOR = 0;
390     private static final int USE_BYLIST = 1;
391 
392     /**
393      * Return whether we should make this list from the BYxxx list or
394      * from the component of the iterator.
395      */
generateByList(int count, int freq, int byFreq)396     int generateByList(int count, int freq, int byFreq)
397     {
398         if (byFreq >= freq) {
399             return USE_ITERATOR;
400         } else {
401             if (count == 0) {
402                 return USE_ITERATOR;
403             } else {
404                 return USE_BYLIST;
405             }
406         }
407     }
408 
useBYX(int freq, int freqConstant, int count)409     private static boolean useBYX(int freq, int freqConstant, int count)
410     {
411         return freq > freqConstant && count > 0;
412     }
413 
414     public static class DaySet
415     {
DaySet(boolean zulu)416         public DaySet(boolean zulu)
417         {
418             mTime = new Time(Time.TIMEZONE_UTC);
419         }
420 
setRecurrence(EventRecurrence r)421         void setRecurrence(EventRecurrence r)
422         {
423             mYear = 0;
424             mMonth = -1;
425             mR = r;
426         }
427 
get(Time iterator, int day)428         boolean get(Time iterator, int day)
429         {
430             int realYear = iterator.getYear();
431             int realMonth = iterator.getMonth();
432 
433             Time t = null;
434 
435             if (SPEW) {
436                 Log.i(TAG, "get called with iterator=" + iterator
437                         + " " + iterator.getMonth()
438                         + "/" + iterator.getDay()
439                         + "/" + iterator.getYear() + " day=" + day);
440             }
441             if (day < 1 || day > 28) {
442                 // if might be past the end of the month, we need to normalize it
443                 t = mTime;
444                 t.set(day, realMonth, realYear);
445                 unsafeNormalize(t);
446                 realYear = t.getYear();
447                 realMonth = t.getMonth();
448                 day = t.getDay();
449                 if (SPEW) {
450                     Log.i(TAG, "normalized t=" + t + " " + t.getMonth()
451                             + "/" + t.getDay()
452                             + "/" + t.getYear());
453                 }
454             }
455 
456             /*
457             if (true || SPEW) {
458                 Log.i(TAG, "set t=" + t + " " + realMonth + "/" + day + "/" + realYear);
459             }
460             */
461             if (realYear != mYear || realMonth != mMonth) {
462                 if (t == null) {
463                     t = mTime;
464                     t.set(day, realMonth, realYear);
465                     unsafeNormalize(t);
466                     if (SPEW) {
467                         Log.i(TAG, "set t=" + t + " " + t.getMonth()
468                                 + "/" + t.getDay()
469                                 + "/" + t.getYear()
470                                 + " realMonth=" + realMonth + " mMonth=" + mMonth);
471                     }
472                 }
473                 mYear = realYear;
474                 mMonth = realMonth;
475                 mDays = generateDaysList(t, mR);
476                 if (SPEW) {
477                     Log.i(TAG, "generated days list");
478                 }
479             }
480             return (mDays & (1<<day)) != 0;
481         }
482 
483         /**
484          * Fill in a bit set containing the days of the month on which this
485          * will occur.
486          *
487          * Only call this if the r.freq > DAILY.  Otherwise, we should be
488          * processing the BYDAY, BYMONTHDAY, etc. as filters instead.
489          *
490          * monthOffset may be -1, 0 or 1
491          */
generateDaysList(Time generated, EventRecurrence r)492         private static int generateDaysList(Time generated, EventRecurrence r)
493         {
494             int days = 0;
495 
496             int i, count, v;
497             int[] byday, bydayNum, bymonthday;
498             int j, lastDayThisMonth;
499             int first; // Time.SUNDAY, etc
500 
501             lastDayThisMonth = generated.getActualMaximum(Time.MONTH_DAY);
502 
503             // BYDAY
504             count = r.bydayCount;
505             if (count > 0) {
506                 // calculate the day of week for the first of this month (first)
507                 j = generated.getDay();
508                 while (j >= 8) {
509                     j -= 7;
510                 }
511                 first = generated.getWeekDay();
512                 if (first >= j) {
513                     first = first - j + 1;
514                 } else {
515                     first = first - j + 8;
516                 }
517 
518                 // What to do if the event is weekly:
519                 // This isn't ideal, but we'll generate a month's worth of events
520                 // and the code that calls this will only use the ones that matter
521                 // for the current week.
522                 byday = r.byday;
523                 bydayNum = r.bydayNum;
524                 for (i=0; i<count; i++) {
525                     v = bydayNum[i];
526                     j = EventRecurrence.day2TimeDay(byday[i]) - first + 1;
527                     if (j <= 0) {
528                         j += 7;
529                     }
530                     if (v == 0) {
531                         // v is 0, each day in the month/week
532                         for (; j<=lastDayThisMonth; j+=7) {
533                             if (SPEW) Log.i(TAG, "setting " + j + " for rule "
534                                     + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
535                             days |= 1 << j;
536                         }
537                     }
538                     else if (v > 0) {
539                         // v is positive, count from the beginning of the month
540                         // -1 b/c the first one should add 0
541                         j += 7*(v-1);
542                         if (j <= lastDayThisMonth) {
543                             if (SPEW) Log.i(TAG, "setting " + j + " for rule "
544                                     + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
545                             // if it's impossible, we drop it
546                             days |= 1 << j;
547                         }
548                     }
549                     else {
550                         // v is negative, count from the end of the month
551                         // find the last one
552                         for (; j<=lastDayThisMonth; j+=7) {
553                         }
554                         // v is negative
555                         // should do +1 b/c the last one should add 0, but we also
556                         // skipped the j -= 7 b/c the loop to find the last one
557                         // overshot by one week
558                         j += 7*v;
559                         if (j >= 1) {
560                             if (SPEW) Log.i(TAG, "setting " + j + " for rule "
561                                     + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
562                             days |= 1 << j;
563                         }
564                     }
565                 }
566             }
567 
568             // BYMONTHDAY
569             // Q: What happens if we have BYMONTHDAY and BYDAY?
570             // A: I didn't see it in the spec, so in lieu of that, we'll
571             // intersect the two.  That seems reasonable to me.
572             if (r.freq > EventRecurrence.WEEKLY) {
573                 count = r.bymonthdayCount;
574                 if (count != 0) {
575                     bymonthday = r.bymonthday;
576                     if (r.bydayCount == 0) {
577                         for (i=0; i<count; i++) {
578                             v = bymonthday[i];
579                             if (v >= 0) {
580                                 days |= 1 << v;
581                             } else {
582                                 j = lastDayThisMonth + v + 1; // v is negative
583                                 if (j >= 1 && j <= lastDayThisMonth) {
584                                     days |= 1 << j;
585                                 }
586                             }
587                         }
588                     } else {
589                         // This is O(lastDayThisMonth*count), which is really
590                         // O(count) with a decent sized constant.
591                         for (j=1; j<=lastDayThisMonth; j++) {
592                             next_day : {
593                                 if ((days&(1<<j)) != 0) {
594                                     for (i=0; i<count; i++) {
595                                         if (bymonthday[i] == j) {
596                                             break next_day;
597                                         }
598                                     }
599                                     days &= ~(1<<j);
600                                 }
601                             }
602                         }
603                     }
604                 }
605             }
606             return days;
607         }
608 
609         private EventRecurrence mR;
610         private int mDays;
611         private Time mTime;
612         private int mYear;
613         private int mMonth;
614     }
615 
616     /**
617      * Expands the recurrence within the given range using the given dtstart
618      * value. Returns an array of longs where each element is a date in UTC
619      * milliseconds. The return value is never null.  If there are no dates
620      * then an array of length zero is returned.
621      *
622      * @param dtstart a Time object representing the first occurrence
623      * @param recur the recurrence rules, including RRULE, RDATES, EXRULE, and
624      * EXDATES
625      * @param rangeStartMillis the beginning of the range to expand, in UTC
626      * milliseconds
627      * @param rangeEndMillis the non-inclusive end of the range to expand, in
628      * UTC milliseconds; use -1 for the entire range.
629      * @return an array of dates, each date is in UTC milliseconds
630      * @throws DateException
631      * @throws IllegalArgumentException if recur cannot be parsed
632      */
expand(Time dtstart, RecurrenceSet recur, long rangeStartMillis, long rangeEndMillis)633     public long[] expand(Time dtstart,
634             RecurrenceSet recur,
635             long rangeStartMillis,
636             long rangeEndMillis) throws DateException {
637         String timezone = dtstart.getTimezone();
638         mIterator.clear(timezone);
639         mGenerated.clear(timezone);
640 
641         // We don't need to clear the mUntil (and it wouldn't do any good to
642         // do so) because the "until" date string is specified in UTC and that
643         // sets the timezone in the mUntil Time object.
644 
645         mIterator.set(rangeStartMillis);
646         long rangeStartDateValue = normDateTimeComparisonValue(mIterator);
647 
648         long rangeEndDateValue;
649         if (rangeEndMillis != -1) {
650             mIterator.set(rangeEndMillis);
651             rangeEndDateValue = normDateTimeComparisonValue(mIterator);
652         } else {
653             rangeEndDateValue = Long.MAX_VALUE;
654         }
655 
656         TreeSet<Long> dtSet = new TreeSet<Long>();
657 
658         if (recur.rrules != null) {
659             for (EventRecurrence rrule : recur.rrules) {
660                 expand(dtstart, rrule, rangeStartDateValue,
661                         rangeEndDateValue, true /* add */, dtSet);
662             }
663         }
664         if (recur.rdates != null) {
665             for (long dt : recur.rdates) {
666                 // The dates are stored as milliseconds. We need to convert
667                 // them to year/month/day values in the local timezone.
668                 mIterator.set(dt);
669                 long dtvalue = normDateTimeComparisonValue(mIterator);
670                 dtSet.add(dtvalue);
671             }
672         }
673         if (recur.exrules != null) {
674             for (EventRecurrence exrule : recur.exrules) {
675                 expand(dtstart, exrule, rangeStartDateValue,
676                         rangeEndDateValue, false /* remove */, dtSet);
677             }
678         }
679         if (recur.exdates != null) {
680             for (long dt : recur.exdates) {
681                 // The dates are stored as milliseconds. We need to convert
682                 // them to year/month/day values in the local timezone.
683                 mIterator.set(dt);
684                 long dtvalue = normDateTimeComparisonValue(mIterator);
685                 dtSet.remove(dtvalue);
686             }
687         }
688         if (dtSet.isEmpty()) {
689             // this can happen if the recurrence does not occur within the
690             // expansion window.
691             return new long[0];
692         }
693 
694         // The values in dtSet are represented in a special form that is useful
695         // for fast comparisons and that is easy to generate from year/month/day
696         // values. We need to convert these to UTC milliseconds and also to
697         // ensure that the dates are valid.
698         int len = dtSet.size();
699         long[] dates = new long[len];
700         int i = 0;
701         for (Long val: dtSet) {
702             setTimeFromLongValue(mIterator, val);
703             dates[i++] = mIterator.toMillis();
704         }
705         return dates;
706     }
707 
708     /**
709      * Run the recurrence algorithm.  Processes events defined in the local
710      * timezone of the event.  Return a list of iCalendar DATETIME
711      * strings containing the start date/times of the occurrences; the output
712      * times are defined in the local timezone of the event.
713      *
714      * If you want all of the events, pass Long.MAX_VALUE for rangeEndDateValue.  If you pass
715      * Long.MAX_VALUE for rangeEnd, and the event doesn't have a COUNT or UNTIL field,
716      * you'll get a DateException.
717      *
718      * @param dtstart the dtstart date as defined in RFC2445.  This
719      * {@link Time} should be in the timezone of the event.
720      * @param r the parsed recurrence, as defiend in RFC2445
721      * @param rangeStartDateValue the first date-time you care about, inclusive
722      * @param rangeEndDateValue the last date-time you care about, not inclusive (so
723      *                  if you care about everything up through and including
724      *                  Dec 22 1995, set last to Dec 23, 1995 00:00:00
725      * @param add Whether or not we should add to out, or remove from out.
726      * @param out the TreeSet you'd like to fill with the events
727      * @throws DateException
728      * @throws IllegalArgumentException if r cannot be parsed.
729      */
expand(Time dtstart, EventRecurrence r, long rangeStartDateValue, long rangeEndDateValue, boolean add, TreeSet<Long> out)730     public void expand(Time dtstart,
731             EventRecurrence r,
732             long rangeStartDateValue,
733             long rangeEndDateValue,
734             boolean add,
735             TreeSet<Long> out) throws DateException {
736         unsafeNormalize(dtstart);
737         long dtstartDateValue = normDateTimeComparisonValue(dtstart);
738         int count = 0;
739 
740         // add the dtstart instance to the recurrence, if within range.
741         // For example, if dtstart is Mar 1, 2010 and the range is Jan 1 - Apr 1,
742         // then add it here and increment count.  If the range is earlier or later,
743         // then don't add it here.  In that case, count will be incremented later
744         // inside  the loop.  It is important that count gets incremented exactly
745         // once here or in the loop for dtstart.
746         //
747         // NOTE: if DTSTART is not synchronized with the recurrence rule, the first instance
748         //       we return will not fit the RRULE pattern.
749         if (add && dtstartDateValue >= rangeStartDateValue
750                 && dtstartDateValue < rangeEndDateValue) {
751             out.add(dtstartDateValue);
752             ++count;
753         }
754 
755         Time iterator = mIterator;
756         Time until = mUntil;
757         StringBuilder sb = mStringBuilder;
758         Time generated = mGenerated;
759         DaySet days = mDays;
760 
761         try {
762 
763             days.setRecurrence(r);
764             if (rangeEndDateValue == Long.MAX_VALUE && r.until == null && r.count == 0) {
765                 throw new DateException(
766                         "No range end provided for a recurrence that has no UNTIL or COUNT.");
767             }
768 
769             // the top-level frequency
770             int freqField;
771             int freqAmount = r.interval;
772             int freq = r.freq;
773             switch (freq)
774             {
775                 case EventRecurrence.SECONDLY:
776                     freqField = Time.SECOND;
777                     break;
778                 case EventRecurrence.MINUTELY:
779                     freqField = Time.MINUTE;
780                     break;
781                 case EventRecurrence.HOURLY:
782                     freqField = Time.HOUR;
783                     break;
784                 case EventRecurrence.DAILY:
785                     freqField = Time.MONTH_DAY;
786                     break;
787                 case EventRecurrence.WEEKLY:
788                     freqField = Time.MONTH_DAY;
789                     freqAmount = 7 * r.interval;
790                     if (freqAmount <= 0) {
791                         freqAmount = 7;
792                     }
793                     break;
794                 case EventRecurrence.MONTHLY:
795                     freqField = Time.MONTH;
796                     break;
797                 case EventRecurrence.YEARLY:
798                     freqField = Time.YEAR;
799                     break;
800                 default:
801                     throw new DateException("bad freq=" + freq);
802             }
803             if (freqAmount <= 0) {
804                 freqAmount = 1;
805             }
806 
807             int bymonthCount = r.bymonthCount;
808             boolean usebymonth = useBYX(freq, EventRecurrence.MONTHLY, bymonthCount);
809             boolean useDays = freq >= EventRecurrence.WEEKLY &&
810                                  (r.bydayCount > 0 || r.bymonthdayCount > 0);
811             int byhourCount = r.byhourCount;
812             boolean usebyhour = useBYX(freq, EventRecurrence.HOURLY, byhourCount);
813             int byminuteCount = r.byminuteCount;
814             boolean usebyminute = useBYX(freq, EventRecurrence.MINUTELY, byminuteCount);
815             int bysecondCount = r.bysecondCount;
816             boolean usebysecond = useBYX(freq, EventRecurrence.SECONDLY, bysecondCount);
817 
818             // initialize the iterator
819             iterator.set(dtstart);
820             if (freqField == Time.MONTH) {
821                 if (useDays) {
822                     // if it's monthly, and we're going to be generating
823                     // days, set the iterator day field to 1 because sometimes
824                     // we'll skip months if it's greater than 28.
825                     // XXX Do we generate days for MONTHLY w/ BYHOUR?  If so,
826                     // we need to do this then too.
827                     iterator.setDay(1);
828                 }
829             }
830 
831             long untilDateValue;
832             if (r.until != null) {
833                 // Ensure that the "until" date string is specified in UTC.
834                 String untilStr = r.until;
835                 // 15 is length of date-time without trailing Z e.g. "20090204T075959"
836                 // A string such as 20090204 is a valid UNTIL (see RFC 2445) and the
837                 // Z should not be added.
838                 if (untilStr.length() == 15) {
839                     untilStr = untilStr + 'Z';
840                 }
841                 // The parse() method will set the timezone to UTC
842                 until.parse(untilStr);
843 
844                 // We need the "until" year/month/day values to be in the same
845                 // timezone as all the generated dates so that we can compare them
846                 // using the values returned by normDateTimeComparisonValue().
847                 until.switchTimezone(dtstart.getTimezone());
848                 untilDateValue = normDateTimeComparisonValue(until);
849             } else {
850                 untilDateValue = Long.MAX_VALUE;
851             }
852 
853             sb.ensureCapacity(15);
854             sb.setLength(15); // TODO: pay attention to whether or not the event
855             // is an all-day one.
856 
857             if (SPEW) {
858                 Log.i(TAG, "expand called w/ rangeStart=" + rangeStartDateValue
859                         + " rangeEnd=" + rangeEndDateValue);
860             }
861 
862             // go until the end of the range or we're done with this event
863             int failsafe = 0; // Avoid infinite loops
864             events: {
865                 while (true) {
866                     int monthIndex = 0;
867                     if (failsafe++ > MAX_ALLOWED_ITERATIONS) { // Give up after about 1 second of processing
868                         Log.w(TAG, "Recurrence processing stuck with r=" + r + " rangeStart="
869                                   + rangeStartDateValue + " rangeEnd=" + rangeEndDateValue);
870                         break;
871                     }
872 
873                     unsafeNormalize(iterator);
874 
875                     int iteratorYear = iterator.getYear();
876                     int iteratorMonth = iterator.getMonth() + 1;
877                     int iteratorDay = iterator.getDay();
878                     int iteratorHour = iterator.getHour();
879                     int iteratorMinute = iterator.getMinute();
880                     int iteratorSecond = iterator.getSecond();
881 
882                     // year is never expanded -- there is no BYYEAR
883                     generated.set(iterator);
884 
885                     if (SPEW) Log.i(TAG, "year=" + generated.getYear());
886 
887                     do { // month
888                         int month = usebymonth
889                                         ? r.bymonth[monthIndex]
890                                         : iteratorMonth;
891                         month--;
892                         if (SPEW) Log.i(TAG, "  month=" + month);
893 
894                         int dayIndex = 1;
895                         int lastDayToExamine = 0;
896 
897                         // Use this to handle weeks that overlap the end of the month.
898                         // Keep the year and month that days is for, and generate it
899                         // when needed in the loop
900                         if (useDays) {
901                             // Determine where to start and end, don't worry if this happens
902                             // to be before dtstart or after the end, because that will be
903                             // filtered in the inner loop
904                             if (freq == EventRecurrence.WEEKLY) {
905                                 /*
906                                  * iterator.weekDay indicates the day of the week (0-6, SU-SA).
907                                  * Because dayIndex might start in the middle of a week, and we're
908                                  * interested in treating a week as a unit, we want to move
909                                  * backward to the start of the week.  (This could make the
910                                  * dayIndex negative, which will be corrected by normalization
911                                  * later on.)
912                                  *
913                                  * The day that starts the week is determined by WKST, which
914                                  * defaults to MO.
915                                  *
916                                  * Example: dayIndex is Tuesday the 8th, and weeks start on
917                                  * Thursdays.  Tuesday is day 2, Thursday is day 4, so we
918                                  * want to move back (2 - 4 + 7) % 7 = 5 days to the previous
919                                  * Thursday.  If weeks started on Mondays, we would only
920                                  * need to move back (2 - 1 + 7) % 7 = 1 day.
921                                  */
922                                 int weekStartAdj = (iterator.getWeekDay() -
923                                         EventRecurrence.day2TimeDay(r.wkst) + 7) % 7;
924                                 dayIndex = iterator.getDay() - weekStartAdj;
925                                 lastDayToExamine = dayIndex + 6;
926                             } else {
927                                 lastDayToExamine = generated
928                                     .getActualMaximum(Time.MONTH_DAY);
929                             }
930                             if (SPEW) Log.i(TAG, "dayIndex=" + dayIndex
931                                     + " lastDayToExamine=" + lastDayToExamine
932                                     + " days=" + days);
933                         }
934 
935                         do { // day
936                             int day;
937                             if (useDays) {
938                                 if (!days.get(iterator, dayIndex)) {
939                                     dayIndex++;
940                                     continue;
941                                 } else {
942                                     day = dayIndex;
943                                 }
944                             } else {
945                                 day = iteratorDay;
946                             }
947                             if (SPEW) Log.i(TAG, "    day=" + day);
948 
949                             // hour
950                             int hourIndex = 0;
951                             do {
952                                 int hour = usebyhour
953                                                 ? r.byhour[hourIndex]
954                                                 : iteratorHour;
955                                 if (SPEW) Log.i(TAG, "      hour=" + hour + " usebyhour=" + usebyhour);
956 
957                                 // minute
958                                 int minuteIndex = 0;
959                                 do {
960                                     int minute = usebyminute
961                                                     ? r.byminute[minuteIndex]
962                                                     : iteratorMinute;
963                                     if (SPEW) Log.i(TAG, "        minute=" + minute);
964 
965                                     // second
966                                     int secondIndex = 0;
967                                     do {
968                                         int second = usebysecond
969                                                         ? r.bysecond[secondIndex]
970                                                         : iteratorSecond;
971                                         if (SPEW) Log.i(TAG, "          second=" + second);
972 
973                                         // we do this here each time, because if we distribute it, we find the
974                                         // month advancing extra times, as we set the month to the 32nd, 33rd, etc.
975                                         // days.
976                                         generated.set(second, minute, hour, day, month, iteratorYear);
977                                         unsafeNormalize(generated);
978 
979                                         long genDateValue = normDateTimeComparisonValue(generated);
980                                         // sometimes events get generated (BYDAY, BYHOUR, etc.) that
981                                         // are before dtstart.  Filter these.  I believe this is correct,
982                                         // but Google Calendar doesn't seem to always do this.
983                                         if (genDateValue >= dtstartDateValue) {
984                                             // filter and then add
985                                             // TODO: we don't check for stop conditions (like
986                                             //       passing the "end" date) unless the filter
987                                             //       allows the event.  Could stop sooner.
988                                             int filtered = filter(r, generated);
989                                             if (0 == filtered) {
990 
991                                                 // increase the count as long
992                                                 // as this isn't the same
993                                                 // as the first instance
994                                                 // specified by the DTSTART
995                                                 // (for RRULEs -- additive).
996                                                 // This condition must be the complement of the
997                                                 // condition for incrementing count at the
998                                                 // beginning of the method, so if we don't
999                                                 // increment count there, we increment it here.
1000                                                 // For example, if add is set and dtstartDateValue
1001                                                 // is inside the start/end range, then it was added
1002                                                 // and count was incremented at the beginning.
1003                                                 // If dtstartDateValue is outside the range or add
1004                                                 // is not set, then we must increment count here.
1005                                                 if (!(dtstartDateValue == genDateValue
1006                                                         && add
1007                                                         && dtstartDateValue >= rangeStartDateValue
1008                                                         && dtstartDateValue < rangeEndDateValue)) {
1009                                                     ++count;
1010                                                 }
1011                                                 // one reason we can stop is that
1012                                                 // we're past the until date
1013                                                 if (genDateValue > untilDateValue) {
1014                                                     if (SPEW) {
1015                                                         Log.i(TAG, "stopping b/c until="
1016                                                             + untilDateValue
1017                                                             + " generated="
1018                                                             + genDateValue);
1019                                                     }
1020                                                     break events;
1021                                                 }
1022                                                 // or we're past rangeEnd
1023                                                 if (genDateValue >= rangeEndDateValue) {
1024                                                     if (SPEW) {
1025                                                         Log.i(TAG, "stopping b/c rangeEnd="
1026                                                                 + rangeEndDateValue
1027                                                                 + " generated=" + generated);
1028                                                     }
1029                                                     break events;
1030                                                 }
1031 
1032                                                 if (genDateValue >= rangeStartDateValue) {
1033                                                     if (SPEW) {
1034                                                         Log.i(TAG, "adding date=" + generated + " filtered=" + filtered);
1035                                                     }
1036                                                     if (add) {
1037                                                         out.add(genDateValue);
1038                                                     } else {
1039                                                         out.remove(genDateValue);
1040                                                     }
1041                                                 }
1042                                                 // another is that count is high enough
1043                                                 if (r.count > 0 && r.count == count) {
1044                                                     //Log.i(TAG, "stopping b/c count=" + count);
1045                                                     break events;
1046                                                 }
1047                                             }
1048                                         }
1049                                         secondIndex++;
1050                                     } while (usebysecond && secondIndex < bysecondCount);
1051                                     minuteIndex++;
1052                                 } while (usebyminute && minuteIndex < byminuteCount);
1053                                 hourIndex++;
1054                             } while (usebyhour && hourIndex < byhourCount);
1055                             dayIndex++;
1056                         } while (useDays && dayIndex <= lastDayToExamine);
1057                         monthIndex++;
1058                     } while (usebymonth && monthIndex < bymonthCount);
1059 
1060                     // Add freqAmount to freqField until we get another date that we want.
1061                     // We don't want to "generate" dates with the iterator.
1062                     // XXX: We do this for days, because there is a varying number of days
1063                     // per month
1064                     int oldDay = iterator.getDay();
1065                     generated.set(iterator);  // just using generated as a temporary.
1066                     int n = 1;
1067                     while (true) {
1068                         int value = freqAmount * n;
1069                         switch (freqField) {
1070                             case Time.SECOND:
1071                             case Time.MINUTE:
1072                             case Time.HOUR:
1073                             case Time.MONTH_DAY:
1074                             case Time.MONTH:
1075                             case Time.YEAR:
1076                             case Time.WEEK_DAY:
1077                             case Time.YEAR_DAY:
1078                                 iterator.add(freqField, value);
1079                                 break;
1080                             default:
1081                                 throw new RuntimeException("bad field=" + freqField);
1082                         }
1083 
1084                         unsafeNormalize(iterator);
1085                         if (freqField != Time.YEAR && freqField != Time.MONTH) {
1086                             break;
1087                         }
1088                         if (iterator.getDay() == oldDay) {
1089                             break;
1090                         }
1091                         n++;
1092                         iterator.set(generated);
1093                     }
1094                 }
1095             }
1096         }
1097         catch (DateException e) {
1098             Log.w(TAG, "DateException with r=" + r + " rangeStart=" + rangeStartDateValue
1099                     + " rangeEnd=" + rangeEndDateValue);
1100             throw e;
1101         }
1102         catch (RuntimeException t) {
1103             Log.w(TAG, "RuntimeException with r=" + r + " rangeStart=" + rangeStartDateValue
1104                     + " rangeEnd=" + rangeEndDateValue);
1105             throw t;
1106         }
1107     }
1108 
1109     /**
1110      * Normalizes the date fields to give a valid date, but if the time falls
1111      * in the invalid window during a transition out of Daylight Saving Time
1112      * when time jumps forward an hour, then the "normalized" value will be
1113      * invalid.
1114      * <p>
1115      * This method also computes the weekDay and yearDay fields.
1116      *
1117      * <p>
1118      * This method does not modify the fields isDst, or gmtOff.
1119      */
unsafeNormalize(Time date)1120     static void unsafeNormalize(Time date) {
1121         int second = date.getSecond();
1122         int minute = date.getMinute();
1123         int hour = date.getHour();
1124         int monthDay = date.getDay();
1125         int month = date.getMonth();
1126         int year = date.getYear();
1127 
1128         int addMinutes = ((second < 0) ? (second - 59) : second) / 60;
1129         second -= addMinutes * 60;
1130         minute += addMinutes;
1131         int addHours = ((minute < 0) ? (minute - 59) : minute) / 60;
1132         minute -= addHours * 60;
1133         hour += addHours;
1134         int addDays = ((hour < 0) ? (hour - 23) : hour) / 24;
1135         hour -= addDays * 24;
1136         monthDay += addDays;
1137 
1138         // We want to make "monthDay" positive. We do this by subtracting one
1139         // from the year and adding a year's worth of days to "monthDay" in
1140         // the following loop while "monthDay" <= 0.
1141         while (monthDay <= 0) {
1142             // If month is after Feb, then add this year's length so that we
1143             // include this year's leap day, if any.
1144             // Otherwise (the month is Feb or earlier), add last year's length.
1145             // Subtract one from the year in either case. This gives the same
1146             // effective date but makes monthDay (the day of the month) much
1147             // larger. Eventually (usually in one iteration) monthDay will
1148             // be positive.
1149             int days = month > 1 ? yearLength(year) : yearLength(year - 1);
1150             monthDay += days;
1151             year -= 1;
1152         }
1153         // At this point, monthDay >= 1. Normalize the month to the range [0,11].
1154         if (month < 0) {
1155             int years = (month + 1) / 12 - 1;
1156             year += years;
1157             month -= 12 * years;
1158         } else if (month >= 12) {
1159             int years = month / 12;
1160             year += years;
1161             month -= 12 * years;
1162         }
1163         // At this point, month is in the range [0,11] and monthDay >= 1.
1164         // Now loop until the monthDay is in the correct range for the month.
1165         while (true) {
1166             // On January, check if we can jump forward a whole year.
1167             if (month == 0) {
1168                 int yearLength = yearLength(year);
1169                 if (monthDay > yearLength) {
1170                     year++;
1171                     monthDay -= yearLength;
1172                 }
1173             }
1174             int monthLength = monthLength(year, month);
1175             if (monthDay > monthLength) {
1176                 monthDay -= monthLength;
1177                 month++;
1178                 if (month >= 12) {
1179                     month -= 12;
1180                     year++;
1181                 }
1182             } else break;
1183         }
1184         // At this point, monthDay <= the length of the current month and is
1185         // in the range [1,31].
1186 
1187         date.setSecond(second);
1188         date.setMinute(minute);
1189         date.setHour(hour);
1190         date.setDay(monthDay);
1191         date.setMonth(month);
1192         date.setYear(year);
1193         date.setWeekDay(weekDay(year, month, monthDay));
1194         date.setYearDay(yearDay(year, month, monthDay));
1195     }
1196 
1197     /**
1198      * Returns true if the given year is a leap year.
1199      *
1200      * @param year the given year to test
1201      * @return true if the given year is a leap year.
1202      */
isLeapYear(int year)1203     static boolean isLeapYear(int year) {
1204         return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
1205     }
1206 
1207     /**
1208      * Returns the number of days in the given year.
1209      *
1210      * @param year the given year
1211      * @return the number of days in the given year.
1212      */
yearLength(int year)1213     static int yearLength(int year) {
1214         return isLeapYear(year) ? 366 : 365;
1215     }
1216 
1217     private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31,
1218             31, 30, 31, 30, 31 };
1219     private static final int[] DAYS_IN_YEAR_PRECEDING_MONTH = { 0, 31, 59, 90,
1220         120, 151, 181, 212, 243, 273, 304, 334 };
1221 
1222     /**
1223      * Returns the number of days in the given month of the given year.
1224      *
1225      * @param year the given year.
1226      * @param month the given month in the range [0,11]
1227      * @return the number of days in the given month of the given year.
1228      */
monthLength(int year, int month)1229     static int monthLength(int year, int month) {
1230         int n = DAYS_PER_MONTH[month];
1231         if (n != 28) {
1232             return n;
1233         }
1234         return isLeapYear(year) ? 29 : 28;
1235     }
1236 
1237     /**
1238      * Computes the weekday, a number in the range [0,6] where Sunday=0, from
1239      * the given year, month, and day.
1240      *
1241      * @param year the year
1242      * @param month the 0-based month in the range [0,11]
1243      * @param day the 1-based day of the month in the range [1,31]
1244      * @return the weekday, a number in the range [0,6] where Sunday=0
1245      */
weekDay(int year, int month, int day)1246     static int weekDay(int year, int month, int day) {
1247         if (month <= 1) {
1248             month += 12;
1249             year -= 1;
1250         }
1251         return (day + (13 * month - 14) / 5 + year + year/4 - year/100 + year/400) % 7;
1252     }
1253 
1254     /**
1255      * Computes the 0-based "year day", given the year, month, and day.
1256      *
1257      * @param year the year
1258      * @param month the 0-based month in the range [0,11]
1259      * @param day the 1-based day in the range [1,31]
1260      * @return the 0-based "year day", the number of days into the year
1261      */
yearDay(int year, int month, int day)1262     static int yearDay(int year, int month, int day) {
1263         int yearDay = DAYS_IN_YEAR_PRECEDING_MONTH[month] + day - 1;
1264         if (month >= 2 && isLeapYear(year)) {
1265             yearDay += 1;
1266         }
1267         return yearDay;
1268     }
1269 
1270     /**
1271      * Converts a normalized Time value to a 64-bit long. The mapping of Time
1272      * values to longs provides a total ordering on the Time values so that
1273      * two Time values can be compared efficiently by comparing their 64-bit
1274      * long values.  This is faster than converting the Time values to UTC
1275      * millliseconds.
1276      *
1277      * @param normalized a Time object whose date and time fields have been
1278      * normalized
1279      * @return a 64-bit long value that can be used for comparing and ordering
1280      * dates and times represented by Time objects
1281      */
normDateTimeComparisonValue(Time normalized)1282     private static final long normDateTimeComparisonValue(Time normalized) {
1283         // 37 bits for the year, 4 bits for the month, 5 bits for the monthDay,
1284         // 5 bits for the hour, 6 bits for the minute, 6 bits for the second.
1285         return ((long)normalized.getYear() << 26) + (normalized.getMonth() << 22)
1286                 + (normalized.getDay() << 17) + (normalized.getHour() << 12)
1287                 + (normalized.getMinute() << 6) + normalized.getSecond();
1288     }
1289 
setTimeFromLongValue(Time date, long val)1290     private static final void setTimeFromLongValue(Time date, long val) {
1291         date.setYear((int) (val >> 26));
1292         date.setMonth((int) (val >> 22) & 0xf);
1293         date.setDay((int) (val >> 17) & 0x1f);
1294         date.setHour((int) (val >> 12) & 0x1f);
1295         date.setMinute((int) (val >> 6) & 0x3f);
1296         date.setSecond((int) (val & 0x3f));
1297     }
1298 }
1299