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