blob: fdd078311316f13c72976299422e0f9af849a606 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.pim;
18
19import android.content.ContentValues;
20import android.database.Cursor;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.provider.Calendar;
22import android.text.TextUtils;
23import android.text.format.Time;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.util.Log;
25
26import java.util.List;
Fabrice Di Meglio66c5bd92010-02-23 11:50:21 -080027import java.util.regex.Pattern;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028
29/**
30 * Basic information about a recurrence, following RFC 2445 Section 4.8.5.
31 * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties.
32 */
33public class RecurrenceSet {
34
35 private final static String TAG = "CalendarProvider";
36
37 private final static String RULE_SEPARATOR = "\n";
Fabrice Di Meglio66c5bd92010-02-23 11:50:21 -080038 private final static String FOLDING_SEPARATOR = "\n ";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039
40 // TODO: make these final?
41 public EventRecurrence[] rrules = null;
42 public long[] rdates = null;
43 public EventRecurrence[] exrules = null;
44 public long[] exdates = null;
45
46 /**
47 * Creates a new RecurrenceSet from information stored in the
48 * events table in the CalendarProvider.
49 * @param values The values retrieved from the Events table.
50 */
Fabrice Di Meglio24b5bdd2010-02-16 18:18:18 -080051 public RecurrenceSet(ContentValues values)
52 throws EventRecurrence.InvalidFormatException {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053 String rruleStr = values.getAsString(Calendar.Events.RRULE);
54 String rdateStr = values.getAsString(Calendar.Events.RDATE);
55 String exruleStr = values.getAsString(Calendar.Events.EXRULE);
56 String exdateStr = values.getAsString(Calendar.Events.EXDATE);
57 init(rruleStr, rdateStr, exruleStr, exdateStr);
58 }
59
60 /**
61 * Creates a new RecurrenceSet from information stored in a database
62 * {@link Cursor} pointing to the events table in the
63 * CalendarProvider. The cursor must contain the RRULE, RDATE, EXRULE,
64 * and EXDATE columns.
65 *
66 * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE
67 * columns.
68 */
Fabrice Di Meglio24b5bdd2010-02-16 18:18:18 -080069 public RecurrenceSet(Cursor cursor)
70 throws EventRecurrence.InvalidFormatException {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071 int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE);
72 int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE);
73 int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE);
74 int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE);
75 String rruleStr = cursor.getString(rruleColumn);
76 String rdateStr = cursor.getString(rdateColumn);
77 String exruleStr = cursor.getString(exruleColumn);
78 String exdateStr = cursor.getString(exdateColumn);
79 init(rruleStr, rdateStr, exruleStr, exdateStr);
80 }
81
82 public RecurrenceSet(String rruleStr, String rdateStr,
Fabrice Di Meglio24b5bdd2010-02-16 18:18:18 -080083 String exruleStr, String exdateStr)
84 throws EventRecurrence.InvalidFormatException {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085 init(rruleStr, rdateStr, exruleStr, exdateStr);
86 }
87
88 private void init(String rruleStr, String rdateStr,
Fabrice Di Meglio24b5bdd2010-02-16 18:18:18 -080089 String exruleStr, String exdateStr)
90 throws EventRecurrence.InvalidFormatException {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080091 if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) {
92
93 if (!TextUtils.isEmpty(rruleStr)) {
94 String[] rruleStrs = rruleStr.split(RULE_SEPARATOR);
95 rrules = new EventRecurrence[rruleStrs.length];
96 for (int i = 0; i < rruleStrs.length; ++i) {
97 EventRecurrence rrule = new EventRecurrence();
98 rrule.parse(rruleStrs[i]);
99 rrules[i] = rrule;
100 }
101 }
102
103 if (!TextUtils.isEmpty(rdateStr)) {
104 rdates = parseRecurrenceDates(rdateStr);
105 }
106
107 if (!TextUtils.isEmpty(exruleStr)) {
108 String[] exruleStrs = exruleStr.split(RULE_SEPARATOR);
109 exrules = new EventRecurrence[exruleStrs.length];
110 for (int i = 0; i < exruleStrs.length; ++i) {
111 EventRecurrence exrule = new EventRecurrence();
112 exrule.parse(exruleStr);
113 exrules[i] = exrule;
114 }
115 }
116
117 if (!TextUtils.isEmpty(exdateStr)) {
118 exdates = parseRecurrenceDates(exdateStr);
119 }
120 }
121 }
122
123 /**
124 * Returns whether or not a recurrence is defined in this RecurrenceSet.
125 * @return Whether or not a recurrence is defined in this RecurrenceSet.
126 */
127 public boolean hasRecurrence() {
128 return (rrules != null || rdates != null);
129 }
130
131 /**
132 * Parses the provided RDATE or EXDATE string into an array of longs
133 * representing each date/time in the recurrence.
134 * @param recurrence The recurrence to be parsed.
135 * @return The list of date/times.
136 */
137 public static long[] parseRecurrenceDates(String recurrence) {
138 // TODO: use "local" time as the default. will need to handle times
139 // that end in "z" (UTC time) explicitly at that point.
140 String tz = Time.TIMEZONE_UTC;
141 int tzidx = recurrence.indexOf(";");
142 if (tzidx != -1) {
143 tz = recurrence.substring(0, tzidx);
144 recurrence = recurrence.substring(tzidx + 1);
145 }
146 Time time = new Time(tz);
147 String[] rawDates = recurrence.split(",");
148 int n = rawDates.length;
149 long[] dates = new long[n];
150 for (int i = 0; i<n; ++i) {
151 // The timezone is updated to UTC if the time string specified 'Z'.
152 time.parse(rawDates[i]);
153 dates[i] = time.toMillis(false /* use isDst */);
154 time.timezone = tz;
155 }
156 return dates;
157 }
158
159 /**
160 * Populates the database map of values with the appropriate RRULE, RDATE,
161 * EXRULE, and EXDATE values extracted from the parsed iCalendar component.
162 * @param component The iCalendar component containing the desired
163 * recurrence specification.
164 * @param values The db values that should be updated.
165 * @return true if the component contained the necessary information
166 * to specify a recurrence. The required fields are DTSTART,
167 * one of DTEND/DURATION, and one of RRULE/RDATE. Returns false if
168 * there was an error, including if the date is out of range.
169 */
170 public static boolean populateContentValues(ICalendar.Component component,
171 ContentValues values) {
172 ICalendar.Property dtstartProperty =
173 component.getFirstProperty("DTSTART");
174 String dtstart = dtstartProperty.getValue();
175 ICalendar.Parameter tzidParam =
176 dtstartProperty.getFirstParameter("TZID");
177 // NOTE: the timezone may be null, if this is a floating time.
178 String tzid = tzidParam == null ? null : tzidParam.value;
179 Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid);
180 boolean inUtc = start.parse(dtstart);
181 boolean allDay = start.allDay;
182
Fabrice Di Meglio9d8d1e52010-04-12 11:14:26 -0700183 // We force TimeZone to UTC for "all day recurring events" as the server is sending no
184 // TimeZone in DTSTART for them
185 if (inUtc || allDay) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800186 tzid = Time.TIMEZONE_UTC;
187 }
188
189 String duration = computeDuration(start, component);
190 String rrule = flattenProperties(component, "RRULE");
191 String rdate = extractDates(component.getFirstProperty("RDATE"));
192 String exrule = flattenProperties(component, "EXRULE");
193 String exdate = extractDates(component.getFirstProperty("EXDATE"));
194
195 if ((TextUtils.isEmpty(dtstart))||
196 (TextUtils.isEmpty(duration))||
197 ((TextUtils.isEmpty(rrule))&&
198 (TextUtils.isEmpty(rdate)))) {
Joe Onorato43a17652011-04-06 19:22:23 -0700199 if (false) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800200 Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, "
201 + "or RRULE/RDATE: "
202 + component.toString());
203 }
204 return false;
205 }
206
207 if (allDay) {
Fabrice Di Meglio9d8d1e52010-04-12 11:14:26 -0700208 start.timezone = Time.TIMEZONE_UTC;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800209 }
210 long millis = start.toMillis(false /* use isDst */);
211 values.put(Calendar.Events.DTSTART, millis);
212 if (millis == -1) {
Joe Onorato43a17652011-04-06 19:22:23 -0700213 if (false) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800214 Log.d(TAG, "DTSTART is out of range: " + component.toString());
215 }
216 return false;
217 }
218
219 values.put(Calendar.Events.RRULE, rrule);
220 values.put(Calendar.Events.RDATE, rdate);
221 values.put(Calendar.Events.EXRULE, exrule);
222 values.put(Calendar.Events.EXDATE, exdate);
223 values.put(Calendar.Events.EVENT_TIMEZONE, tzid);
224 values.put(Calendar.Events.DURATION, duration);
225 values.put(Calendar.Events.ALL_DAY, allDay ? 1 : 0);
226 return true;
227 }
228
Ken Shirriff3b95f532009-07-06 10:45:38 -0700229 // This can be removed when the old CalendarSyncAdapter is removed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800230 public static boolean populateComponent(Cursor cursor,
231 ICalendar.Component component) {
232
233 int dtstartColumn = cursor.getColumnIndex(Calendar.Events.DTSTART);
234 int durationColumn = cursor.getColumnIndex(Calendar.Events.DURATION);
235 int tzidColumn = cursor.getColumnIndex(Calendar.Events.EVENT_TIMEZONE);
236 int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE);
237 int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE);
238 int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE);
239 int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE);
240 int allDayColumn = cursor.getColumnIndex(Calendar.Events.ALL_DAY);
241
242
243 long dtstart = -1;
244 if (!cursor.isNull(dtstartColumn)) {
245 dtstart = cursor.getLong(dtstartColumn);
246 }
247 String duration = cursor.getString(durationColumn);
248 String tzid = cursor.getString(tzidColumn);
249 String rruleStr = cursor.getString(rruleColumn);
250 String rdateStr = cursor.getString(rdateColumn);
251 String exruleStr = cursor.getString(exruleColumn);
252 String exdateStr = cursor.getString(exdateColumn);
253 boolean allDay = cursor.getInt(allDayColumn) == 1;
254
255 if ((dtstart == -1) ||
256 (TextUtils.isEmpty(duration))||
257 ((TextUtils.isEmpty(rruleStr))&&
258 (TextUtils.isEmpty(rdateStr)))) {
259 // no recurrence.
260 return false;
261 }
262
263 ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
264 Time dtstartTime = null;
265 if (!TextUtils.isEmpty(tzid)) {
266 if (!allDay) {
267 dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
268 }
269 dtstartTime = new Time(tzid);
270 } else {
271 // use the "floating" timezone
272 dtstartTime = new Time(Time.TIMEZONE_UTC);
273 }
274
275 dtstartTime.set(dtstart);
276 // make sure the time is printed just as a date, if all day.
277 // TODO: android.pim.Time really should take care of this for us.
278 if (allDay) {
279 dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
280 dtstartTime.allDay = true;
281 dtstartTime.hour = 0;
282 dtstartTime.minute = 0;
283 dtstartTime.second = 0;
284 }
285
286 dtstartProp.setValue(dtstartTime.format2445());
287 component.addProperty(dtstartProp);
288 ICalendar.Property durationProp = new ICalendar.Property("DURATION");
289 durationProp.setValue(duration);
290 component.addProperty(durationProp);
291
292 addPropertiesForRuleStr(component, "RRULE", rruleStr);
293 addPropertyForDateStr(component, "RDATE", rdateStr);
294 addPropertiesForRuleStr(component, "EXRULE", exruleStr);
295 addPropertyForDateStr(component, "EXDATE", exdateStr);
296 return true;
297 }
298
Ken Shirriff3b95f532009-07-06 10:45:38 -0700299public static boolean populateComponent(ContentValues values,
300 ICalendar.Component component) {
301 long dtstart = -1;
302 if (values.containsKey(Calendar.Events.DTSTART)) {
303 dtstart = values.getAsLong(Calendar.Events.DTSTART);
304 }
305 String duration = values.getAsString(Calendar.Events.DURATION);
306 String tzid = values.getAsString(Calendar.Events.EVENT_TIMEZONE);
307 String rruleStr = values.getAsString(Calendar.Events.RRULE);
308 String rdateStr = values.getAsString(Calendar.Events.RDATE);
309 String exruleStr = values.getAsString(Calendar.Events.EXRULE);
310 String exdateStr = values.getAsString(Calendar.Events.EXDATE);
Fabrice Di Meglio66c5bd92010-02-23 11:50:21 -0800311 Integer allDayInteger = values.getAsInteger(Calendar.Events.ALL_DAY);
312 boolean allDay = (null != allDayInteger) ? (allDayInteger == 1) : false;
Ken Shirriff3b95f532009-07-06 10:45:38 -0700313
314 if ((dtstart == -1) ||
315 (TextUtils.isEmpty(duration))||
316 ((TextUtils.isEmpty(rruleStr))&&
317 (TextUtils.isEmpty(rdateStr)))) {
318 // no recurrence.
319 return false;
320 }
321
322 ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
323 Time dtstartTime = null;
324 if (!TextUtils.isEmpty(tzid)) {
325 if (!allDay) {
326 dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
327 }
328 dtstartTime = new Time(tzid);
329 } else {
330 // use the "floating" timezone
331 dtstartTime = new Time(Time.TIMEZONE_UTC);
332 }
333
334 dtstartTime.set(dtstart);
335 // make sure the time is printed just as a date, if all day.
336 // TODO: android.pim.Time really should take care of this for us.
337 if (allDay) {
338 dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
339 dtstartTime.allDay = true;
340 dtstartTime.hour = 0;
341 dtstartTime.minute = 0;
342 dtstartTime.second = 0;
343 }
344
345 dtstartProp.setValue(dtstartTime.format2445());
346 component.addProperty(dtstartProp);
347 ICalendar.Property durationProp = new ICalendar.Property("DURATION");
348 durationProp.setValue(duration);
349 component.addProperty(durationProp);
350
351 addPropertiesForRuleStr(component, "RRULE", rruleStr);
352 addPropertyForDateStr(component, "RDATE", rdateStr);
353 addPropertiesForRuleStr(component, "EXRULE", exruleStr);
354 addPropertyForDateStr(component, "EXDATE", exdateStr);
355 return true;
356 }
357
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800358 private static void addPropertiesForRuleStr(ICalendar.Component component,
359 String propertyName,
360 String ruleStr) {
361 if (TextUtils.isEmpty(ruleStr)) {
362 return;
363 }
Fabrice Di Meglio66c5bd92010-02-23 11:50:21 -0800364 String[] rrules = getRuleStrings(ruleStr);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800365 for (String rrule : rrules) {
366 ICalendar.Property prop = new ICalendar.Property(propertyName);
367 prop.setValue(rrule);
368 component.addProperty(prop);
369 }
370 }
371
Fabrice Di Meglio66c5bd92010-02-23 11:50:21 -0800372 private static String[] getRuleStrings(String ruleStr) {
373 if (null == ruleStr) {
374 return new String[0];
375 }
376 String unfoldedRuleStr = unfold(ruleStr);
377 String[] split = unfoldedRuleStr.split(RULE_SEPARATOR);
378 int count = split.length;
379 for (int n = 0; n < count; n++) {
380 split[n] = fold(split[n]);
381 }
382 return split;
383 }
384
385
386 private static final Pattern IGNORABLE_ICAL_WHITESPACE_RE =
387 Pattern.compile("(?:\\r\\n?|\\n)[ \t]");
388
389 private static final Pattern FOLD_RE = Pattern.compile(".{75}");
390
391 /**
392 * fold and unfolds ical content lines as per RFC 2445 section 4.1.
393 *
394 * <h3>4.1 Content Lines</h3>
395 *
396 * <p>The iCalendar object is organized into individual lines of text, called
397 * content lines. Content lines are delimited by a line break, which is a CRLF
398 * sequence (US-ASCII decimal 13, followed by US-ASCII decimal 10).
399 *
400 * <p>Lines of text SHOULD NOT be longer than 75 octets, excluding the line
401 * break. Long content lines SHOULD be split into a multiple line
402 * representations using a line "folding" technique. That is, a long line can
403 * be split between any two characters by inserting a CRLF immediately
404 * followed by a single linear white space character (i.e., SPACE, US-ASCII
405 * decimal 32 or HTAB, US-ASCII decimal 9). Any sequence of CRLF followed
406 * immediately by a single linear white space character is ignored (i.e.,
407 * removed) when processing the content type.
408 */
409 public static String fold(String unfoldedIcalContent) {
410 return FOLD_RE.matcher(unfoldedIcalContent).replaceAll("$0\r\n ");
411 }
412
413 public static String unfold(String foldedIcalContent) {
414 return IGNORABLE_ICAL_WHITESPACE_RE.matcher(
415 foldedIcalContent).replaceAll("");
416 }
417
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800418 private static void addPropertyForDateStr(ICalendar.Component component,
419 String propertyName,
420 String dateStr) {
421 if (TextUtils.isEmpty(dateStr)) {
422 return;
423 }
424
425 ICalendar.Property prop = new ICalendar.Property(propertyName);
426 String tz = null;
427 int tzidx = dateStr.indexOf(";");
428 if (tzidx != -1) {
429 tz = dateStr.substring(0, tzidx);
430 dateStr = dateStr.substring(tzidx + 1);
431 }
432 if (!TextUtils.isEmpty(tz)) {
433 prop.addParameter(new ICalendar.Parameter("TZID", tz));
434 }
435 prop.setValue(dateStr);
436 component.addProperty(prop);
437 }
438
439 private static String computeDuration(Time start,
440 ICalendar.Component component) {
441 // see if a duration is defined
442 ICalendar.Property durationProperty =
443 component.getFirstProperty("DURATION");
444 if (durationProperty != null) {
445 // just return the duration
446 return durationProperty.getValue();
447 }
448
449 // must compute a duration from the DTEND
450 ICalendar.Property dtendProperty =
451 component.getFirstProperty("DTEND");
452 if (dtendProperty == null) {
453 // no DURATION, no DTEND: 0 second duration
454 return "+P0S";
455 }
456 ICalendar.Parameter endTzidParameter =
457 dtendProperty.getFirstParameter("TZID");
458 String endTzid = (endTzidParameter == null)
459 ? start.timezone : endTzidParameter.value;
460
461 Time end = new Time(endTzid);
462 end.parse(dtendProperty.getValue());
Ken Shirriff1ce2e2e2009-08-21 14:21:59 -0700463 long durationMillis = end.toMillis(false /* use isDst */)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800464 - start.toMillis(false /* use isDst */);
465 long durationSeconds = (durationMillis / 1000);
Ken Shirriff1ce2e2e2009-08-21 14:21:59 -0700466 if (start.allDay && (durationSeconds % 86400) == 0) {
467 return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S
468 } else {
469 return "P" + durationSeconds + "S";
470 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800471 }
472
473 private static String flattenProperties(ICalendar.Component component,
474 String name) {
475 List<ICalendar.Property> properties = component.getProperties(name);
476 if (properties == null || properties.isEmpty()) {
477 return null;
478 }
479
480 if (properties.size() == 1) {
481 return properties.get(0).getValue();
482 }
483
484 StringBuilder sb = new StringBuilder();
485
486 boolean first = true;
487 for (ICalendar.Property property : component.getProperties(name)) {
488 if (first) {
489 first = false;
490 } else {
491 // TODO: use commas. our RECUR parsing should handle that
492 // anyway.
493 sb.append(RULE_SEPARATOR);
494 }
495 sb.append(property.getValue());
496 }
497 return sb.toString();
498 }
499
500 private static String extractDates(ICalendar.Property recurrence) {
501 if (recurrence == null) {
502 return null;
503 }
504 ICalendar.Parameter tzidParam =
505 recurrence.getFirstParameter("TZID");
506 if (tzidParam != null) {
507 return tzidParam.value + ";" + recurrence.getValue();
508 }
509 return recurrence.getValue();
510 }
511}