RecurrenceSet.java revision 24b5bdd29e202d107ffaecb66229280253dd33a2
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 throws EventRecurrence.InvalidFormatException { 53 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 */ 69 public RecurrenceSet(Cursor cursor) 70 throws EventRecurrence.InvalidFormatException { 71 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, 83 String exruleStr, String exdateStr) 84 throws EventRecurrence.InvalidFormatException { 85 init(rruleStr, rdateStr, exruleStr, exdateStr); 86 } 87 88 private void init(String rruleStr, String rdateStr, 89 String exruleStr, String exdateStr) 90 throws EventRecurrence.InvalidFormatException { 91 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 183 if (inUtc) { 184 tzid = Time.TIMEZONE_UTC; 185 } 186 187 String duration = computeDuration(start, component); 188 String rrule = flattenProperties(component, "RRULE"); 189 String rdate = extractDates(component.getFirstProperty("RDATE")); 190 String exrule = flattenProperties(component, "EXRULE"); 191 String exdate = extractDates(component.getFirstProperty("EXDATE")); 192 193 if ((TextUtils.isEmpty(dtstart))|| 194 (TextUtils.isEmpty(duration))|| 195 ((TextUtils.isEmpty(rrule))&& 196 (TextUtils.isEmpty(rdate)))) { 197 if (Config.LOGD) { 198 Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, " 199 + "or RRULE/RDATE: " 200 + component.toString()); 201 } 202 return false; 203 } 204 205 if (allDay) { 206 // TODO: also change tzid to be UTC? that would be consistent, but 207 // that would not reflect the original timezone value back to the 208 // server. 209 start.timezone = Time.TIMEZONE_UTC; 210 } 211 long millis = start.toMillis(false /* use isDst */); 212 values.put(Calendar.Events.DTSTART, millis); 213 if (millis == -1) { 214 if (Config.LOGD) { 215 Log.d(TAG, "DTSTART is out of range: " + component.toString()); 216 } 217 return false; 218 } 219 220 values.put(Calendar.Events.RRULE, rrule); 221 values.put(Calendar.Events.RDATE, rdate); 222 values.put(Calendar.Events.EXRULE, exrule); 223 values.put(Calendar.Events.EXDATE, exdate); 224 values.put(Calendar.Events.EVENT_TIMEZONE, tzid); 225 values.put(Calendar.Events.DURATION, duration); 226 values.put(Calendar.Events.ALL_DAY, allDay ? 1 : 0); 227 return true; 228 } 229 230 // This can be removed when the old CalendarSyncAdapter is removed. 231 public static boolean populateComponent(Cursor cursor, 232 ICalendar.Component component) { 233 234 int dtstartColumn = cursor.getColumnIndex(Calendar.Events.DTSTART); 235 int durationColumn = cursor.getColumnIndex(Calendar.Events.DURATION); 236 int tzidColumn = cursor.getColumnIndex(Calendar.Events.EVENT_TIMEZONE); 237 int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE); 238 int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE); 239 int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE); 240 int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE); 241 int allDayColumn = cursor.getColumnIndex(Calendar.Events.ALL_DAY); 242 243 244 long dtstart = -1; 245 if (!cursor.isNull(dtstartColumn)) { 246 dtstart = cursor.getLong(dtstartColumn); 247 } 248 String duration = cursor.getString(durationColumn); 249 String tzid = cursor.getString(tzidColumn); 250 String rruleStr = cursor.getString(rruleColumn); 251 String rdateStr = cursor.getString(rdateColumn); 252 String exruleStr = cursor.getString(exruleColumn); 253 String exdateStr = cursor.getString(exdateColumn); 254 boolean allDay = cursor.getInt(allDayColumn) == 1; 255 256 if ((dtstart == -1) || 257 (TextUtils.isEmpty(duration))|| 258 ((TextUtils.isEmpty(rruleStr))&& 259 (TextUtils.isEmpty(rdateStr)))) { 260 // no recurrence. 261 return false; 262 } 263 264 ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); 265 Time dtstartTime = null; 266 if (!TextUtils.isEmpty(tzid)) { 267 if (!allDay) { 268 dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); 269 } 270 dtstartTime = new Time(tzid); 271 } else { 272 // use the "floating" timezone 273 dtstartTime = new Time(Time.TIMEZONE_UTC); 274 } 275 276 dtstartTime.set(dtstart); 277 // make sure the time is printed just as a date, if all day. 278 // TODO: android.pim.Time really should take care of this for us. 279 if (allDay) { 280 dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); 281 dtstartTime.allDay = true; 282 dtstartTime.hour = 0; 283 dtstartTime.minute = 0; 284 dtstartTime.second = 0; 285 } 286 287 dtstartProp.setValue(dtstartTime.format2445()); 288 component.addProperty(dtstartProp); 289 ICalendar.Property durationProp = new ICalendar.Property("DURATION"); 290 durationProp.setValue(duration); 291 component.addProperty(durationProp); 292 293 addPropertiesForRuleStr(component, "RRULE", rruleStr); 294 addPropertyForDateStr(component, "RDATE", rdateStr); 295 addPropertiesForRuleStr(component, "EXRULE", exruleStr); 296 addPropertyForDateStr(component, "EXDATE", exdateStr); 297 return true; 298 } 299 300public static boolean populateComponent(ContentValues values, 301 ICalendar.Component component) { 302 long dtstart = -1; 303 if (values.containsKey(Calendar.Events.DTSTART)) { 304 dtstart = values.getAsLong(Calendar.Events.DTSTART); 305 } 306 String duration = values.getAsString(Calendar.Events.DURATION); 307 String tzid = values.getAsString(Calendar.Events.EVENT_TIMEZONE); 308 String rruleStr = values.getAsString(Calendar.Events.RRULE); 309 String rdateStr = values.getAsString(Calendar.Events.RDATE); 310 String exruleStr = values.getAsString(Calendar.Events.EXRULE); 311 String exdateStr = values.getAsString(Calendar.Events.EXDATE); 312 boolean allDay = values.getAsInteger(Calendar.Events.ALL_DAY) == 1; 313 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 358 private static void addPropertiesForRuleStr(ICalendar.Component component, 359 String propertyName, 360 String ruleStr) { 361 if (TextUtils.isEmpty(ruleStr)) { 362 return; 363 } 364 String[] rrules = ruleStr.split(RULE_SEPARATOR); 365 for (String rrule : rrules) { 366 ICalendar.Property prop = new ICalendar.Property(propertyName); 367 prop.setValue(rrule); 368 component.addProperty(prop); 369 } 370 } 371 372 private static void addPropertyForDateStr(ICalendar.Component component, 373 String propertyName, 374 String dateStr) { 375 if (TextUtils.isEmpty(dateStr)) { 376 return; 377 } 378 379 ICalendar.Property prop = new ICalendar.Property(propertyName); 380 String tz = null; 381 int tzidx = dateStr.indexOf(";"); 382 if (tzidx != -1) { 383 tz = dateStr.substring(0, tzidx); 384 dateStr = dateStr.substring(tzidx + 1); 385 } 386 if (!TextUtils.isEmpty(tz)) { 387 prop.addParameter(new ICalendar.Parameter("TZID", tz)); 388 } 389 prop.setValue(dateStr); 390 component.addProperty(prop); 391 } 392 393 private static String computeDuration(Time start, 394 ICalendar.Component component) { 395 // see if a duration is defined 396 ICalendar.Property durationProperty = 397 component.getFirstProperty("DURATION"); 398 if (durationProperty != null) { 399 // just return the duration 400 return durationProperty.getValue(); 401 } 402 403 // must compute a duration from the DTEND 404 ICalendar.Property dtendProperty = 405 component.getFirstProperty("DTEND"); 406 if (dtendProperty == null) { 407 // no DURATION, no DTEND: 0 second duration 408 return "+P0S"; 409 } 410 ICalendar.Parameter endTzidParameter = 411 dtendProperty.getFirstParameter("TZID"); 412 String endTzid = (endTzidParameter == null) 413 ? start.timezone : endTzidParameter.value; 414 415 Time end = new Time(endTzid); 416 end.parse(dtendProperty.getValue()); 417 long durationMillis = end.toMillis(false /* use isDst */) 418 - start.toMillis(false /* use isDst */); 419 long durationSeconds = (durationMillis / 1000); 420 if (start.allDay && (durationSeconds % 86400) == 0) { 421 return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S 422 } else { 423 return "P" + durationSeconds + "S"; 424 } 425 } 426 427 private static String flattenProperties(ICalendar.Component component, 428 String name) { 429 List<ICalendar.Property> properties = component.getProperties(name); 430 if (properties == null || properties.isEmpty()) { 431 return null; 432 } 433 434 if (properties.size() == 1) { 435 return properties.get(0).getValue(); 436 } 437 438 StringBuilder sb = new StringBuilder(); 439 440 boolean first = true; 441 for (ICalendar.Property property : component.getProperties(name)) { 442 if (first) { 443 first = false; 444 } else { 445 // TODO: use commas. our RECUR parsing should handle that 446 // anyway. 447 sb.append(RULE_SEPARATOR); 448 } 449 sb.append(property.getValue()); 450 } 451 return sb.toString(); 452 } 453 454 private static String extractDates(ICalendar.Property recurrence) { 455 if (recurrence == null) { 456 return null; 457 } 458 ICalendar.Parameter tzidParam = 459 recurrence.getFirstParameter("TZID"); 460 if (tzidParam != null) { 461 return tzidParam.value + ";" + recurrence.getValue(); 462 } 463 return recurrence.getValue(); 464 } 465} 466