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