RecurrenceSet.java revision 3b95f5378957c4e985429dfefda3975416c1a039
1/* 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; 21import android.os.Bundle; 22import android.provider.Calendar; 23import android.text.TextUtils; 24import android.text.format.Time; 25import android.util.Config; 26import android.util.Log; 27 28import java.util.List; 29 30/** 31 * Basic information about a recurrence, following RFC 2445 Section 4.8.5. 32 * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties. 33 */ 34public class RecurrenceSet { 35 36 private final static String TAG = "CalendarProvider"; 37 38 private final static String RULE_SEPARATOR = "\n"; 39 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 */ 51 public RecurrenceSet(ContentValues values) { 52 String rruleStr = values.getAsString(Calendar.Events.RRULE); 53 String rdateStr = values.getAsString(Calendar.Events.RDATE); 54 String exruleStr = values.getAsString(Calendar.Events.EXRULE); 55 String exdateStr = values.getAsString(Calendar.Events.EXDATE); 56 init(rruleStr, rdateStr, exruleStr, exdateStr); 57 } 58 59 /** 60 * Creates a new RecurrenceSet from information stored in a database 61 * {@link Cursor} pointing to the events table in the 62 * CalendarProvider. The cursor must contain the RRULE, RDATE, EXRULE, 63 * and EXDATE columns. 64 * 65 * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE 66 * columns. 67 */ 68 public RecurrenceSet(Cursor cursor) { 69 int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE); 70 int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE); 71 int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE); 72 int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE); 73 String rruleStr = cursor.getString(rruleColumn); 74 String rdateStr = cursor.getString(rdateColumn); 75 String exruleStr = cursor.getString(exruleColumn); 76 String exdateStr = cursor.getString(exdateColumn); 77 init(rruleStr, rdateStr, exruleStr, exdateStr); 78 } 79 80 public RecurrenceSet(String rruleStr, String rdateStr, 81 String exruleStr, String exdateStr) { 82 init(rruleStr, rdateStr, exruleStr, exdateStr); 83 } 84 85 private void init(String rruleStr, String rdateStr, 86 String exruleStr, String exdateStr) { 87 if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) { 88 89 if (!TextUtils.isEmpty(rruleStr)) { 90 String[] rruleStrs = rruleStr.split(RULE_SEPARATOR); 91 rrules = new EventRecurrence[rruleStrs.length]; 92 for (int i = 0; i < rruleStrs.length; ++i) { 93 EventRecurrence rrule = new EventRecurrence(); 94 rrule.parse(rruleStrs[i]); 95 rrules[i] = rrule; 96 } 97 } 98 99 if (!TextUtils.isEmpty(rdateStr)) { 100 rdates = parseRecurrenceDates(rdateStr); 101 } 102 103 if (!TextUtils.isEmpty(exruleStr)) { 104 String[] exruleStrs = exruleStr.split(RULE_SEPARATOR); 105 exrules = new EventRecurrence[exruleStrs.length]; 106 for (int i = 0; i < exruleStrs.length; ++i) { 107 EventRecurrence exrule = new EventRecurrence(); 108 exrule.parse(exruleStr); 109 exrules[i] = exrule; 110 } 111 } 112 113 if (!TextUtils.isEmpty(exdateStr)) { 114 exdates = parseRecurrenceDates(exdateStr); 115 } 116 } 117 } 118 119 /** 120 * Returns whether or not a recurrence is defined in this RecurrenceSet. 121 * @return Whether or not a recurrence is defined in this RecurrenceSet. 122 */ 123 public boolean hasRecurrence() { 124 return (rrules != null || rdates != null); 125 } 126 127 /** 128 * Parses the provided RDATE or EXDATE string into an array of longs 129 * representing each date/time in the recurrence. 130 * @param recurrence The recurrence to be parsed. 131 * @return The list of date/times. 132 */ 133 public static long[] parseRecurrenceDates(String recurrence) { 134 // TODO: use "local" time as the default. will need to handle times 135 // that end in "z" (UTC time) explicitly at that point. 136 String tz = Time.TIMEZONE_UTC; 137 int tzidx = recurrence.indexOf(";"); 138 if (tzidx != -1) { 139 tz = recurrence.substring(0, tzidx); 140 recurrence = recurrence.substring(tzidx + 1); 141 } 142 Time time = new Time(tz); 143 String[] rawDates = recurrence.split(","); 144 int n = rawDates.length; 145 long[] dates = new long[n]; 146 for (int i = 0; i<n; ++i) { 147 // The timezone is updated to UTC if the time string specified 'Z'. 148 time.parse(rawDates[i]); 149 dates[i] = time.toMillis(false /* use isDst */); 150 time.timezone = tz; 151 } 152 return dates; 153 } 154 155 /** 156 * Populates the database map of values with the appropriate RRULE, RDATE, 157 * EXRULE, and EXDATE values extracted from the parsed iCalendar component. 158 * @param component The iCalendar component containing the desired 159 * recurrence specification. 160 * @param values The db values that should be updated. 161 * @return true if the component contained the necessary information 162 * to specify a recurrence. The required fields are DTSTART, 163 * one of DTEND/DURATION, and one of RRULE/RDATE. Returns false if 164 * there was an error, including if the date is out of range. 165 */ 166 public static boolean populateContentValues(ICalendar.Component component, 167 ContentValues values) { 168 ICalendar.Property dtstartProperty = 169 component.getFirstProperty("DTSTART"); 170 String dtstart = dtstartProperty.getValue(); 171 ICalendar.Parameter tzidParam = 172 dtstartProperty.getFirstParameter("TZID"); 173 // NOTE: the timezone may be null, if this is a floating time. 174 String tzid = tzidParam == null ? null : tzidParam.value; 175 Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid); 176 boolean inUtc = start.parse(dtstart); 177 boolean allDay = start.allDay; 178 179 if (inUtc) { 180 tzid = Time.TIMEZONE_UTC; 181 } 182 183 String duration = computeDuration(start, component); 184 String rrule = flattenProperties(component, "RRULE"); 185 String rdate = extractDates(component.getFirstProperty("RDATE")); 186 String exrule = flattenProperties(component, "EXRULE"); 187 String exdate = extractDates(component.getFirstProperty("EXDATE")); 188 189 if ((TextUtils.isEmpty(dtstart))|| 190 (TextUtils.isEmpty(duration))|| 191 ((TextUtils.isEmpty(rrule))&& 192 (TextUtils.isEmpty(rdate)))) { 193 if (Config.LOGD) { 194 Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, " 195 + "or RRULE/RDATE: " 196 + component.toString()); 197 } 198 return false; 199 } 200 201 if (allDay) { 202 // TODO: also change tzid to be UTC? that would be consistent, but 203 // that would not reflect the original timezone value back to the 204 // server. 205 start.timezone = Time.TIMEZONE_UTC; 206 } 207 long millis = start.toMillis(false /* use isDst */); 208 values.put(Calendar.Events.DTSTART, millis); 209 if (millis == -1) { 210 if (Config.LOGD) { 211 Log.d(TAG, "DTSTART is out of range: " + component.toString()); 212 } 213 return false; 214 } 215 216 values.put(Calendar.Events.RRULE, rrule); 217 values.put(Calendar.Events.RDATE, rdate); 218 values.put(Calendar.Events.EXRULE, exrule); 219 values.put(Calendar.Events.EXDATE, exdate); 220 values.put(Calendar.Events.EVENT_TIMEZONE, tzid); 221 values.put(Calendar.Events.DURATION, duration); 222 values.put(Calendar.Events.ALL_DAY, allDay ? 1 : 0); 223 return true; 224 } 225 226 // This can be removed when the old CalendarSyncAdapter is removed. 227 public static boolean populateComponent(Cursor cursor, 228 ICalendar.Component component) { 229 230 int dtstartColumn = cursor.getColumnIndex(Calendar.Events.DTSTART); 231 int durationColumn = cursor.getColumnIndex(Calendar.Events.DURATION); 232 int tzidColumn = cursor.getColumnIndex(Calendar.Events.EVENT_TIMEZONE); 233 int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE); 234 int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE); 235 int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE); 236 int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE); 237 int allDayColumn = cursor.getColumnIndex(Calendar.Events.ALL_DAY); 238 239 240 long dtstart = -1; 241 if (!cursor.isNull(dtstartColumn)) { 242 dtstart = cursor.getLong(dtstartColumn); 243 } 244 String duration = cursor.getString(durationColumn); 245 String tzid = cursor.getString(tzidColumn); 246 String rruleStr = cursor.getString(rruleColumn); 247 String rdateStr = cursor.getString(rdateColumn); 248 String exruleStr = cursor.getString(exruleColumn); 249 String exdateStr = cursor.getString(exdateColumn); 250 boolean allDay = cursor.getInt(allDayColumn) == 1; 251 252 if ((dtstart == -1) || 253 (TextUtils.isEmpty(duration))|| 254 ((TextUtils.isEmpty(rruleStr))&& 255 (TextUtils.isEmpty(rdateStr)))) { 256 // no recurrence. 257 return false; 258 } 259 260 ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); 261 Time dtstartTime = null; 262 if (!TextUtils.isEmpty(tzid)) { 263 if (!allDay) { 264 dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); 265 } 266 dtstartTime = new Time(tzid); 267 } else { 268 // use the "floating" timezone 269 dtstartTime = new Time(Time.TIMEZONE_UTC); 270 } 271 272 dtstartTime.set(dtstart); 273 // make sure the time is printed just as a date, if all day. 274 // TODO: android.pim.Time really should take care of this for us. 275 if (allDay) { 276 dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); 277 dtstartTime.allDay = true; 278 dtstartTime.hour = 0; 279 dtstartTime.minute = 0; 280 dtstartTime.second = 0; 281 } 282 283 dtstartProp.setValue(dtstartTime.format2445()); 284 component.addProperty(dtstartProp); 285 ICalendar.Property durationProp = new ICalendar.Property("DURATION"); 286 durationProp.setValue(duration); 287 component.addProperty(durationProp); 288 289 addPropertiesForRuleStr(component, "RRULE", rruleStr); 290 addPropertyForDateStr(component, "RDATE", rdateStr); 291 addPropertiesForRuleStr(component, "EXRULE", exruleStr); 292 addPropertyForDateStr(component, "EXDATE", exdateStr); 293 return true; 294 } 295 296public static boolean populateComponent(ContentValues values, 297 ICalendar.Component component) { 298 long dtstart = -1; 299 if (values.containsKey(Calendar.Events.DTSTART)) { 300 dtstart = values.getAsLong(Calendar.Events.DTSTART); 301 } 302 String duration = values.getAsString(Calendar.Events.DURATION); 303 String tzid = values.getAsString(Calendar.Events.EVENT_TIMEZONE); 304 String rruleStr = values.getAsString(Calendar.Events.RRULE); 305 String rdateStr = values.getAsString(Calendar.Events.RDATE); 306 String exruleStr = values.getAsString(Calendar.Events.EXRULE); 307 String exdateStr = values.getAsString(Calendar.Events.EXDATE); 308 boolean allDay = values.getAsInteger(Calendar.Events.ALL_DAY) == 1; 309 310 if ((dtstart == -1) || 311 (TextUtils.isEmpty(duration))|| 312 ((TextUtils.isEmpty(rruleStr))&& 313 (TextUtils.isEmpty(rdateStr)))) { 314 // no recurrence. 315 return false; 316 } 317 318 ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); 319 Time dtstartTime = null; 320 if (!TextUtils.isEmpty(tzid)) { 321 if (!allDay) { 322 dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); 323 } 324 dtstartTime = new Time(tzid); 325 } else { 326 // use the "floating" timezone 327 dtstartTime = new Time(Time.TIMEZONE_UTC); 328 } 329 330 dtstartTime.set(dtstart); 331 // make sure the time is printed just as a date, if all day. 332 // TODO: android.pim.Time really should take care of this for us. 333 if (allDay) { 334 dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); 335 dtstartTime.allDay = true; 336 dtstartTime.hour = 0; 337 dtstartTime.minute = 0; 338 dtstartTime.second = 0; 339 } 340 341 dtstartProp.setValue(dtstartTime.format2445()); 342 component.addProperty(dtstartProp); 343 ICalendar.Property durationProp = new ICalendar.Property("DURATION"); 344 durationProp.setValue(duration); 345 component.addProperty(durationProp); 346 347 addPropertiesForRuleStr(component, "RRULE", rruleStr); 348 addPropertyForDateStr(component, "RDATE", rdateStr); 349 addPropertiesForRuleStr(component, "EXRULE", exruleStr); 350 addPropertyForDateStr(component, "EXDATE", exdateStr); 351 return true; 352 } 353 354 private static void addPropertiesForRuleStr(ICalendar.Component component, 355 String propertyName, 356 String ruleStr) { 357 if (TextUtils.isEmpty(ruleStr)) { 358 return; 359 } 360 String[] rrules = ruleStr.split(RULE_SEPARATOR); 361 for (String rrule : rrules) { 362 ICalendar.Property prop = new ICalendar.Property(propertyName); 363 prop.setValue(rrule); 364 component.addProperty(prop); 365 } 366 } 367 368 private static void addPropertyForDateStr(ICalendar.Component component, 369 String propertyName, 370 String dateStr) { 371 if (TextUtils.isEmpty(dateStr)) { 372 return; 373 } 374 375 ICalendar.Property prop = new ICalendar.Property(propertyName); 376 String tz = null; 377 int tzidx = dateStr.indexOf(";"); 378 if (tzidx != -1) { 379 tz = dateStr.substring(0, tzidx); 380 dateStr = dateStr.substring(tzidx + 1); 381 } 382 if (!TextUtils.isEmpty(tz)) { 383 prop.addParameter(new ICalendar.Parameter("TZID", tz)); 384 } 385 prop.setValue(dateStr); 386 component.addProperty(prop); 387 } 388 389 private static String computeDuration(Time start, 390 ICalendar.Component component) { 391 // see if a duration is defined 392 ICalendar.Property durationProperty = 393 component.getFirstProperty("DURATION"); 394 if (durationProperty != null) { 395 // just return the duration 396 return durationProperty.getValue(); 397 } 398 399 // must compute a duration from the DTEND 400 ICalendar.Property dtendProperty = 401 component.getFirstProperty("DTEND"); 402 if (dtendProperty == null) { 403 // no DURATION, no DTEND: 0 second duration 404 return "+P0S"; 405 } 406 ICalendar.Parameter endTzidParameter = 407 dtendProperty.getFirstParameter("TZID"); 408 String endTzid = (endTzidParameter == null) 409 ? start.timezone : endTzidParameter.value; 410 411 Time end = new Time(endTzid); 412 end.parse(dtendProperty.getValue()); 413 long durationMillis = end.toMillis(false /* use isDst */) 414 - start.toMillis(false /* use isDst */); 415 long durationSeconds = (durationMillis / 1000); 416 return "P" + durationSeconds + "S"; 417 } 418 419 private static String flattenProperties(ICalendar.Component component, 420 String name) { 421 List<ICalendar.Property> properties = component.getProperties(name); 422 if (properties == null || properties.isEmpty()) { 423 return null; 424 } 425 426 if (properties.size() == 1) { 427 return properties.get(0).getValue(); 428 } 429 430 StringBuilder sb = new StringBuilder(); 431 432 boolean first = true; 433 for (ICalendar.Property property : component.getProperties(name)) { 434 if (first) { 435 first = false; 436 } else { 437 // TODO: use commas. our RECUR parsing should handle that 438 // anyway. 439 sb.append(RULE_SEPARATOR); 440 } 441 sb.append(property.getValue()); 442 } 443 return sb.toString(); 444 } 445 446 private static String extractDates(ICalendar.Property recurrence) { 447 if (recurrence == null) { 448 return null; 449 } 450 ICalendar.Parameter tzidParam = 451 recurrence.getFirstParameter("TZID"); 452 if (tzidParam != null) { 453 return tzidParam.value + ";" + recurrence.getValue(); 454 } 455 return recurrence.getValue(); 456 } 457} 458