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