CalendarInstancesHelper.java revision f5930ab5c4c0fe728cb8fdf923d482b4f272eb1f
1/* 2 * Copyright (C) 2011 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 com.android.providers.calendar; 18 19import com.android.providers.calendar.CalendarDatabaseHelper.Tables; 20import android.content.ContentValues; 21import android.database.Cursor; 22import android.database.DatabaseUtils; 23import android.database.sqlite.SQLiteDatabase; 24import android.database.sqlite.SQLiteQueryBuilder; 25import android.os.Debug; 26import android.pim.EventRecurrence; 27import android.pim.RecurrenceSet; 28import android.provider.BaseColumns; 29import android.provider.Calendar.Calendars; 30import android.provider.Calendar.Events; 31import android.provider.Calendar.Instances; 32import android.text.TextUtils; 33import android.text.format.Time; 34import android.util.Log; 35import android.util.TimeFormatException; 36 37import java.util.ArrayList; 38import java.util.HashMap; 39import java.util.Set; 40 41public class CalendarInstancesHelper { 42 public static final class EventInstancesMap extends 43 HashMap<String, CalendarInstancesHelper.InstancesList> { 44 public void add(String syncIdKey, ContentValues values) { 45 CalendarInstancesHelper.InstancesList instances = get(syncIdKey); 46 if (instances == null) { 47 instances = new CalendarInstancesHelper.InstancesList(); 48 put(syncIdKey, instances); 49 } 50 instances.add(values); 51 } 52 } 53 54 public static final class InstancesList extends ArrayList<ContentValues> { 55 } 56 57 private static final String TAG = "CalInstances"; 58 private CalendarDatabaseHelper mDbHelper; 59 private SQLiteDatabase mDb; 60 private MetaData mMetaData; 61 private CalendarCache mCalendarCache; 62 63 private static final String SQL_WHERE_GET_EVENTS_ENTRIES = 64 "((" + Events.DTSTART + " <= ? AND " 65 + "(" + Events.LAST_DATE + " IS NULL OR " + Events.LAST_DATE + " >= ?)) OR " 66 + "(" + Events.ORIGINAL_INSTANCE_TIME + " IS NOT NULL AND " 67 + Events.ORIGINAL_INSTANCE_TIME 68 + " <= ? AND " + Events.ORIGINAL_INSTANCE_TIME + " >= ?)) AND " 69 + "(" + Calendars.SYNC_EVENTS + " != 0)"; 70 71 private static final String SQL_SELECT_EVENTS_SYNC_ID = 72 "SELECT " + Events._SYNC_ID + 73 " FROM " + Tables.EVENTS + 74 " WHERE " + CalendarProvider2.SQL_WHERE_ID; 75 76 private static final String SQL_WHERE_ID_FROM_INSTANCES_NOT_SYNCED = 77 BaseColumns._ID + " IN " + 78 "(SELECT " + Tables.INSTANCES + "." + BaseColumns._ID + " as _id" + 79 " FROM " + Tables.INSTANCES + 80 " INNER JOIN " + Tables.EVENTS + 81 " ON (" + 82 Tables.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "." + Instances.EVENT_ID + 83 ")" + 84 " WHERE " + Tables.EVENTS + "." + Events._ID + "=?)"; 85 86 private static final String SQL_WHERE_ID_FROM_INSTANCES_SYNCED = 87 BaseColumns._ID + " IN " + 88 "(SELECT " + Tables.INSTANCES + "." + BaseColumns._ID + " as _id" + 89 " FROM " + Tables.INSTANCES + 90 " INNER JOIN " + Tables.EVENTS + 91 " ON (" + 92 Tables.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "." + Instances.EVENT_ID + 93 ")" + 94 " WHERE " + Tables.EVENTS + "." + Events._SYNC_ID + "=?" + " OR " + 95 Tables.EVENTS + "." + Events.ORIGINAL_EVENT + "=?)"; 96 97 private static final String[] EXPAND_COLUMNS = new String[] { 98 Events._ID, 99 Events._SYNC_ID, 100 Events.STATUS, 101 Events.DTSTART, 102 Events.DTEND, 103 Events.EVENT_TIMEZONE, 104 Events.RRULE, 105 Events.RDATE, 106 Events.EXRULE, 107 Events.EXDATE, 108 Events.DURATION, 109 Events.ALL_DAY, 110 Events.ORIGINAL_EVENT, 111 Events.ORIGINAL_INSTANCE_TIME, 112 Events.CALENDAR_ID, 113 Events.DELETED 114 }; 115 116 // To determine if a recurrence exception originally overlapped the 117 // window, we need to assume a maximum duration, since we only know 118 // the original start time. 119 private static final int MAX_ASSUMED_DURATION = 7 * 24 * 60 * 60 * 1000; 120 121 public CalendarInstancesHelper(CalendarDatabaseHelper calendarDbHelper) { 122 mDbHelper = calendarDbHelper; 123 mDb = mDbHelper.getWritableDatabase(); 124 mMetaData = new MetaData(mDbHelper); 125 mCalendarCache = new CalendarCache(mDbHelper); 126 } 127 128 /** 129 * Perform instance expansion on the given entries. 130 * 131 * @param begin Window start (ms). 132 * @param end Window end (ms). 133 * @param localTimezone 134 * @param entries The entries to process. 135 */ 136 protected void performInstanceExpansion(long begin, long end, String localTimezone, 137 Cursor entries) { 138 RecurrenceProcessor rp = new RecurrenceProcessor(); 139 140 // Key into the instance values to hold the original event concatenated 141 // with calendar id. 142 final String ORIGINAL_EVENT_AND_CALENDAR = "ORIGINAL_EVENT_AND_CALENDAR"; 143 144 int statusColumn = entries.getColumnIndex(Events.STATUS); 145 int dtstartColumn = entries.getColumnIndex(Events.DTSTART); 146 int dtendColumn = entries.getColumnIndex(Events.DTEND); 147 int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE); 148 int durationColumn = entries.getColumnIndex(Events.DURATION); 149 int rruleColumn = entries.getColumnIndex(Events.RRULE); 150 int rdateColumn = entries.getColumnIndex(Events.RDATE); 151 int exruleColumn = entries.getColumnIndex(Events.EXRULE); 152 int exdateColumn = entries.getColumnIndex(Events.EXDATE); 153 int allDayColumn = entries.getColumnIndex(Events.ALL_DAY); 154 int idColumn = entries.getColumnIndex(Events._ID); 155 int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID); 156 int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT); 157 int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME); 158 int calendarIdColumn = entries.getColumnIndex(Events.CALENDAR_ID); 159 int deletedColumn = entries.getColumnIndex(Events.DELETED); 160 161 ContentValues initialValues; 162 CalendarInstancesHelper.EventInstancesMap instancesMap = 163 new CalendarInstancesHelper.EventInstancesMap(); 164 165 Duration duration = new Duration(); 166 Time eventTime = new Time(); 167 168 // Invariant: entries contains all events that affect the current 169 // window. It consists of: 170 // a) Individual events that fall in the window. These will be 171 // displayed. 172 // b) Recurrences that included the window. These will be displayed 173 // if not canceled. 174 // c) Recurrence exceptions that fall in the window. These will be 175 // displayed if not cancellations. 176 // d) Recurrence exceptions that modify an instance inside the 177 // window (subject to 1 week assumption above), but are outside 178 // the window. These will not be displayed. Cases c and d are 179 // distingushed by the start / end time. 180 181 while (entries.moveToNext()) { 182 try { 183 initialValues = null; 184 185 boolean allDay = entries.getInt(allDayColumn) != 0; 186 187 String eventTimezone = entries.getString(eventTimezoneColumn); 188 if (allDay || TextUtils.isEmpty(eventTimezone)) { 189 // in the events table, allDay events start at midnight. 190 // this forces them to stay at midnight for all day events 191 // TODO: check that this actually does the right thing. 192 eventTimezone = Time.TIMEZONE_UTC; 193 } 194 195 long dtstartMillis = entries.getLong(dtstartColumn); 196 Long eventId = Long.valueOf(entries.getLong(idColumn)); 197 198 String durationStr = entries.getString(durationColumn); 199 if (durationStr != null) { 200 try { 201 duration.parse(durationStr); 202 } 203 catch (DateException e) { 204 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 205 Log.w(CalendarProvider2.TAG, "error parsing duration for event " 206 + eventId + "'" + durationStr + "'", e); 207 } 208 duration.sign = 1; 209 duration.weeks = 0; 210 duration.days = 0; 211 duration.hours = 0; 212 duration.minutes = 0; 213 duration.seconds = 0; 214 durationStr = "+P0S"; 215 } 216 } 217 218 String syncId = entries.getString(syncIdColumn); 219 String originalEvent = entries.getString(originalEventColumn); 220 221 long originalInstanceTimeMillis = -1; 222 if (!entries.isNull(originalInstanceTimeColumn)) { 223 originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn); 224 } 225 int status = entries.getInt(statusColumn); 226 boolean deleted = (entries.getInt(deletedColumn) != 0); 227 228 String rruleStr = entries.getString(rruleColumn); 229 String rdateStr = entries.getString(rdateColumn); 230 String exruleStr = entries.getString(exruleColumn); 231 String exdateStr = entries.getString(exdateColumn); 232 long calendarId = entries.getLong(calendarIdColumn); 233 // key into instancesMap 234 String syncIdKey = CalendarInstancesHelper.getSyncIdKey(syncId, calendarId); 235 236 RecurrenceSet recur = null; 237 try { 238 recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr); 239 } catch (EventRecurrence.InvalidFormatException e) { 240 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 241 Log.w(CalendarProvider2.TAG, "Could not parse RRULE recurrence string: " 242 + rruleStr, e); 243 } 244 continue; 245 } 246 247 if (null != recur && recur.hasRecurrence()) { 248 // the event is repeating 249 250 if (status == Events.STATUS_CANCELED) { 251 // should not happen! 252 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 253 Log.e(CalendarProvider2.TAG, "Found canceled recurring event in " 254 + "Events table. Ignoring."); 255 } 256 continue; 257 } 258 if (deleted) { 259 if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) { 260 Log.d(CalendarProvider2.TAG, "Found deleted recurring event in " 261 + "Events table. Ignoring."); 262 } 263 continue; 264 } 265 266 // need to parse the event into a local calendar. 267 eventTime.timezone = eventTimezone; 268 eventTime.set(dtstartMillis); 269 eventTime.allDay = allDay; 270 271 if (durationStr == null) { 272 // should not happen. 273 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 274 Log.e(CalendarProvider2.TAG, "Repeating event has no duration -- " 275 + "should not happen."); 276 } 277 if (allDay) { 278 // set to one day. 279 duration.sign = 1; 280 duration.weeks = 0; 281 duration.days = 1; 282 duration.hours = 0; 283 duration.minutes = 0; 284 duration.seconds = 0; 285 durationStr = "+P1D"; 286 } else { 287 // compute the duration from dtend, if we can. 288 // otherwise, use 0s. 289 duration.sign = 1; 290 duration.weeks = 0; 291 duration.days = 0; 292 duration.hours = 0; 293 duration.minutes = 0; 294 if (!entries.isNull(dtendColumn)) { 295 long dtendMillis = entries.getLong(dtendColumn); 296 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000); 297 durationStr = "+P" + duration.seconds + "S"; 298 } else { 299 duration.seconds = 0; 300 durationStr = "+P0S"; 301 } 302 } 303 } 304 305 long[] dates; 306 dates = rp.expand(eventTime, recur, begin, end); 307 308 // Initialize the "eventTime" timezone outside the loop. 309 // This is used in computeTimezoneDependentFields(). 310 if (allDay) { 311 eventTime.timezone = Time.TIMEZONE_UTC; 312 } else { 313 eventTime.timezone = localTimezone; 314 } 315 316 long durationMillis = duration.getMillis(); 317 for (long date : dates) { 318 initialValues = new ContentValues(); 319 initialValues.put(Instances.EVENT_ID, eventId); 320 321 initialValues.put(Instances.BEGIN, date); 322 long dtendMillis = date + durationMillis; 323 initialValues.put(Instances.END, dtendMillis); 324 325 CalendarInstancesHelper.computeTimezoneDependentFields(date, dtendMillis, 326 eventTime, initialValues); 327 instancesMap.add(syncIdKey, initialValues); 328 } 329 } else { 330 // the event is not repeating 331 initialValues = new ContentValues(); 332 333 // if this event has an "original" field, then record 334 // that we need to cancel the original event (we can't 335 // do that here because the order of this loop isn't 336 // defined) 337 if (originalEvent != null && originalInstanceTimeMillis != -1) { 338 // The ORIGINAL_EVENT_AND_CALENDAR holds the 339 // calendar id concatenated with the ORIGINAL_EVENT to form 340 // a unique key, matching the keys for instancesMap. 341 initialValues.put(ORIGINAL_EVENT_AND_CALENDAR, 342 CalendarInstancesHelper.getSyncIdKey(originalEvent, calendarId)); 343 initialValues.put(Events.ORIGINAL_INSTANCE_TIME, 344 originalInstanceTimeMillis); 345 initialValues.put(Events.STATUS, status); 346 } 347 348 long dtendMillis = dtstartMillis; 349 if (durationStr == null) { 350 if (!entries.isNull(dtendColumn)) { 351 dtendMillis = entries.getLong(dtendColumn); 352 } 353 } else { 354 dtendMillis = duration.addTo(dtstartMillis); 355 } 356 357 // this non-recurring event might be a recurrence exception that doesn't 358 // actually fall within our expansion window, but instead was selected 359 // so we can correctly cancel expanded recurrence instances below. do not 360 // add events to the instances map if they don't actually fall within our 361 // expansion window. 362 if ((dtendMillis < begin) || (dtstartMillis > end)) { 363 if (originalEvent != null && originalInstanceTimeMillis != -1) { 364 initialValues.put(Events.STATUS, Events.STATUS_CANCELED); 365 } else { 366 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 367 Log.w(CalendarProvider2.TAG, "Unexpected event outside window: " 368 + syncId); 369 } 370 continue; 371 } 372 } 373 374 initialValues.put(Instances.EVENT_ID, eventId); 375 376 initialValues.put(Instances.BEGIN, dtstartMillis); 377 initialValues.put(Instances.END, dtendMillis); 378 379 // we temporarily store the DELETED status (will be cleaned later) 380 initialValues.put(Events.DELETED, deleted); 381 382 if (allDay) { 383 eventTime.timezone = Time.TIMEZONE_UTC; 384 } else { 385 eventTime.timezone = localTimezone; 386 } 387 CalendarInstancesHelper.computeTimezoneDependentFields(dtstartMillis, 388 dtendMillis, eventTime, initialValues); 389 390 instancesMap.add(syncIdKey, initialValues); 391 } 392 } catch (DateException e) { 393 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 394 Log.w(CalendarProvider2.TAG, "RecurrenceProcessor error ", e); 395 } 396 } catch (TimeFormatException e) { 397 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) { 398 Log.w(CalendarProvider2.TAG, "RecurrenceProcessor error ", e); 399 } 400 } 401 } 402 403 // Invariant: instancesMap contains all instances that affect the 404 // window, indexed by original sync id concatenated with calendar id. 405 // It consists of: 406 // a) Individual events that fall in the window. They have: 407 // EVENT_ID, BEGIN, END 408 // b) Instances of recurrences that fall in the window. They may 409 // be subject to exceptions. They have: 410 // EVENT_ID, BEGIN, END 411 // c) Exceptions that fall in the window. They have: 412 // ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS (since they can 413 // be a modification or cancellation), EVENT_ID, BEGIN, END 414 // d) Recurrence exceptions that modify an instance inside the 415 // window but fall outside the window. They have: 416 // ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS = 417 // STATUS_CANCELED, EVENT_ID, BEGIN, END 418 419 // First, delete the original instances corresponding to recurrence 420 // exceptions. We do this by iterating over the list and for each 421 // recurrence exception, we search the list for an instance with a 422 // matching "original instance time". If we find such an instance, 423 // we remove it from the list. If we don't find such an instance 424 // then we cancel the recurrence exception. 425 Set<String> keys = instancesMap.keySet(); 426 for (String syncIdKey : keys) { 427 CalendarInstancesHelper.InstancesList list = instancesMap.get(syncIdKey); 428 for (ContentValues values : list) { 429 430 // If this instance is not a recurrence exception, then 431 // skip it. 432 if (!values.containsKey(ORIGINAL_EVENT_AND_CALENDAR)) { 433 continue; 434 } 435 436 String originalEventPlusCalendar = values.getAsString(ORIGINAL_EVENT_AND_CALENDAR); 437 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 438 CalendarInstancesHelper.InstancesList originalList = instancesMap 439 .get(originalEventPlusCalendar); 440 if (originalList == null) { 441 // The original recurrence is not present, so don't try canceling it. 442 continue; 443 } 444 445 // Search the original event for a matching original 446 // instance time. If there is a matching one, then remove 447 // the original one. We do this both for exceptions that 448 // change the original instance as well as for exceptions 449 // that delete the original instance. 450 for (int num = originalList.size() - 1; num >= 0; num--) { 451 ContentValues originalValues = originalList.get(num); 452 long beginTime = originalValues.getAsLong(Instances.BEGIN); 453 if (beginTime == originalTime) { 454 // We found the original instance, so remove it. 455 originalList.remove(num); 456 } 457 } 458 } 459 } 460 461 // Invariant: instancesMap contains filtered instances. 462 // It consists of: 463 // a) Individual events that fall in the window. 464 // b) Instances of recurrences that fall in the window and have not 465 // been subject to exceptions. 466 // c) Exceptions that fall in the window. They will have 467 // STATUS_CANCELED if they are cancellations. 468 // d) Recurrence exceptions that modify an instance inside the 469 // window but fall outside the window. These are STATUS_CANCELED. 470 471 // Now do the inserts. Since the db lock is held when this method is executed, 472 // this will be done in a transaction. 473 // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db 474 // while the calendar app is trying to query the db (expanding instances)), we will 475 // not be "polite" and yield the lock until we're done. This will favor local query 476 // operations over sync/write operations. 477 for (String syncIdKey : keys) { 478 CalendarInstancesHelper.InstancesList list = instancesMap.get(syncIdKey); 479 for (ContentValues values : list) { 480 481 // If this instance was cancelled or deleted then don't create a new 482 // instance. 483 Integer status = values.getAsInteger(Events.STATUS); 484 boolean deleted = values.containsKey(Events.DELETED) ? 485 values.getAsBoolean(Events.DELETED) : false; 486 if ((status != null && status == Events.STATUS_CANCELED) || deleted) { 487 continue; 488 } 489 490 // We remove this useless key (not valid in the context of Instances table) 491 values.remove(Events.DELETED); 492 493 // Remove these fields before inserting a new instance 494 values.remove(ORIGINAL_EVENT_AND_CALENDAR); 495 values.remove(Events.ORIGINAL_INSTANCE_TIME); 496 values.remove(Events.STATUS); 497 498 mDbHelper.instancesReplace(values); 499 } 500 } 501 } 502 503 /** 504 * Make instances for the given range. 505 */ 506 protected void expandInstanceRangeLocked(long begin, long end, String localTimezone) { 507 508 if (CalendarProvider2.PROFILE) { 509 Debug.startMethodTracing("expandInstanceRangeLocked"); 510 } 511 512 if (Log.isLoggable(TAG, Log.VERBOSE)) { 513 Log.v(TAG, "Expanding events between " + begin + " and " + end); 514 } 515 516 Cursor entries = getEntries(begin, end); 517 try { 518 performInstanceExpansion(begin, end, localTimezone, entries); 519 } finally { 520 if (entries != null) { 521 entries.close(); 522 } 523 } 524 if (CalendarProvider2.PROFILE) { 525 Debug.stopMethodTracing(); 526 } 527 } 528 529 /** 530 * Get all entries affecting the given window. 531 * 532 * @param begin Window start (ms). 533 * @param end Window end (ms). 534 * @return Cursor for the entries; caller must close it. 535 */ 536 private Cursor getEntries(long begin, long end) { 537 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 538 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 539 qb.setProjectionMap(CalendarProvider2.sEventsProjectionMap); 540 541 String beginString = String.valueOf(begin); 542 String endString = String.valueOf(end); 543 544 // grab recurrence exceptions that fall outside our expansion window but 545 // modify 546 // recurrences that do fall within our window. we won't insert these 547 // into the output 548 // set of instances, but instead will just add them to our cancellations 549 // list, so we 550 // can cancel the correct recurrence expansion instances. 551 // we don't have originalInstanceDuration or end time. for now, assume 552 // the original 553 // instance lasts no longer than 1 week. 554 // also filter with syncable state (we dont want the entries from a non 555 // syncable account) 556 // TODO: compute the originalInstanceEndTime or get this from the 557 // server. 558 qb.appendWhere(SQL_WHERE_GET_EVENTS_ENTRIES); 559 String selectionArgs[] = new String[] { 560 endString, beginString, endString, String.valueOf(begin - MAX_ASSUMED_DURATION) 561 }; 562 Cursor c = qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs, 563 null /* groupBy */, null /* having */, null /* sortOrder */); 564 if (Log.isLoggable(TAG, Log.VERBOSE)) { 565 Log.v(TAG, "Instance expansion: got " + c.getCount() + " entries"); 566 } 567 return c; 568 } 569 570 /** 571 * Updates the instances table when an event is added or updated. 572 * 573 * @param values The new values of the event. 574 * @param rowId The database row id of the event. 575 * @param newEvent true if the event is new. 576 * @param db The database 577 */ 578 public void updateInstancesLocked(ContentValues values, long rowId, boolean newEvent, 579 SQLiteDatabase db) { 580 581 // If there are no expanded Instances, then return. 582 MetaData.Fields fields = mMetaData.getFieldsLocked(); 583 if (fields.maxInstance == 0) { 584 return; 585 } 586 587 Long dtstartMillis = values.getAsLong(Events.DTSTART); 588 if (dtstartMillis == null) { 589 if (newEvent) { 590 // must be present for a new event. 591 throw new RuntimeException("DTSTART missing."); 592 } 593 if (Log.isLoggable(TAG, Log.VERBOSE)) { 594 Log.v(TAG, "Missing DTSTART. No need to update instance."); 595 } 596 return; 597 } 598 599 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 600 Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 601 602 if (!newEvent) { 603 // Want to do this for regular event, recurrence, or exception. 604 // For recurrence or exception, more deletion may happen below if we 605 // do an instance expansion. This deletion will suffice if the 606 // exception 607 // is moved outside the window, for instance. 608 db.delete(Tables.INSTANCES, Instances.EVENT_ID + "=?", new String[] { 609 String.valueOf(rowId) 610 }); 611 } 612 613 String rrule = values.getAsString(Events.RRULE); 614 String rdate = values.getAsString(Events.RDATE); 615 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 616 if (CalendarProvider2.isRecurrenceEvent(rrule, rdate, originalEvent)) { 617 // The recurrence or exception needs to be (re-)expanded if: 618 // a) Exception or recurrence that falls inside window 619 boolean insideWindow = dtstartMillis <= fields.maxInstance 620 && (lastDateMillis == null || lastDateMillis >= fields.minInstance); 621 // b) Exception that affects instance inside window 622 // These conditions match the query in getEntries 623 // See getEntries comment for explanation of subtracting 1 week. 624 boolean affectsWindow = originalInstanceTime != null 625 && originalInstanceTime <= fields.maxInstance 626 && originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION; 627 if (insideWindow || affectsWindow) { 628 updateRecurrenceInstancesLocked(values, rowId, db); 629 } 630 // TODO: an exception creation or update could be optimized by 631 // updating just the affected instances, instead of regenerating 632 // the recurrence. 633 return; 634 } 635 636 Long dtendMillis = values.getAsLong(Events.DTEND); 637 if (dtendMillis == null) { 638 dtendMillis = dtstartMillis; 639 } 640 641 // if the event is in the expanded range, insert 642 // into the instances table. 643 // TODO: deal with durations. currently, durations are only used in 644 // recurrences. 645 646 if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) { 647 ContentValues instanceValues = new ContentValues(); 648 instanceValues.put(Instances.EVENT_ID, rowId); 649 instanceValues.put(Instances.BEGIN, dtstartMillis); 650 instanceValues.put(Instances.END, dtendMillis); 651 652 boolean allDay = false; 653 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 654 if (allDayInteger != null) { 655 allDay = allDayInteger != 0; 656 } 657 658 // Update the timezone-dependent fields. 659 Time local = new Time(); 660 if (allDay) { 661 local.timezone = Time.TIMEZONE_UTC; 662 } else { 663 local.timezone = fields.timezone; 664 } 665 666 CalendarInstancesHelper.computeTimezoneDependentFields(dtstartMillis, dtendMillis, 667 local, instanceValues); 668 mDbHelper.instancesInsert(instanceValues); 669 } 670 } 671 672 /** 673 * Do incremental Instances update of a recurrence or recurrence exception. 674 * This method does performInstanceExpansion on just the modified 675 * recurrence, to avoid the overhead of recomputing the entire instance 676 * table. 677 * 678 * @param values The new values of the event. 679 * @param rowId The database row id of the event. 680 * @param db The database 681 */ 682 private void updateRecurrenceInstancesLocked(ContentValues values, long rowId, 683 SQLiteDatabase db) { 684 MetaData.Fields fields = mMetaData.getFieldsLocked(); 685 String instancesTimezone = mCalendarCache.readTimezoneInstances(); 686 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 687 String recurrenceSyncId; 688 if (originalEvent != null) { 689 recurrenceSyncId = originalEvent; 690 } else { 691 // Get the recurrence's sync id from the database 692 recurrenceSyncId = DatabaseUtils.stringForQuery(db, SQL_SELECT_EVENTS_SYNC_ID, 693 new String[] { 694 String.valueOf(rowId) 695 }); 696 } 697 // recurrenceSyncId is the _sync_id of the underlying recurrence 698 // If the recurrence hasn't gone to the server, it will be null. 699 700 // Need to clear out old instances 701 if (recurrenceSyncId == null) { 702 // Creating updating a recurrence that hasn't gone to the server. 703 // Need to delete based on row id 704 String where = SQL_WHERE_ID_FROM_INSTANCES_NOT_SYNCED; 705 db.delete(Tables.INSTANCES, where, new String[] { 706 "" + rowId 707 }); 708 } else { 709 // Creating or modifying a recurrence or exception. 710 // Delete instances for recurrence (_sync_id = recurrenceSyncId) 711 // and all exceptions (originalEvent = recurrenceSyncId) 712 String where = SQL_WHERE_ID_FROM_INSTANCES_SYNCED; 713 db.delete(Tables.INSTANCES, where, new String[] { 714 recurrenceSyncId, recurrenceSyncId 715 }); 716 } 717 718 // Now do instance expansion 719 Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId); 720 try { 721 performInstanceExpansion(fields.minInstance, fields.maxInstance, 722 instancesTimezone, entries); 723 } finally { 724 if (entries != null) { 725 entries.close(); 726 } 727 } 728 } 729 730 /** 731 * Determines the recurrence entries associated with a particular 732 * recurrence. This set is the base recurrence and any exception. Normally 733 * the entries are indicated by the sync id of the base recurrence (which is 734 * the originalEvent in the exceptions). However, a complication is that a 735 * recurrence may not yet have a sync id. In that case, the recurrence is 736 * specified by the rowId. 737 * 738 * @param recurrenceSyncId The sync id of the base recurrence, or null. 739 * @param rowId The row id of the base recurrence. 740 * @return the relevant entries. 741 */ 742 private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) { 743 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 744 745 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 746 qb.setProjectionMap(CalendarProvider2.sEventsProjectionMap); 747 String selectionArgs[]; 748 if (recurrenceSyncId == null) { 749 String where = CalendarProvider2.SQL_WHERE_ID; 750 qb.appendWhere(where); 751 selectionArgs = new String[] { 752 String.valueOf(rowId) 753 }; 754 } else { 755 String where = Events._SYNC_ID + "=? OR " + Events.ORIGINAL_EVENT + "=?"; 756 qb.appendWhere(where); 757 selectionArgs = new String[] { 758 recurrenceSyncId, recurrenceSyncId 759 }; 760 } 761 if (Log.isLoggable(TAG, Log.VERBOSE)) { 762 Log.v(TAG, "Retrieving events to expand: " + qb.toString()); 763 } 764 765 return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs, 766 null /* groupBy */, null /* having */, null /* sortOrder */); 767 } 768 769 /** 770 * Generates a unique key from the syncId and calendarId. The purpose of 771 * this is to prevent collisions if two different calendars use the same 772 * sync id. This can happen if a Google calendar is accessed by two 773 * different accounts, or with Exchange, where ids are not unique between 774 * calendars. 775 * 776 * @param syncId Id for the event 777 * @param calendarId Id for the calendar 778 * @return key 779 */ 780 static String getSyncIdKey(String syncId, long calendarId) { 781 return calendarId + ":" + syncId; 782 } 783 784 /** 785 * Computes the timezone-dependent fields of an instance of an event and 786 * updates the "values" map to contain those fields. 787 * 788 * @param begin the start time of the instance (in UTC milliseconds) 789 * @param end the end time of the instance (in UTC milliseconds) 790 * @param local a Time object with the timezone set to the local timezone 791 * @param values a map that will contain the timezone-dependent fields 792 */ 793 static void computeTimezoneDependentFields(long begin, long end, 794 Time local, ContentValues values) { 795 local.set(begin); 796 int startDay = Time.getJulianDay(begin, local.gmtoff); 797 int startMinute = local.hour * 60 + local.minute; 798 799 local.set(end); 800 int endDay = Time.getJulianDay(end, local.gmtoff); 801 int endMinute = local.hour * 60 + local.minute; 802 803 // Special case for midnight, which has endMinute == 0. Change 804 // that to +24 hours on the previous day to make everything simpler. 805 // Exception: if start and end minute are both 0 on the same day, 806 // then leave endMinute alone. 807 if (endMinute == 0 && endDay > startDay) { 808 endMinute = 24 * 60; 809 endDay -= 1; 810 } 811 812 values.put(Instances.START_DAY, startDay); 813 values.put(Instances.END_DAY, endDay); 814 values.put(Instances.START_MINUTE, startMinute); 815 values.put(Instances.END_MINUTE, endMinute); 816 } 817 818} 819