| /* //device/content/providers/pim/RecurrenceProcessor.java |
| ** |
| ** Copyright 2006, The Android Open Source Project |
| ** |
| ** Licensed under the Apache License, Version 2.0 (the "License"); |
| ** you may not use this file except in compliance with the License. |
| ** You may obtain a copy of the License at |
| ** |
| ** http://www.apache.org/licenses/LICENSE-2.0 |
| ** |
| ** Unless required by applicable law or agreed to in writing, software |
| ** distributed under the License is distributed on an "AS IS" BASIS, |
| ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| ** See the License for the specific language governing permissions and |
| ** limitations under the License. |
| */ |
| |
| package com.android.calendarcommon2; |
| |
| import android.text.format.Time; |
| import android.util.Log; |
| |
| import java.util.TreeSet; |
| |
| public class RecurrenceProcessor |
| { |
| // these are created once and reused. |
| private Time mIterator = new Time(Time.TIMEZONE_UTC); |
| private Time mUntil = new Time(Time.TIMEZONE_UTC); |
| private StringBuilder mStringBuilder = new StringBuilder(); |
| private Time mGenerated = new Time(Time.TIMEZONE_UTC); |
| private DaySet mDays = new DaySet(false); |
| // Give up after this many loops. This is roughly 1 second of expansion. |
| private static final int MAX_ALLOWED_ITERATIONS = 2000; |
| |
| public RecurrenceProcessor() |
| { |
| } |
| |
| private static final String TAG = "RecurrenceProcessor"; |
| |
| private static final boolean SPEW = false; |
| |
| /** |
| * Returns the time (millis since epoch) of the last occurrence, |
| * or -1 if the event repeats forever. If there are no occurrences |
| * (because the exrule or exdates cancel all the occurrences) and the |
| * event does not repeat forever, then 0 is returned. |
| * |
| * This computes a conservative estimate of the last occurrence. That is, |
| * the time of the actual last occurrence might be earlier than the time |
| * returned by this method. |
| * |
| * @param dtstart the time of the first occurrence |
| * @param recur the recurrence |
| * @return an estimate of the time (in UTC milliseconds) of the last |
| * occurrence, which may be greater than the actual last occurrence |
| * @throws DateException |
| */ |
| public long getLastOccurence(Time dtstart, |
| RecurrenceSet recur) throws DateException { |
| return getLastOccurence(dtstart, null /* no limit */, recur); |
| } |
| |
| /** |
| * Returns the time (millis since epoch) of the last occurrence, |
| * or -1 if the event repeats forever. If there are no occurrences |
| * (because the exrule or exdates cancel all the occurrences) and the |
| * event does not repeat forever, then 0 is returned. |
| * |
| * This computes a conservative estimate of the last occurrence. That is, |
| * the time of the actual last occurrence might be earlier than the time |
| * returned by this method. |
| * |
| * @param dtstart the time of the first occurrence |
| * @param maxtime the max possible time of the last occurrence. null means no limit |
| * @param recur the recurrence |
| * @return an estimate of the time (in UTC milliseconds) of the last |
| * occurrence, which may be greater than the actual last occurrence |
| * @throws DateException |
| */ |
| public long getLastOccurence(Time dtstart, Time maxtime, |
| RecurrenceSet recur) throws DateException { |
| long lastTime = -1; |
| boolean hasCount = false; |
| |
| // first see if there are any "until"s specified. if so, use the latest |
| // until / rdate. |
| if (recur.rrules != null) { |
| for (EventRecurrence rrule : recur.rrules) { |
| if (rrule.count != 0) { |
| hasCount = true; |
| } else if (rrule.until != null) { |
| // according to RFC 2445, until must be in UTC. |
| mIterator.parse(rrule.until); |
| long untilTime = mIterator.toMillis(false /* use isDst */); |
| if (untilTime > lastTime) { |
| lastTime = untilTime; |
| } |
| } |
| } |
| if (lastTime != -1 && recur.rdates != null) { |
| for (long dt : recur.rdates) { |
| if (dt > lastTime) { |
| lastTime = dt; |
| } |
| } |
| } |
| |
| // If there were only "until"s and no "count"s, then return the |
| // last "until" date or "rdate". |
| if (lastTime != -1 && !hasCount) { |
| return lastTime; |
| } |
| } else if (recur.rdates != null && |
| recur.exrules == null && recur.exdates == null) { |
| // if there are only rdates, we can just pick the last one. |
| for (long dt : recur.rdates) { |
| if (dt > lastTime) { |
| lastTime = dt; |
| } |
| } |
| return lastTime; |
| } |
| |
| // Expand the complete recurrence if there were any counts specified, |
| // or if there were rdates specified. |
| if (hasCount || recur.rdates != null || maxtime != null) { |
| // The expansion might not contain any dates if the exrule or |
| // exdates cancel all the generated dates. |
| long[] dates = expand(dtstart, recur, |
| dtstart.toMillis(false /* use isDst */) /* range start */, |
| (maxtime != null) ? |
| maxtime.toMillis(false /* use isDst */) : -1 /* range end */); |
| |
| // The expansion might not contain any dates if exrule or exdates |
| // cancel all the generated dates. |
| if (dates.length == 0) { |
| return 0; |
| } |
| return dates[dates.length - 1]; |
| } |
| return -1; |
| } |
| |
| /** |
| * a -- list of values |
| * N -- number of values to use in a |
| * v -- value to check for |
| */ |
| private static boolean listContains(int[] a, int N, int v) |
| { |
| for (int i=0; i<N; i++) { |
| if (a[i] == v) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * a -- list of values |
| * N -- number of values to use in a |
| * v -- value to check for |
| * max -- if a value in a is negative, add that negative value |
| * to max and compare that instead; this is how we deal with |
| * negative numbers being offsets from the end value |
| */ |
| private static boolean listContains(int[] a, int N, int v, int max) |
| { |
| for (int i=0; i<N; i++) { |
| int w = a[i]; |
| if (w > 0) { |
| if (w == v) { |
| return true; |
| } |
| } else { |
| max += w; // w is negative |
| if (max == v) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Filter out the ones for events whose BYxxx rule is for |
| * a period greater than or equal to the period of the FREQ. |
| * |
| * Returns 0 if the event should not be filtered out |
| * Returns something else (a rule number which is useful for debugging) |
| * if the event should not be returned |
| */ |
| private static int filter(EventRecurrence r, Time iterator) |
| { |
| boolean found; |
| int freq = r.freq; |
| |
| if (EventRecurrence.MONTHLY >= freq) { |
| // BYMONTH |
| if (r.bymonthCount > 0) { |
| found = listContains(r.bymonth, r.bymonthCount, |
| iterator.month + 1); |
| if (!found) { |
| return 1; |
| } |
| } |
| } |
| if (EventRecurrence.WEEKLY >= freq) { |
| // BYWEEK -- this is just a guess. I wonder how many events |
| // acutally use BYWEEKNO. |
| if (r.byweeknoCount > 0) { |
| found = listContains(r.byweekno, r.byweeknoCount, |
| iterator.getWeekNumber(), |
| iterator.getActualMaximum(Time.WEEK_NUM)); |
| if (!found) { |
| return 2; |
| } |
| } |
| } |
| if (EventRecurrence.DAILY >= freq) { |
| // BYYEARDAY |
| if (r.byyeardayCount > 0) { |
| found = listContains(r.byyearday, r.byyeardayCount, |
| iterator.yearDay, iterator.getActualMaximum(Time.YEAR_DAY)); |
| if (!found) { |
| return 3; |
| } |
| } |
| // BYMONTHDAY |
| if (r.bymonthdayCount > 0 ) { |
| found = listContains(r.bymonthday, r.bymonthdayCount, |
| iterator.monthDay, |
| iterator.getActualMaximum(Time.MONTH_DAY)); |
| if (!found) { |
| return 4; |
| } |
| } |
| // BYDAY -- when filtering, we ignore the number field, because it |
| // only is meaningful when creating more events. |
| byday: |
| if (r.bydayCount > 0) { |
| int a[] = r.byday; |
| int N = r.bydayCount; |
| int v = EventRecurrence.timeDay2Day(iterator.weekDay); |
| for (int i=0; i<N; i++) { |
| if (a[i] == v) { |
| break byday; |
| } |
| } |
| return 5; |
| } |
| } |
| if (EventRecurrence.HOURLY >= freq) { |
| // BYHOUR |
| found = listContains(r.byhour, r.byhourCount, |
| iterator.hour, |
| iterator.getActualMaximum(Time.HOUR)); |
| if (!found) { |
| return 6; |
| } |
| } |
| if (EventRecurrence.MINUTELY >= freq) { |
| // BYMINUTE |
| found = listContains(r.byminute, r.byminuteCount, |
| iterator.minute, |
| iterator.getActualMaximum(Time.MINUTE)); |
| if (!found) { |
| return 7; |
| } |
| } |
| if (EventRecurrence.SECONDLY >= freq) { |
| // BYSECOND |
| found = listContains(r.bysecond, r.bysecondCount, |
| iterator.second, |
| iterator.getActualMaximum(Time.SECOND)); |
| if (!found) { |
| return 8; |
| } |
| } |
| |
| if (r.bysetposCount > 0) { |
| bysetpos: |
| // BYSETPOS - we only handle rules like FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 |
| if (freq == EventRecurrence.MONTHLY && r.bydayCount > 0) { |
| // Check for stuff like BYDAY=1TU |
| for (int i = r.bydayCount - 1; i >= 0; i--) { |
| if (r.bydayNum[i] != 0) { |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.v(TAG, "BYSETPOS not supported with these rules: " + r); |
| } |
| break bysetpos; |
| } |
| } |
| if (!filterMonthlySetPos(r, iterator)) { |
| // Not allowed, filter it out. |
| return 9; |
| } |
| } else { |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.v(TAG, "BYSETPOS not supported with these rules: " + r); |
| } |
| } |
| // BYSETPOS was defined but we don't know how to handle it. Do no filtering based |
| // on it. |
| } |
| |
| // if we got to here, we didn't filter it out |
| return 0; |
| } |
| |
| /** |
| * Filters out instances that don't match the BYSETPOS clause of a monthly recurrence rule. |
| * This is an awkward and inefficient way to go about it. |
| * |
| * @returns true if this instance should be kept |
| */ |
| private static boolean filterMonthlySetPos(EventRecurrence r, Time instance) { |
| /* |
| * Compute the day of the week for the first day of the month. "instance" has a |
| * day number and a DotW, so we compute the DotW of the 1st from that. Note DotW |
| * is 0-6, where 0=SUNDAY. |
| * |
| * The basic calculation is to take the instance's "day of the week" number, subtract |
| * (day of the month - 1) mod 7, and then make sure it's positive. We can simplify |
| * that with some algebra. |
| */ |
| int dotw = (instance.weekDay - instance.monthDay + 36) % 7; |
| |
| /* |
| * The byday[] values are specified as bits, so we can just OR them all |
| * together. |
| */ |
| int bydayMask = 0; |
| for (int i = 0; i < r.bydayCount; i++) { |
| bydayMask |= r.byday[i]; |
| } |
| |
| /* |
| * Generate a set according to the BYDAY rules. For each day of the month, determine |
| * if its day of the week is included. If so, append it to the day set. |
| */ |
| int maxDay = instance.getActualMaximum(Time.MONTH_DAY); |
| int daySet[] = new int[maxDay]; |
| int daySetLength = 0; |
| |
| for (int md = 1; md <= maxDay; md++) { |
| // For each month day, see if it's part of the set. (This makes some assumptions |
| // about the exact form of the DotW constants.) |
| int dayBit = EventRecurrence.SU << dotw; |
| if ((bydayMask & dayBit) != 0) { |
| daySet[daySetLength++] = md; |
| } |
| |
| dotw++; |
| if (dotw == 7) |
| dotw = 0; |
| } |
| |
| /* |
| * Now walk through the BYSETPOS list and see if the instance is equal to any of the |
| * specified daySet entries. |
| */ |
| for (int i = r.bysetposCount - 1; i >= 0; i--) { |
| int index = r.bysetpos[i]; |
| if (index > 0) { |
| if (index > daySetLength) { |
| continue; // out of range |
| } |
| if (daySet[index-1] == instance.monthDay) { |
| return true; |
| } |
| } else if (index < 0) { |
| if (daySetLength + index < 0) { |
| continue; // out of range |
| } |
| if (daySet[daySetLength + index] == instance.monthDay) { |
| return true; |
| } |
| } else { |
| // should have been caught by parser |
| throw new RuntimeException("invalid bysetpos value"); |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| private static final int USE_ITERATOR = 0; |
| private static final int USE_BYLIST = 1; |
| |
| /** |
| * Return whether we should make this list from the BYxxx list or |
| * from the component of the iterator. |
| */ |
| int generateByList(int count, int freq, int byFreq) |
| { |
| if (byFreq >= freq) { |
| return USE_ITERATOR; |
| } else { |
| if (count == 0) { |
| return USE_ITERATOR; |
| } else { |
| return USE_BYLIST; |
| } |
| } |
| } |
| |
| private static boolean useBYX(int freq, int freqConstant, int count) |
| { |
| return freq > freqConstant && count > 0; |
| } |
| |
| public static class DaySet |
| { |
| public DaySet(boolean zulu) |
| { |
| mTime = new Time(Time.TIMEZONE_UTC); |
| } |
| |
| void setRecurrence(EventRecurrence r) |
| { |
| mYear = 0; |
| mMonth = -1; |
| mR = r; |
| } |
| |
| boolean get(Time iterator, int day) |
| { |
| int realYear = iterator.year; |
| int realMonth = iterator.month; |
| |
| Time t = null; |
| |
| if (SPEW) { |
| Log.i(TAG, "get called with iterator=" + iterator |
| + " " + iterator.month |
| + "/" + iterator.monthDay |
| + "/" + iterator.year + " day=" + day); |
| } |
| if (day < 1 || day > 28) { |
| // if might be past the end of the month, we need to normalize it |
| t = mTime; |
| t.set(day, realMonth, realYear); |
| unsafeNormalize(t); |
| realYear = t.year; |
| realMonth = t.month; |
| day = t.monthDay; |
| if (SPEW) { |
| Log.i(TAG, "normalized t=" + t + " " + t.month |
| + "/" + t.monthDay |
| + "/" + t.year); |
| } |
| } |
| |
| /* |
| if (true || SPEW) { |
| Log.i(TAG, "set t=" + t + " " + realMonth + "/" + day + "/" + realYear); |
| } |
| */ |
| if (realYear != mYear || realMonth != mMonth) { |
| if (t == null) { |
| t = mTime; |
| t.set(day, realMonth, realYear); |
| unsafeNormalize(t); |
| if (SPEW) { |
| Log.i(TAG, "set t=" + t + " " + t.month |
| + "/" + t.monthDay |
| + "/" + t.year |
| + " realMonth=" + realMonth + " mMonth=" + mMonth); |
| } |
| } |
| mYear = realYear; |
| mMonth = realMonth; |
| mDays = generateDaysList(t, mR); |
| if (SPEW) { |
| Log.i(TAG, "generated days list"); |
| } |
| } |
| return (mDays & (1<<day)) != 0; |
| } |
| |
| /** |
| * Fill in a bit set containing the days of the month on which this |
| * will occur. |
| * |
| * Only call this if the r.freq > DAILY. Otherwise, we should be |
| * processing the BYDAY, BYMONTHDAY, etc. as filters instead. |
| * |
| * monthOffset may be -1, 0 or 1 |
| */ |
| private static int generateDaysList(Time generated, EventRecurrence r) |
| { |
| int days = 0; |
| |
| int i, count, v; |
| int[] byday, bydayNum, bymonthday; |
| int j, lastDayThisMonth; |
| int first; // Time.SUNDAY, etc |
| int k; |
| |
| lastDayThisMonth = generated.getActualMaximum(Time.MONTH_DAY); |
| |
| // BYDAY |
| count = r.bydayCount; |
| if (count > 0) { |
| // calculate the day of week for the first of this month (first) |
| j = generated.monthDay; |
| while (j >= 8) { |
| j -= 7; |
| } |
| first = generated.weekDay; |
| if (first >= j) { |
| first = first - j + 1; |
| } else { |
| first = first - j + 8; |
| } |
| |
| // What to do if the event is weekly: |
| // This isn't ideal, but we'll generate a month's worth of events |
| // and the code that calls this will only use the ones that matter |
| // for the current week. |
| byday = r.byday; |
| bydayNum = r.bydayNum; |
| for (i=0; i<count; i++) { |
| v = bydayNum[i]; |
| j = EventRecurrence.day2TimeDay(byday[i]) - first + 1; |
| if (j <= 0) { |
| j += 7; |
| } |
| if (v == 0) { |
| // v is 0, each day in the month/week |
| for (; j<=lastDayThisMonth; j+=7) { |
| if (SPEW) Log.i(TAG, "setting " + j + " for rule " |
| + v + "/" + EventRecurrence.day2TimeDay(byday[i])); |
| days |= 1 << j; |
| } |
| } |
| else if (v > 0) { |
| // v is positive, count from the beginning of the month |
| // -1 b/c the first one should add 0 |
| j += 7*(v-1); |
| if (j <= lastDayThisMonth) { |
| if (SPEW) Log.i(TAG, "setting " + j + " for rule " |
| + v + "/" + EventRecurrence.day2TimeDay(byday[i])); |
| // if it's impossible, we drop it |
| days |= 1 << j; |
| } |
| } |
| else { |
| // v is negative, count from the end of the month |
| // find the last one |
| for (; j<=lastDayThisMonth; j+=7) { |
| } |
| // v is negative |
| // should do +1 b/c the last one should add 0, but we also |
| // skipped the j -= 7 b/c the loop to find the last one |
| // overshot by one week |
| j += 7*v; |
| if (j >= 1) { |
| if (SPEW) Log.i(TAG, "setting " + j + " for rule " |
| + v + "/" + EventRecurrence.day2TimeDay(byday[i])); |
| days |= 1 << j; |
| } |
| } |
| } |
| } |
| |
| // BYMONTHDAY |
| // Q: What happens if we have BYMONTHDAY and BYDAY? |
| // A: I didn't see it in the spec, so in lieu of that, we'll |
| // intersect the two. That seems reasonable to me. |
| if (r.freq > EventRecurrence.WEEKLY) { |
| count = r.bymonthdayCount; |
| if (count != 0) { |
| bymonthday = r.bymonthday; |
| if (r.bydayCount == 0) { |
| for (i=0; i<count; i++) { |
| v = bymonthday[i]; |
| if (v >= 0) { |
| days |= 1 << v; |
| } else { |
| j = lastDayThisMonth + v + 1; // v is negative |
| if (j >= 1 && j <= lastDayThisMonth) { |
| days |= 1 << j; |
| } |
| } |
| } |
| } else { |
| // This is O(lastDayThisMonth*count), which is really |
| // O(count) with a decent sized constant. |
| for (j=1; j<=lastDayThisMonth; j++) { |
| next_day : { |
| if ((days&(1<<j)) != 0) { |
| for (i=0; i<count; i++) { |
| if (bymonthday[i] == j) { |
| break next_day; |
| } |
| } |
| days &= ~(1<<j); |
| } |
| } |
| } |
| } |
| } |
| } |
| return days; |
| } |
| |
| private EventRecurrence mR; |
| private int mDays; |
| private Time mTime; |
| private int mYear; |
| private int mMonth; |
| } |
| |
| /** |
| * Expands the recurrence within the given range using the given dtstart |
| * value. Returns an array of longs where each element is a date in UTC |
| * milliseconds. The return value is never null. If there are no dates |
| * then an array of length zero is returned. |
| * |
| * @param dtstart a Time object representing the first occurrence |
| * @param recur the recurrence rules, including RRULE, RDATES, EXRULE, and |
| * EXDATES |
| * @param rangeStartMillis the beginning of the range to expand, in UTC |
| * milliseconds |
| * @param rangeEndMillis the non-inclusive end of the range to expand, in |
| * UTC milliseconds; use -1 for the entire range. |
| * @return an array of dates, each date is in UTC milliseconds |
| * @throws DateException |
| * @throws android.util.TimeFormatException if recur cannot be parsed |
| */ |
| public long[] expand(Time dtstart, |
| RecurrenceSet recur, |
| long rangeStartMillis, |
| long rangeEndMillis) throws DateException { |
| String timezone = dtstart.timezone; |
| mIterator.clear(timezone); |
| mGenerated.clear(timezone); |
| |
| // We don't need to clear the mUntil (and it wouldn't do any good to |
| // do so) because the "until" date string is specified in UTC and that |
| // sets the timezone in the mUntil Time object. |
| |
| mIterator.set(rangeStartMillis); |
| long rangeStartDateValue = normDateTimeComparisonValue(mIterator); |
| |
| long rangeEndDateValue; |
| if (rangeEndMillis != -1) { |
| mIterator.set(rangeEndMillis); |
| rangeEndDateValue = normDateTimeComparisonValue(mIterator); |
| } else { |
| rangeEndDateValue = Long.MAX_VALUE; |
| } |
| |
| TreeSet<Long> dtSet = new TreeSet<Long>(); |
| |
| if (recur.rrules != null) { |
| for (EventRecurrence rrule : recur.rrules) { |
| expand(dtstart, rrule, rangeStartDateValue, |
| rangeEndDateValue, true /* add */, dtSet); |
| } |
| } |
| if (recur.rdates != null) { |
| for (long dt : recur.rdates) { |
| // The dates are stored as milliseconds. We need to convert |
| // them to year/month/day values in the local timezone. |
| mIterator.set(dt); |
| long dtvalue = normDateTimeComparisonValue(mIterator); |
| dtSet.add(dtvalue); |
| } |
| } |
| if (recur.exrules != null) { |
| for (EventRecurrence exrule : recur.exrules) { |
| expand(dtstart, exrule, rangeStartDateValue, |
| rangeEndDateValue, false /* remove */, dtSet); |
| } |
| } |
| if (recur.exdates != null) { |
| for (long dt : recur.exdates) { |
| // The dates are stored as milliseconds. We need to convert |
| // them to year/month/day values in the local timezone. |
| mIterator.set(dt); |
| long dtvalue = normDateTimeComparisonValue(mIterator); |
| dtSet.remove(dtvalue); |
| } |
| } |
| if (dtSet.isEmpty()) { |
| // this can happen if the recurrence does not occur within the |
| // expansion window. |
| return new long[0]; |
| } |
| |
| // The values in dtSet are represented in a special form that is useful |
| // for fast comparisons and that is easy to generate from year/month/day |
| // values. We need to convert these to UTC milliseconds and also to |
| // ensure that the dates are valid. |
| int len = dtSet.size(); |
| long[] dates = new long[len]; |
| int i = 0; |
| for (Long val: dtSet) { |
| setTimeFromLongValue(mIterator, val); |
| dates[i++] = mIterator.toMillis(true /* ignore isDst */); |
| } |
| return dates; |
| } |
| |
| /** |
| * Run the recurrence algorithm. Processes events defined in the local |
| * timezone of the event. Return a list of iCalendar DATETIME |
| * strings containing the start date/times of the occurrences; the output |
| * times are defined in the local timezone of the event. |
| * |
| * If you want all of the events, pass Long.MAX_VALUE for rangeEndDateValue. If you pass |
| * Long.MAX_VALUE for rangeEnd, and the event doesn't have a COUNT or UNTIL field, |
| * you'll get a DateException. |
| * |
| * @param dtstart the dtstart date as defined in RFC2445. This |
| * {@link Time} should be in the timezone of the event. |
| * @param r the parsed recurrence, as defiend in RFC2445 |
| * @param rangeStartDateValue the first date-time you care about, inclusive |
| * @param rangeEndDateValue the last date-time you care about, not inclusive (so |
| * if you care about everything up through and including |
| * Dec 22 1995, set last to Dec 23, 1995 00:00:00 |
| * @param add Whether or not we should add to out, or remove from out. |
| * @param out the TreeSet you'd like to fill with the events |
| * @throws DateException |
| * @throws android.util.TimeFormatException if r cannot be parsed. |
| */ |
| public void expand(Time dtstart, |
| EventRecurrence r, |
| long rangeStartDateValue, |
| long rangeEndDateValue, |
| boolean add, |
| TreeSet<Long> out) throws DateException { |
| unsafeNormalize(dtstart); |
| long dtstartDateValue = normDateTimeComparisonValue(dtstart); |
| int count = 0; |
| |
| // add the dtstart instance to the recurrence, if within range. |
| // For example, if dtstart is Mar 1, 2010 and the range is Jan 1 - Apr 1, |
| // then add it here and increment count. If the range is earlier or later, |
| // then don't add it here. In that case, count will be incremented later |
| // inside the loop. It is important that count gets incremented exactly |
| // once here or in the loop for dtstart. |
| // |
| // NOTE: if DTSTART is not synchronized with the recurrence rule, the first instance |
| // we return will not fit the RRULE pattern. |
| if (add && dtstartDateValue >= rangeStartDateValue |
| && dtstartDateValue < rangeEndDateValue) { |
| out.add(dtstartDateValue); |
| ++count; |
| } |
| |
| Time iterator = mIterator; |
| Time until = mUntil; |
| StringBuilder sb = mStringBuilder; |
| Time generated = mGenerated; |
| DaySet days = mDays; |
| |
| try { |
| |
| days.setRecurrence(r); |
| if (rangeEndDateValue == Long.MAX_VALUE && r.until == null && r.count == 0) { |
| throw new DateException( |
| "No range end provided for a recurrence that has no UNTIL or COUNT."); |
| } |
| |
| // the top-level frequency |
| int freqField; |
| int freqAmount = r.interval; |
| int freq = r.freq; |
| switch (freq) |
| { |
| case EventRecurrence.SECONDLY: |
| freqField = Time.SECOND; |
| break; |
| case EventRecurrence.MINUTELY: |
| freqField = Time.MINUTE; |
| break; |
| case EventRecurrence.HOURLY: |
| freqField = Time.HOUR; |
| break; |
| case EventRecurrence.DAILY: |
| freqField = Time.MONTH_DAY; |
| break; |
| case EventRecurrence.WEEKLY: |
| freqField = Time.MONTH_DAY; |
| freqAmount = 7 * r.interval; |
| if (freqAmount <= 0) { |
| freqAmount = 7; |
| } |
| break; |
| case EventRecurrence.MONTHLY: |
| freqField = Time.MONTH; |
| break; |
| case EventRecurrence.YEARLY: |
| freqField = Time.YEAR; |
| break; |
| default: |
| throw new DateException("bad freq=" + freq); |
| } |
| if (freqAmount <= 0) { |
| freqAmount = 1; |
| } |
| |
| int bymonthCount = r.bymonthCount; |
| boolean usebymonth = useBYX(freq, EventRecurrence.MONTHLY, bymonthCount); |
| boolean useDays = freq >= EventRecurrence.WEEKLY && |
| (r.bydayCount > 0 || r.bymonthdayCount > 0); |
| int byhourCount = r.byhourCount; |
| boolean usebyhour = useBYX(freq, EventRecurrence.HOURLY, byhourCount); |
| int byminuteCount = r.byminuteCount; |
| boolean usebyminute = useBYX(freq, EventRecurrence.MINUTELY, byminuteCount); |
| int bysecondCount = r.bysecondCount; |
| boolean usebysecond = useBYX(freq, EventRecurrence.SECONDLY, bysecondCount); |
| |
| // initialize the iterator |
| iterator.set(dtstart); |
| if (freqField == Time.MONTH) { |
| if (useDays) { |
| // if it's monthly, and we're going to be generating |
| // days, set the iterator day field to 1 because sometimes |
| // we'll skip months if it's greater than 28. |
| // XXX Do we generate days for MONTHLY w/ BYHOUR? If so, |
| // we need to do this then too. |
| iterator.monthDay = 1; |
| } |
| } |
| |
| long untilDateValue; |
| if (r.until != null) { |
| // Ensure that the "until" date string is specified in UTC. |
| String untilStr = r.until; |
| // 15 is length of date-time without trailing Z e.g. "20090204T075959" |
| // A string such as 20090204 is a valid UNTIL (see RFC 2445) and the |
| // Z should not be added. |
| if (untilStr.length() == 15) { |
| untilStr = untilStr + 'Z'; |
| } |
| // The parse() method will set the timezone to UTC |
| until.parse(untilStr); |
| |
| // We need the "until" year/month/day values to be in the same |
| // timezone as all the generated dates so that we can compare them |
| // using the values returned by normDateTimeComparisonValue(). |
| until.switchTimezone(dtstart.timezone); |
| untilDateValue = normDateTimeComparisonValue(until); |
| } else { |
| untilDateValue = Long.MAX_VALUE; |
| } |
| |
| sb.ensureCapacity(15); |
| sb.setLength(15); // TODO: pay attention to whether or not the event |
| // is an all-day one. |
| |
| if (SPEW) { |
| Log.i(TAG, "expand called w/ rangeStart=" + rangeStartDateValue |
| + " rangeEnd=" + rangeEndDateValue); |
| } |
| |
| // go until the end of the range or we're done with this event |
| boolean eventEnded = false; |
| int failsafe = 0; // Avoid infinite loops |
| events: { |
| while (true) { |
| int monthIndex = 0; |
| if (failsafe++ > MAX_ALLOWED_ITERATIONS) { // Give up after about 1 second of processing |
| Log.w(TAG, "Recurrence processing stuck with r=" + r + " rangeStart=" |
| + rangeStartDateValue + " rangeEnd=" + rangeEndDateValue); |
| break; |
| } |
| |
| unsafeNormalize(iterator); |
| |
| int iteratorYear = iterator.year; |
| int iteratorMonth = iterator.month + 1; |
| int iteratorDay = iterator.monthDay; |
| int iteratorHour = iterator.hour; |
| int iteratorMinute = iterator.minute; |
| int iteratorSecond = iterator.second; |
| |
| // year is never expanded -- there is no BYYEAR |
| generated.set(iterator); |
| |
| if (SPEW) Log.i(TAG, "year=" + generated.year); |
| |
| do { // month |
| int month = usebymonth |
| ? r.bymonth[monthIndex] |
| : iteratorMonth; |
| month--; |
| if (SPEW) Log.i(TAG, " month=" + month); |
| |
| int dayIndex = 1; |
| int lastDayToExamine = 0; |
| |
| // Use this to handle weeks that overlap the end of the month. |
| // Keep the year and month that days is for, and generate it |
| // when needed in the loop |
| if (useDays) { |
| // Determine where to start and end, don't worry if this happens |
| // to be before dtstart or after the end, because that will be |
| // filtered in the inner loop |
| if (freq == EventRecurrence.WEEKLY) { |
| /* |
| * iterator.weekDay indicates the day of the week (0-6, SU-SA). |
| * Because dayIndex might start in the middle of a week, and we're |
| * interested in treating a week as a unit, we want to move |
| * backward to the start of the week. (This could make the |
| * dayIndex negative, which will be corrected by normalization |
| * later on.) |
| * |
| * The day that starts the week is determined by WKST, which |
| * defaults to MO. |
| * |
| * Example: dayIndex is Tuesday the 8th, and weeks start on |
| * Thursdays. Tuesday is day 2, Thursday is day 4, so we |
| * want to move back (2 - 4 + 7) % 7 = 5 days to the previous |
| * Thursday. If weeks started on Mondays, we would only |
| * need to move back (2 - 1 + 7) % 7 = 1 day. |
| */ |
| int weekStartAdj = (iterator.weekDay - |
| EventRecurrence.day2TimeDay(r.wkst) + 7) % 7; |
| dayIndex = iterator.monthDay - weekStartAdj; |
| lastDayToExamine = dayIndex + 6; |
| } else { |
| lastDayToExamine = generated |
| .getActualMaximum(Time.MONTH_DAY); |
| } |
| if (SPEW) Log.i(TAG, "dayIndex=" + dayIndex |
| + " lastDayToExamine=" + lastDayToExamine |
| + " days=" + days); |
| } |
| |
| do { // day |
| int day; |
| if (useDays) { |
| if (!days.get(iterator, dayIndex)) { |
| dayIndex++; |
| continue; |
| } else { |
| day = dayIndex; |
| } |
| } else { |
| day = iteratorDay; |
| } |
| if (SPEW) Log.i(TAG, " day=" + day); |
| |
| // hour |
| int hourIndex = 0; |
| do { |
| int hour = usebyhour |
| ? r.byhour[hourIndex] |
| : iteratorHour; |
| if (SPEW) Log.i(TAG, " hour=" + hour + " usebyhour=" + usebyhour); |
| |
| // minute |
| int minuteIndex = 0; |
| do { |
| int minute = usebyminute |
| ? r.byminute[minuteIndex] |
| : iteratorMinute; |
| if (SPEW) Log.i(TAG, " minute=" + minute); |
| |
| // second |
| int secondIndex = 0; |
| do { |
| int second = usebysecond |
| ? r.bysecond[secondIndex] |
| : iteratorSecond; |
| if (SPEW) Log.i(TAG, " second=" + second); |
| |
| // we do this here each time, because if we distribute it, we find the |
| // month advancing extra times, as we set the month to the 32nd, 33rd, etc. |
| // days. |
| generated.set(second, minute, hour, day, month, iteratorYear); |
| unsafeNormalize(generated); |
| |
| long genDateValue = normDateTimeComparisonValue(generated); |
| // sometimes events get generated (BYDAY, BYHOUR, etc.) that |
| // are before dtstart. Filter these. I believe this is correct, |
| // but Google Calendar doesn't seem to always do this. |
| if (genDateValue >= dtstartDateValue) { |
| // filter and then add |
| // TODO: we don't check for stop conditions (like |
| // passing the "end" date) unless the filter |
| // allows the event. Could stop sooner. |
| int filtered = filter(r, generated); |
| if (0 == filtered) { |
| |
| // increase the count as long |
| // as this isn't the same |
| // as the first instance |
| // specified by the DTSTART |
| // (for RRULEs -- additive). |
| // This condition must be the complement of the |
| // condition for incrementing count at the |
| // beginning of the method, so if we don't |
| // increment count there, we increment it here. |
| // For example, if add is set and dtstartDateValue |
| // is inside the start/end range, then it was added |
| // and count was incremented at the beginning. |
| // If dtstartDateValue is outside the range or add |
| // is not set, then we must increment count here. |
| if (!(dtstartDateValue == genDateValue |
| && add |
| && dtstartDateValue >= rangeStartDateValue |
| && dtstartDateValue < rangeEndDateValue)) { |
| ++count; |
| } |
| // one reason we can stop is that |
| // we're past the until date |
| if (genDateValue > untilDateValue) { |
| if (SPEW) { |
| Log.i(TAG, "stopping b/c until=" |
| + untilDateValue |
| + " generated=" |
| + genDateValue); |
| } |
| break events; |
| } |
| // or we're past rangeEnd |
| if (genDateValue >= rangeEndDateValue) { |
| if (SPEW) { |
| Log.i(TAG, "stopping b/c rangeEnd=" |
| + rangeEndDateValue |
| + " generated=" + generated); |
| } |
| break events; |
| } |
| |
| if (genDateValue >= rangeStartDateValue) { |
| if (SPEW) { |
| Log.i(TAG, "adding date=" + generated + " filtered=" + filtered); |
| } |
| if (add) { |
| out.add(genDateValue); |
| } else { |
| out.remove(genDateValue); |
| } |
| } |
| // another is that count is high enough |
| if (r.count > 0 && r.count == count) { |
| //Log.i(TAG, "stopping b/c count=" + count); |
| break events; |
| } |
| } |
| } |
| secondIndex++; |
| } while (usebysecond && secondIndex < bysecondCount); |
| minuteIndex++; |
| } while (usebyminute && minuteIndex < byminuteCount); |
| hourIndex++; |
| } while (usebyhour && hourIndex < byhourCount); |
| dayIndex++; |
| } while (useDays && dayIndex <= lastDayToExamine); |
| monthIndex++; |
| } while (usebymonth && monthIndex < bymonthCount); |
| |
| // Add freqAmount to freqField until we get another date that we want. |
| // We don't want to "generate" dates with the iterator. |
| // XXX: We do this for days, because there is a varying number of days |
| // per month |
| int oldDay = iterator.monthDay; |
| generated.set(iterator); // just using generated as a temporary. |
| int n = 1; |
| while (true) { |
| int value = freqAmount * n; |
| switch (freqField) { |
| case Time.SECOND: |
| iterator.second += value; |
| break; |
| case Time.MINUTE: |
| iterator.minute += value; |
| break; |
| case Time.HOUR: |
| iterator.hour += value; |
| break; |
| case Time.MONTH_DAY: |
| iterator.monthDay += value; |
| break; |
| case Time.MONTH: |
| iterator.month += value; |
| break; |
| case Time.YEAR: |
| iterator.year += value; |
| break; |
| case Time.WEEK_DAY: |
| iterator.monthDay += value; |
| break; |
| case Time.YEAR_DAY: |
| iterator.monthDay += value; |
| break; |
| default: |
| throw new RuntimeException("bad field=" + freqField); |
| } |
| |
| unsafeNormalize(iterator); |
| if (freqField != Time.YEAR && freqField != Time.MONTH) { |
| break; |
| } |
| if (iterator.monthDay == oldDay) { |
| break; |
| } |
| n++; |
| iterator.set(generated); |
| } |
| } |
| } |
| } |
| catch (DateException e) { |
| Log.w(TAG, "DateException with r=" + r + " rangeStart=" + rangeStartDateValue |
| + " rangeEnd=" + rangeEndDateValue); |
| throw e; |
| } |
| catch (RuntimeException t) { |
| Log.w(TAG, "RuntimeException with r=" + r + " rangeStart=" + rangeStartDateValue |
| + " rangeEnd=" + rangeEndDateValue); |
| throw t; |
| } |
| } |
| |
| /** |
| * Normalizes the date fields to give a valid date, but if the time falls |
| * in the invalid window during a transition out of Daylight Saving Time |
| * when time jumps forward an hour, then the "normalized" value will be |
| * invalid. |
| * <p> |
| * This method also computes the weekDay and yearDay fields. |
| * |
| * <p> |
| * This method does not modify the fields isDst, or gmtOff. |
| */ |
| static void unsafeNormalize(Time date) { |
| int second = date.second; |
| int minute = date.minute; |
| int hour = date.hour; |
| int monthDay = date.monthDay; |
| int month = date.month; |
| int year = date.year; |
| |
| int addMinutes = ((second < 0) ? (second - 59) : second) / 60; |
| second -= addMinutes * 60; |
| minute += addMinutes; |
| int addHours = ((minute < 0) ? (minute - 59) : minute) / 60; |
| minute -= addHours * 60; |
| hour += addHours; |
| int addDays = ((hour < 0) ? (hour - 23) : hour) / 24; |
| hour -= addDays * 24; |
| monthDay += addDays; |
| |
| // We want to make "monthDay" positive. We do this by subtracting one |
| // from the year and adding a year's worth of days to "monthDay" in |
| // the following loop while "monthDay" <= 0. |
| while (monthDay <= 0) { |
| // If month is after Feb, then add this year's length so that we |
| // include this year's leap day, if any. |
| // Otherwise (the month is Feb or earlier), add last year's length. |
| // Subtract one from the year in either case. This gives the same |
| // effective date but makes monthDay (the day of the month) much |
| // larger. Eventually (usually in one iteration) monthDay will |
| // be positive. |
| int days = month > 1 ? yearLength(year) : yearLength(year - 1); |
| monthDay += days; |
| year -= 1; |
| } |
| // At this point, monthDay >= 1. Normalize the month to the range [0,11]. |
| if (month < 0) { |
| int years = (month + 1) / 12 - 1; |
| year += years; |
| month -= 12 * years; |
| } else if (month >= 12) { |
| int years = month / 12; |
| year += years; |
| month -= 12 * years; |
| } |
| // At this point, month is in the range [0,11] and monthDay >= 1. |
| // Now loop until the monthDay is in the correct range for the month. |
| while (true) { |
| // On January, check if we can jump forward a whole year. |
| if (month == 0) { |
| int yearLength = yearLength(year); |
| if (monthDay > yearLength) { |
| year++; |
| monthDay -= yearLength; |
| } |
| } |
| int monthLength = monthLength(year, month); |
| if (monthDay > monthLength) { |
| monthDay -= monthLength; |
| month++; |
| if (month >= 12) { |
| month -= 12; |
| year++; |
| } |
| } else break; |
| } |
| // At this point, monthDay <= the length of the current month and is |
| // in the range [1,31]. |
| |
| date.second = second; |
| date.minute = minute; |
| date.hour = hour; |
| date.monthDay = monthDay; |
| date.month = month; |
| date.year = year; |
| date.weekDay = weekDay(year, month, monthDay); |
| date.yearDay = yearDay(year, month, monthDay); |
| } |
| |
| /** |
| * Returns true if the given year is a leap year. |
| * |
| * @param year the given year to test |
| * @return true if the given year is a leap year. |
| */ |
| static boolean isLeapYear(int year) { |
| return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); |
| } |
| |
| /** |
| * Returns the number of days in the given year. |
| * |
| * @param year the given year |
| * @return the number of days in the given year. |
| */ |
| static int yearLength(int year) { |
| return isLeapYear(year) ? 366 : 365; |
| } |
| |
| private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31, |
| 31, 30, 31, 30, 31 }; |
| private static final int[] DAYS_IN_YEAR_PRECEDING_MONTH = { 0, 31, 59, 90, |
| 120, 151, 180, 212, 243, 273, 304, 334 }; |
| |
| /** |
| * Returns the number of days in the given month of the given year. |
| * |
| * @param year the given year. |
| * @param month the given month in the range [0,11] |
| * @return the number of days in the given month of the given year. |
| */ |
| static int monthLength(int year, int month) { |
| int n = DAYS_PER_MONTH[month]; |
| if (n != 28) { |
| return n; |
| } |
| return isLeapYear(year) ? 29 : 28; |
| } |
| |
| /** |
| * Computes the weekday, a number in the range [0,6] where Sunday=0, from |
| * the given year, month, and day. |
| * |
| * @param year the year |
| * @param month the 0-based month in the range [0,11] |
| * @param day the 1-based day of the month in the range [1,31] |
| * @return the weekday, a number in the range [0,6] where Sunday=0 |
| */ |
| static int weekDay(int year, int month, int day) { |
| if (month <= 1) { |
| month += 12; |
| year -= 1; |
| } |
| return (day + (13 * month - 14) / 5 + year + year/4 - year/100 + year/400) % 7; |
| } |
| |
| /** |
| * Computes the 0-based "year day", given the year, month, and day. |
| * |
| * @param year the year |
| * @param month the 0-based month in the range [0,11] |
| * @param day the 1-based day in the range [1,31] |
| * @return the 0-based "year day", the number of days into the year |
| */ |
| static int yearDay(int year, int month, int day) { |
| int yearDay = DAYS_IN_YEAR_PRECEDING_MONTH[month] + day - 1; |
| if (month >= 2 && isLeapYear(year)) { |
| yearDay += 1; |
| } |
| return yearDay; |
| } |
| |
| /** |
| * Converts a normalized Time value to a 64-bit long. The mapping of Time |
| * values to longs provides a total ordering on the Time values so that |
| * two Time values can be compared efficiently by comparing their 64-bit |
| * long values. This is faster than converting the Time values to UTC |
| * millliseconds. |
| * |
| * @param normalized a Time object whose date and time fields have been |
| * normalized |
| * @return a 64-bit long value that can be used for comparing and ordering |
| * dates and times represented by Time objects |
| */ |
| private static final long normDateTimeComparisonValue(Time normalized) { |
| // 37 bits for the year, 4 bits for the month, 5 bits for the monthDay, |
| // 5 bits for the hour, 6 bits for the minute, 6 bits for the second. |
| return ((long)normalized.year << 26) + (normalized.month << 22) |
| + (normalized.monthDay << 17) + (normalized.hour << 12) |
| + (normalized.minute << 6) + normalized.second; |
| } |
| |
| private static final void setTimeFromLongValue(Time date, long val) { |
| date.year = (int) (val >> 26); |
| date.month = (int) (val >> 22) & 0xf; |
| date.monthDay = (int) (val >> 17) & 0x1f; |
| date.hour = (int) (val >> 12) & 0x1f; |
| date.minute = (int) (val >> 6) & 0x3f; |
| date.second = (int) (val & 0x3f); |
| } |
| } |