1 /* 2 ** 3 ** Copyright 2006, The Android Open Source Project 4 ** 5 ** Licensed under the Apache License, Version 2.0 (the "License"); 6 ** you may not use this file except in compliance with the License. 7 ** You may obtain a copy of the License at 8 ** 9 ** http://www.apache.org/licenses/LICENSE-2.0 10 ** 11 ** Unless required by applicable law or agreed to in writing, software 12 ** distributed under the License is distributed on an "AS IS" BASIS, 13 ** See the License for the specific language governing permissions and 14 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 ** limitations under the License. 16 */ 17 18 package com.android.providers.calendar; 19 20 import com.android.providers.calendar.CalendarDatabaseHelper.Tables; 21 import com.google.common.annotations.VisibleForTesting; 22 23 import android.accounts.Account; 24 import android.accounts.AccountManager; 25 import android.accounts.OnAccountsUpdateListener; 26 import android.app.AlarmManager; 27 import android.app.PendingIntent; 28 import android.content.BroadcastReceiver; 29 import android.content.ContentResolver; 30 import android.content.ContentUris; 31 import android.content.ContentValues; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.content.UriMatcher; 36 import android.database.Cursor; 37 import android.database.DatabaseUtils; 38 import android.database.SQLException; 39 import android.database.sqlite.SQLiteDatabase; 40 import android.database.sqlite.SQLiteQueryBuilder; 41 import android.net.Uri; 42 import android.os.Debug; 43 import android.os.Process; 44 import android.pim.EventRecurrence; 45 import android.pim.RecurrenceSet; 46 import android.provider.BaseColumns; 47 import android.provider.Calendar; 48 import android.provider.Calendar.Attendees; 49 import android.provider.Calendar.CalendarAlerts; 50 import android.provider.Calendar.Calendars; 51 import android.provider.Calendar.Events; 52 import android.provider.Calendar.Instances; 53 import android.provider.Calendar.Reminders; 54 import android.text.TextUtils; 55 import android.text.format.DateUtils; 56 import android.text.format.Time; 57 import android.util.Log; 58 import android.util.TimeFormatException; 59 import android.util.TimeUtils; 60 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.HashMap; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Set; 67 import java.util.TimeZone; 68 69 /** 70 * Calendar content provider. The contract between this provider and applications 71 * is defined in {@link android.provider.Calendar}. 72 */ 73 public class CalendarProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener { 74 75 private static final String TAG = "CalendarProvider2"; 76 77 private static final String TIMEZONE_GMT = "GMT"; 78 79 private static final boolean PROFILE = false; 80 private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true; 81 82 private static final String INVALID_CALENDARALERTS_SELECTOR = 83 "_id IN (SELECT ca._id FROM CalendarAlerts AS ca" 84 + " LEFT OUTER JOIN Instances USING (event_id, begin, end)" 85 + " LEFT OUTER JOIN Reminders AS r ON" 86 + " (ca.event_id=r.event_id AND ca.minutes=r.minutes)" 87 + " WHERE Instances.begin ISNULL OR ca.alarmTime<?" 88 + " OR (r.minutes ISNULL AND ca.minutes<>0))"; 89 90 private static final String[] ID_ONLY_PROJECTION = 91 new String[] {Events._ID}; 92 93 private static final String[] EVENTS_PROJECTION = new String[] { 94 Events._SYNC_ID, 95 Events.RRULE, 96 Events.RDATE, 97 Events.ORIGINAL_EVENT, 98 }; 99 private static final int EVENTS_SYNC_ID_INDEX = 0; 100 private static final int EVENTS_RRULE_INDEX = 1; 101 private static final int EVENTS_RDATE_INDEX = 2; 102 private static final int EVENTS_ORIGINAL_EVENT_INDEX = 3; 103 104 private static final String[] ID_PROJECTION = new String[] { 105 Attendees._ID, 106 Attendees.EVENT_ID, // Assume these are the same for each table 107 }; 108 private static final int ID_INDEX = 0; 109 private static final int EVENT_ID_INDEX = 1; 110 111 /** 112 * Projection to query for correcting times in allDay events. 113 */ 114 private static final String[] ALLDAY_TIME_PROJECTION = new String[] { 115 Events._ID, 116 Events.DTSTART, 117 Events.DTEND, 118 Events.DURATION 119 }; 120 private static final int ALLDAY_ID_INDEX = 0; 121 private static final int ALLDAY_DTSTART_INDEX = 1; 122 private static final int ALLDAY_DTEND_INDEX = 2; 123 private static final int ALLDAY_DURATION_INDEX = 3; 124 125 private static final int DAY_IN_SECONDS = 24 * 60 * 60; 126 127 /** 128 * The cached copy of the CalendarMetaData database table. 129 * Make this "package private" instead of "private" so that test code 130 * can access it. 131 */ 132 MetaData mMetaData; 133 CalendarCache mCalendarCache; 134 135 private CalendarDatabaseHelper mDbHelper; 136 137 private static final Uri SYNCSTATE_CONTENT_URI = Uri.parse("content://syncstate/state"); 138 // 139 // SCHEDULE_ALARM_URI runs scheduleNextAlarm(false) 140 // SCHEDULE_ALARM_REMOVE_URI runs scheduleNextAlarm(true) 141 // TODO: use a service to schedule alarms rather than private URI 142 /* package */ static final String SCHEDULE_ALARM_PATH = "schedule_alarms"; 143 /* package */ static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove"; 144 /* package */ static final Uri SCHEDULE_ALARM_URI = 145 Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_PATH); 146 /* package */ static final Uri SCHEDULE_ALARM_REMOVE_URI = 147 Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH); 148 149 // 5 second delay before updating alarms 150 private static final long ALARM_SCHEDULER_DELAY = 5000; 151 152 // To determine if a recurrence exception originally overlapped the 153 // window, we need to assume a maximum duration, since we only know 154 // the original start time. 155 private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000; 156 157 // The extended property name for storing an Event original Timezone. 158 // Due to an issue in Calendar Server restricting the length of the name we had to strip it down 159 // TODO - Better name would be: 160 // "com.android.providers.calendar.CalendarSyncAdapter#originalTimezone" 161 protected static final String EXT_PROP_ORIGINAL_TIMEZONE = 162 "CalendarSyncAdapter#originalTimezone"; 163 164 private static final String SQL_SELECT_EVENTSRAWTIMES = "SELECT " + 165 EventsRawTimesColumns.EVENT_ID + ", " + 166 EventsRawTimesColumns.DTSTART_2445 + ", " + 167 EventsRawTimesColumns.DTEND_2445 + ", " + 168 Events.EVENT_TIMEZONE + 169 " FROM " + 170 "EventsRawTimes" + ", " + 171 "Events" + 172 " WHERE " + 173 EventsRawTimesColumns.EVENT_ID + " = " + "Events." + Events._ID; 174 175 private static final String SQL_SELECT_COUNT_FOR_SYNC_ID = 176 "SELECT COUNT(*) FROM " + Tables.EVENTS + " WHERE " + Events._SYNC_ID + "=?"; 177 178 private static final String SQL_WHERE_ID = BaseColumns._ID + "=?"; 179 180 public static final class TimeRange { 181 public long begin; 182 public long end; 183 public boolean allDay; 184 } 185 186 public static final class InstancesRange { 187 public long begin; 188 public long end; 189 190 public InstancesRange(long begin, long end) { 191 this.begin = begin; 192 this.end = end; 193 } 194 } 195 196 public static final class InstancesList 197 extends ArrayList<ContentValues> { 198 } 199 200 public static final class EventInstancesMap 201 extends HashMap<String, InstancesList> { 202 public void add(String syncIdKey, ContentValues values) { 203 InstancesList instances = get(syncIdKey); 204 if (instances == null) { 205 instances = new InstancesList(); 206 put(syncIdKey, instances); 207 } 208 instances.add(values); 209 } 210 } 211 212 // A thread that runs in the background and schedules the next 213 // calendar event alarm. It delays for 5 seconds before updating 214 // to aggregate further requests. 215 private class AlarmScheduler extends Thread { 216 boolean mRemoveAlarms; 217 218 public AlarmScheduler(boolean removeAlarms) { 219 mRemoveAlarms = removeAlarms; 220 } 221 222 @Override 223 public void run() { 224 Context context = CalendarProvider2.this.getContext(); 225 // Because the handler does not guarantee message delivery in 226 // the case that the provider is killed, we need to make sure 227 // that the provider stays alive long enough to deliver the 228 // notification. This empty service is sufficient to "wedge" the 229 // process until we finish. 230 context.startService(new Intent(context, EmptyService.class)); 231 while (true) { 232 // Wait a bit before writing to collect any other requests that 233 // may come in 234 try { 235 sleep(ALARM_SCHEDULER_DELAY); 236 } catch (InterruptedException e1) { 237 if(Log.isLoggable(TAG, Log.DEBUG)) { 238 Log.d(TAG, "AlarmScheduler woke up early: " + e1.getMessage()); 239 } 240 } 241 // Clear any new requests and update whether or not we should 242 // remove alarms 243 synchronized (mAlarmLock) { 244 mRemoveAlarms = mRemoveAlarms || mRemoveAlarmsOnRerun; 245 mRerunAlarmScheduler = false; 246 mRemoveAlarmsOnRerun = false; 247 } 248 // Run the update 249 try { 250 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 251 runScheduleNextAlarm(mRemoveAlarms); 252 } catch (SQLException e) { 253 if (Log.isLoggable(TAG, Log.ERROR)) { 254 Log.e(TAG, "runScheduleNextAlarm() failed", e); 255 } 256 } 257 // Check if anyone requested another alarm change while we were busy. 258 // if not clear everything out and exit. 259 synchronized (mAlarmLock) { 260 if (!mRerunAlarmScheduler) { 261 mAlarmScheduler = null; 262 mRerunAlarmScheduler = false; 263 mRemoveAlarmsOnRerun = false; 264 context.stopService(new Intent(context, EmptyService.class)); 265 return; 266 } 267 } 268 } 269 } 270 } 271 272 private static AlarmScheduler mAlarmScheduler; 273 274 private static boolean mRerunAlarmScheduler = false; 275 private static boolean mRemoveAlarmsOnRerun = false; 276 277 /** 278 * We search backward in time for event reminders that we may have missed 279 * and schedule them if the event has not yet expired. The amount in 280 * the past to search backwards is controlled by this constant. It 281 * should be at least a few minutes to allow for an event that was 282 * recently created on the web to make its way to the phone. Two hours 283 * might seem like overkill, but it is useful in the case where the user 284 * just crossed into a new timezone and might have just missed an alarm. 285 */ 286 private static final long SCHEDULE_ALARM_SLACK = 2 * DateUtils.HOUR_IN_MILLIS; 287 288 /** 289 * Alarms older than this threshold will be deleted from the CalendarAlerts 290 * table. This should be at least a day because if the timezone is 291 * wrong and the user corrects it we might delete good alarms that 292 * appear to be old because the device time was incorrectly in the future. 293 * This threshold must also be larger than SCHEDULE_ALARM_SLACK. We add 294 * the SCHEDULE_ALARM_SLACK to ensure this. 295 * 296 * To make it easier to find and debug problems with missed reminders, 297 * set this to something greater than a day. 298 */ 299 private static final long CLEAR_OLD_ALARM_THRESHOLD = 300 7 * DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK; 301 302 // A lock for synchronizing access to fields that are shared 303 // with the AlarmScheduler thread. 304 private Object mAlarmLock = new Object(); 305 306 // Make sure we load at least two months worth of data. 307 // Client apps can load more data in a background thread. 308 private static final long MINIMUM_EXPANSION_SPAN = 309 2L * 31 * 24 * 60 * 60 * 1000; 310 311 private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID }; 312 private static final int CALENDARS_INDEX_ID = 0; 313 314 // Allocate the string constant once here instead of on the heap 315 private static final String CALENDAR_ID_SELECTION = "calendar_id=?"; 316 317 private static final String[] sInstancesProjection = 318 new String[] { Instances.START_DAY, Instances.END_DAY, 319 Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY }; 320 321 private static final int INSTANCES_INDEX_START_DAY = 0; 322 private static final int INSTANCES_INDEX_END_DAY = 1; 323 private static final int INSTANCES_INDEX_START_MINUTE = 2; 324 private static final int INSTANCES_INDEX_END_MINUTE = 3; 325 private static final int INSTANCES_INDEX_ALL_DAY = 4; 326 327 private AlarmManager mAlarmManager; 328 329 private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance(); 330 331 /** 332 * Listens for timezone changes and disk-no-longer-full events 333 */ 334 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 335 @Override 336 public void onReceive(Context context, Intent intent) { 337 String action = intent.getAction(); 338 if (Log.isLoggable(TAG, Log.DEBUG)) { 339 Log.d(TAG, "onReceive() " + action); 340 } 341 if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { 342 updateTimezoneDependentFields(); 343 scheduleNextAlarm(false /* do not remove alarms */); 344 } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { 345 // Try to clean up if things were screwy due to a full disk 346 updateTimezoneDependentFields(); 347 scheduleNextAlarm(false /* do not remove alarms */); 348 } else if (Intent.ACTION_TIME_CHANGED.equals(action)) { 349 scheduleNextAlarm(false /* do not remove alarms */); 350 } 351 } 352 }; 353 354 /** 355 * Columns from the EventsRawTimes table 356 */ 357 public interface EventsRawTimesColumns 358 { 359 /** 360 * The corresponding event id 361 * <P>Type: INTEGER (long)</P> 362 */ 363 public static final String EVENT_ID = "event_id"; 364 365 /** 366 * The RFC2445 compliant time the event starts 367 * <P>Type: TEXT</P> 368 */ 369 public static final String DTSTART_2445 = "dtstart2445"; 370 371 /** 372 * The RFC2445 compliant time the event ends 373 * <P>Type: TEXT</P> 374 */ 375 public static final String DTEND_2445 = "dtend2445"; 376 377 /** 378 * The RFC2445 compliant original instance time of the recurring event for which this 379 * event is an exception. 380 * <P>Type: TEXT</P> 381 */ 382 public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445"; 383 384 /** 385 * The RFC2445 compliant last date this event repeats on, or NULL if it never ends 386 * <P>Type: TEXT</P> 387 */ 388 public static final String LAST_DATE_2445 = "lastDate2445"; 389 } 390 391 protected void verifyAccounts() { 392 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 393 onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); 394 } 395 396 /* Visible for testing */ 397 @Override 398 protected CalendarDatabaseHelper getDatabaseHelper(final Context context) { 399 return CalendarDatabaseHelper.getInstance(context); 400 } 401 402 @Override 403 public boolean onCreate() { 404 super.onCreate(); 405 mDbHelper = (CalendarDatabaseHelper)getDatabaseHelper(); 406 407 verifyAccounts(); 408 409 // Register for Intent broadcasts 410 IntentFilter filter = new IntentFilter(); 411 412 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 413 filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); 414 filter.addAction(Intent.ACTION_TIME_CHANGED); 415 final Context c = getContext(); 416 417 // We don't ever unregister this because this thread always wants 418 // to receive notifications, even in the background. And if this 419 // thread is killed then the whole process will be killed and the 420 // memory resources will be reclaimed. 421 c.registerReceiver(mIntentReceiver, filter); 422 423 mMetaData = new MetaData(mDbHelper); 424 mCalendarCache = new CalendarCache(mDbHelper); 425 426 updateTimezoneDependentFields(); 427 428 return true; 429 } 430 431 /** 432 * This creates a background thread to check the timezone and update 433 * the timezone dependent fields in the Instances table if the timezone 434 * has changed. 435 */ 436 protected void updateTimezoneDependentFields() { 437 Thread thread = new TimezoneCheckerThread(); 438 thread.start(); 439 } 440 441 private class TimezoneCheckerThread extends Thread { 442 @Override 443 public void run() { 444 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 445 try { 446 doUpdateTimezoneDependentFields(); 447 triggerAppWidgetUpdate(-1 /*changedEventId*/ ); 448 } catch (SQLException e) { 449 if (Log.isLoggable(TAG, Log.ERROR)) { 450 Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e); 451 } 452 try { 453 // Clear at least the in-memory data (and if possible the 454 // database fields) to force a re-computation of Instances. 455 mMetaData.clearInstanceRange(); 456 } catch (SQLException e2) { 457 if (Log.isLoggable(TAG, Log.ERROR)) { 458 Log.e(TAG, "clearInstanceRange() also failed: " + e2); 459 } 460 } 461 } 462 } 463 } 464 465 /** 466 * Check if we are in the same time zone 467 */ 468 private boolean isLocalSameAsInstancesTimezone() { 469 String localTimezone = TimeZone.getDefault().getID(); 470 return TextUtils.equals(mCalendarCache.readTimezoneInstances(), localTimezone); 471 } 472 473 /** 474 * This method runs in a background thread. If the timezone db or timezone has changed 475 * then the Instances table will be regenerated. 476 */ 477 protected void doUpdateTimezoneDependentFields() { 478 String timezoneType = mCalendarCache.readTimezoneType(); 479 // Nothing to do if we have the "home" timezone type (timezone is sticky) 480 if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_HOME)) { 481 return; 482 } 483 // We are here in "auto" mode, the timezone is coming from the device 484 if (! isSameTimezoneDatabaseVersion()) { 485 String localTimezone = TimeZone.getDefault().getID(); 486 doProcessEventRawTimes(localTimezone, TimeUtils.getTimeZoneDatabaseVersion()); 487 } 488 if (isLocalSameAsInstancesTimezone()) { 489 // Even if the timezone hasn't changed, check for missed alarms. 490 // This code executes when the CalendarProvider2 is created and 491 // helps to catch missed alarms when the Calendar process is 492 // killed (because of low-memory conditions) and then restarted. 493 rescheduleMissedAlarms(); 494 } 495 } 496 497 protected void doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion) { 498 mDb = mDbHelper.getWritableDatabase(); 499 if (mDb == null) { 500 if (Log.isLoggable(TAG, Log.VERBOSE)) { 501 Log.v(TAG, "Cannot update Events table from EventsRawTimes table"); 502 } 503 return; 504 } 505 mDb.beginTransaction(); 506 try { 507 updateEventsStartEndFromEventRawTimesLocked(); 508 updateTimezoneDatabaseVersion(timeZoneDatabaseVersion); 509 mCalendarCache.writeTimezoneInstances(localTimezone); 510 regenerateInstancesTable(); 511 mDb.setTransactionSuccessful(); 512 } finally { 513 mDb.endTransaction(); 514 } 515 } 516 517 private void updateEventsStartEndFromEventRawTimesLocked() { 518 Cursor cursor = mDb.rawQuery(SQL_SELECT_EVENTSRAWTIMES, null /* selection args */); 519 try { 520 while (cursor.moveToNext()) { 521 long eventId = cursor.getLong(0); 522 String dtStart2445 = cursor.getString(1); 523 String dtEnd2445 = cursor.getString(2); 524 String eventTimezone = cursor.getString(3); 525 if (dtStart2445 == null && dtEnd2445 == null) { 526 if (Log.isLoggable(TAG, Log.ERROR)) { 527 Log.e(TAG, "Event " + eventId + " has dtStart2445 and dtEnd2445 null " 528 + "at the same time in EventsRawTimes!"); 529 } 530 continue; 531 } 532 updateEventsStartEndLocked(eventId, 533 eventTimezone, 534 dtStart2445, 535 dtEnd2445); 536 } 537 } finally { 538 cursor.close(); 539 cursor = null; 540 } 541 } 542 543 private long get2445ToMillis(String timezone, String dt2445) { 544 if (null == dt2445) { 545 if (Log.isLoggable(TAG, Log.VERBOSE)) { 546 Log.v( TAG, "Cannot parse null RFC2445 date"); 547 } 548 return 0; 549 } 550 Time time = (timezone != null) ? new Time(timezone) : new Time(); 551 try { 552 time.parse(dt2445); 553 } catch (TimeFormatException e) { 554 if (Log.isLoggable(TAG, Log.ERROR)) { 555 Log.e( TAG, "Cannot parse RFC2445 date " + dt2445); 556 } 557 return 0; 558 } 559 return time.toMillis(true /* ignore DST */); 560 } 561 562 private void updateEventsStartEndLocked(long eventId, 563 String timezone, String dtStart2445, String dtEnd2445) { 564 565 ContentValues values = new ContentValues(); 566 values.put("dtstart", get2445ToMillis(timezone, dtStart2445)); 567 values.put("dtend", get2445ToMillis(timezone, dtEnd2445)); 568 569 int result = mDb.update("Events", values, "_id=?", 570 new String[] {String.valueOf(eventId)}); 571 if (0 == result) { 572 if (Log.isLoggable(TAG, Log.VERBOSE)) { 573 Log.v(TAG, "Could not update Events table with values " + values); 574 } 575 } 576 } 577 578 private void updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion) { 579 try { 580 mCalendarCache.writeTimezoneDatabaseVersion(timeZoneDatabaseVersion); 581 } catch (CalendarCache.CacheException e) { 582 if (Log.isLoggable(TAG, Log.ERROR)) { 583 Log.e(TAG, "Could not write timezone database version in the cache"); 584 } 585 } 586 } 587 588 /** 589 * Check if the time zone database version is the same as the cached one 590 */ 591 protected boolean isSameTimezoneDatabaseVersion() { 592 String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion(); 593 if (timezoneDatabaseVersion == null) { 594 return false; 595 } 596 return TextUtils.equals(timezoneDatabaseVersion, TimeUtils.getTimeZoneDatabaseVersion()); 597 } 598 599 @VisibleForTesting 600 protected String getTimezoneDatabaseVersion() { 601 String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion(); 602 if (timezoneDatabaseVersion == null) { 603 return ""; 604 } 605 if (Log.isLoggable(TAG, Log.INFO)) { 606 Log.i(TAG, "timezoneDatabaseVersion = " + timezoneDatabaseVersion); 607 } 608 return timezoneDatabaseVersion; 609 } 610 611 private boolean isHomeTimezone() { 612 String type = mCalendarCache.readTimezoneType(); 613 return type.equals(CalendarCache.TIMEZONE_TYPE_HOME); 614 } 615 616 private void regenerateInstancesTable() { 617 // The database timezone is different from the current timezone. 618 // Regenerate the Instances table for this month. Include events 619 // starting at the beginning of this month. 620 long now = System.currentTimeMillis(); 621 String instancesTimezone = mCalendarCache.readTimezoneInstances(); 622 Time time = new Time(instancesTimezone); 623 time.set(now); 624 time.monthDay = 1; 625 time.hour = 0; 626 time.minute = 0; 627 time.second = 0; 628 629 long begin = time.normalize(true); 630 long end = begin + MINIMUM_EXPANSION_SPAN; 631 632 Cursor cursor = null; 633 try { 634 cursor = handleInstanceQuery(new SQLiteQueryBuilder(), 635 begin, end, 636 new String[] { Instances._ID }, 637 null /* selection */, null /* sort */, 638 false /* searchByDayInsteadOfMillis */, 639 true /* force Instances deletion and expansion */, 640 instancesTimezone, 641 isHomeTimezone()); 642 } finally { 643 if (cursor != null) { 644 cursor.close(); 645 } 646 } 647 648 rescheduleMissedAlarms(); 649 } 650 651 private void rescheduleMissedAlarms() { 652 AlarmManager manager = getAlarmManager(); 653 if (manager != null) { 654 Context context = getContext(); 655 ContentResolver cr = context.getContentResolver(); 656 CalendarAlerts.rescheduleMissedAlarms(cr, context, manager); 657 } 658 } 659 660 /** 661 * Appends comma separated ids. 662 * @param ids Should not be empty 663 */ 664 private void appendIds(StringBuilder sb, HashSet<Long> ids) { 665 for (long id : ids) { 666 sb.append(id).append(','); 667 } 668 669 sb.setLength(sb.length() - 1); // Yank the last comma 670 } 671 672 @Override 673 protected void notifyChange() { 674 // Note that semantics are changed: notification is for CONTENT_URI, not the specific 675 // Uri that was modified. 676 getContext().getContentResolver().notifyChange(Calendar.CONTENT_URI, null, 677 true /* syncToNetwork */); 678 } 679 680 @Override 681 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 682 String sortOrder) { 683 if (Log.isLoggable(TAG, Log.VERBOSE)) { 684 Log.v(TAG, "query uri - " + uri); 685 } 686 687 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 688 689 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 690 String groupBy = null; 691 String limit = null; // Not currently implemented 692 String instancesTimezone; 693 694 final int match = sUriMatcher.match(uri); 695 switch (match) { 696 case SYNCSTATE: 697 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 698 sortOrder); 699 700 case EVENTS: 701 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 702 qb.setProjectionMap(sEventsProjectionMap); 703 appendAccountFromParameter(qb, uri); 704 break; 705 case EVENTS_ID: 706 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 707 qb.setProjectionMap(sEventsProjectionMap); 708 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 709 qb.appendWhere("_id=?"); 710 break; 711 712 case EVENT_ENTITIES: 713 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 714 qb.setProjectionMap(sEventEntitiesProjectionMap); 715 appendAccountFromParameter(qb, uri); 716 break; 717 case EVENT_ENTITIES_ID: 718 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 719 qb.setProjectionMap(sEventEntitiesProjectionMap); 720 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 721 qb.appendWhere("_id=?"); 722 break; 723 724 case CALENDARS: 725 qb.setTables("Calendars"); 726 appendAccountFromParameter(qb, uri); 727 break; 728 case CALENDARS_ID: 729 qb.setTables("Calendars"); 730 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 731 qb.appendWhere("_id=?"); 732 break; 733 case INSTANCES: 734 case INSTANCES_BY_DAY: 735 long begin; 736 long end; 737 try { 738 begin = Long.valueOf(uri.getPathSegments().get(2)); 739 } catch (NumberFormatException nfe) { 740 throw new IllegalArgumentException("Cannot parse begin " 741 + uri.getPathSegments().get(2)); 742 } 743 try { 744 end = Long.valueOf(uri.getPathSegments().get(3)); 745 } catch (NumberFormatException nfe) { 746 throw new IllegalArgumentException("Cannot parse end " 747 + uri.getPathSegments().get(3)); 748 } 749 instancesTimezone = mCalendarCache.readTimezoneInstances(); 750 return handleInstanceQuery(qb, begin, end, projection, 751 selection, sortOrder, match == INSTANCES_BY_DAY, 752 false /* do not force Instances deletion and expansion */, 753 instancesTimezone, isHomeTimezone()); 754 case EVENT_DAYS: 755 int startDay; 756 int endDay; 757 try { 758 startDay = Integer.valueOf(uri.getPathSegments().get(2)); 759 } catch (NumberFormatException nfe) { 760 throw new IllegalArgumentException("Cannot parse start day " 761 + uri.getPathSegments().get(2)); 762 } 763 try { 764 endDay = Integer.valueOf(uri.getPathSegments().get(3)); 765 } catch (NumberFormatException nfe) { 766 throw new IllegalArgumentException("Cannot parse end day " 767 + uri.getPathSegments().get(3)); 768 } 769 instancesTimezone = mCalendarCache.readTimezoneInstances(); 770 return handleEventDayQuery(qb, startDay, endDay, projection, selection, 771 instancesTimezone, isHomeTimezone()); 772 case ATTENDEES: 773 qb.setTables("Attendees, Events"); 774 qb.setProjectionMap(sAttendeesProjectionMap); 775 qb.appendWhere("Events._id=Attendees.event_id"); 776 break; 777 case ATTENDEES_ID: 778 qb.setTables("Attendees, Events"); 779 qb.setProjectionMap(sAttendeesProjectionMap); 780 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 781 qb.appendWhere("Attendees._id=? AND Events._id=Attendees.event_id"); 782 break; 783 case REMINDERS: 784 qb.setTables("Reminders"); 785 break; 786 case REMINDERS_ID: 787 qb.setTables("Reminders, Events"); 788 qb.setProjectionMap(sRemindersProjectionMap); 789 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 790 qb.appendWhere("Reminders._id=? AND Events._id=Reminders.event_id"); 791 break; 792 case CALENDAR_ALERTS: 793 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS); 794 qb.setProjectionMap(sCalendarAlertsProjectionMap); 795 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS + 796 "._id=CalendarAlerts.event_id"); 797 break; 798 case CALENDAR_ALERTS_BY_INSTANCE: 799 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS); 800 qb.setProjectionMap(sCalendarAlertsProjectionMap); 801 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS + 802 "._id=CalendarAlerts.event_id"); 803 groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN; 804 break; 805 case CALENDAR_ALERTS_ID: 806 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS); 807 qb.setProjectionMap(sCalendarAlertsProjectionMap); 808 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 809 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS + 810 "._id=CalendarAlerts.event_id AND CalendarAlerts._id=?"); 811 break; 812 case EXTENDED_PROPERTIES: 813 qb.setTables("ExtendedProperties"); 814 break; 815 case EXTENDED_PROPERTIES_ID: 816 qb.setTables("ExtendedProperties"); 817 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 818 qb.appendWhere("ExtendedProperties._id=?"); 819 break; 820 case PROVIDER_PROPERTIES: 821 qb.setTables("CalendarCache"); 822 qb.setProjectionMap(sCalendarCacheProjectionMap); 823 break; 824 default: 825 throw new IllegalArgumentException("Unknown URL " + uri); 826 } 827 828 // run the query 829 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 830 } 831 832 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 833 String selection, String[] selectionArgs, String sortOrder, String groupBy, 834 String limit) { 835 836 if (Log.isLoggable(TAG, Log.VERBOSE)) { 837 Log.v(TAG, "query sql - projection: " + Arrays.toString(projection) + 838 " selection: " + selection + 839 " selectionArgs: " + Arrays.toString(selectionArgs) + 840 " sortOrder: " + sortOrder + 841 " groupBy: " + groupBy + 842 " limit: " + limit); 843 } 844 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 845 sortOrder, limit); 846 if (c != null) { 847 // TODO: is this the right notification Uri? 848 c.setNotificationUri(getContext().getContentResolver(), Calendar.Events.CONTENT_URI); 849 } 850 return c; 851 } 852 853 /* 854 * Fills the Instances table, if necessary, for the given range and then 855 * queries the Instances table. 856 * 857 * @param qb The query 858 * @param rangeBegin start of range (Julian days or ms) 859 * @param rangeEnd end of range (Julian days or ms) 860 * @param projection The projection 861 * @param selection The selection 862 * @param sort How to sort 863 * @param searchByDay if true, range is in Julian days, if false, range is in ms 864 * @param forceExpansion force the Instance deletion and expansion if set to true 865 * @param instancesTimezone timezone we need to use for computing the instances 866 * @param isHomeTimezone if true, we are in the "home" timezone 867 * @return 868 */ 869 private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, 870 long rangeEnd, String[] projection, String selection, String sort, 871 boolean searchByDay, boolean forceExpansion, String instancesTimezone, 872 boolean isHomeTimezone) { 873 874 qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " + 875 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)"); 876 qb.setProjectionMap(sInstancesProjectionMap); 877 if (searchByDay) { 878 // Convert the first and last Julian day range to a range that uses 879 // UTC milliseconds. 880 Time time = new Time(instancesTimezone); 881 long beginMs = time.setJulianDay((int) rangeBegin); 882 // We add one to lastDay because the time is set to 12am on the given 883 // Julian day and we want to include all the events on the last day. 884 long endMs = time.setJulianDay((int) rangeEnd + 1); 885 // will lock the database. 886 acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */, 887 forceExpansion, instancesTimezone, isHomeTimezone 888 ); 889 qb.appendWhere("startDay<=? AND endDay>=?"); 890 } else { 891 // will lock the database. 892 acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */, 893 forceExpansion, instancesTimezone, isHomeTimezone 894 ); 895 qb.appendWhere("begin<=? AND end>=?"); 896 } 897 String selectionArgs[] = new String[] {String.valueOf(rangeEnd), 898 String.valueOf(rangeBegin)}; 899 return qb.query(mDb, projection, selection, selectionArgs, null /* groupBy */, 900 null /* having */, sort); 901 } 902 903 private Cursor handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end, 904 String[] projection, String selection, String instancesTimezone, 905 boolean isHomeTimezone) { 906 qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " + 907 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)"); 908 qb.setProjectionMap(sInstancesProjectionMap); 909 // Convert the first and last Julian day range to a range that uses 910 // UTC milliseconds. 911 Time time = new Time(instancesTimezone); 912 long beginMs = time.setJulianDay(begin); 913 // We add one to lastDay because the time is set to 12am on the given 914 // Julian day and we want to include all the events on the last day. 915 long endMs = time.setJulianDay(end + 1); 916 917 acquireInstanceRange(beginMs, endMs, true, 918 false /* do not force Instances expansion */, instancesTimezone, isHomeTimezone); 919 qb.appendWhere("startDay<=? AND endDay>=?"); 920 String selectionArgs[] = new String[] {String.valueOf(end), String.valueOf(begin)}; 921 922 return qb.query(mDb, projection, selection, selectionArgs, 923 Instances.START_DAY /* groupBy */, null /* having */, null); 924 } 925 926 /** 927 * Ensure that the date range given has all elements in the instance 928 * table. Acquires the database lock and calls {@link #acquireInstanceRangeLocked}. 929 * 930 * @param begin start of range (ms) 931 * @param end end of range (ms) 932 * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN 933 * @param forceExpansion force the Instance deletion and expansion if set to true 934 * @param instancesTimezone timezone we need to use for computing the instances 935 * @param isHomeTimezone if true, we are in the "home" timezone 936 */ 937 private void acquireInstanceRange(final long begin, final long end, 938 final boolean useMinimumExpansionWindow, final boolean forceExpansion, 939 final String instancesTimezone, final boolean isHomeTimezone) { 940 mDb.beginTransaction(); 941 try { 942 acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow, 943 forceExpansion, instancesTimezone, isHomeTimezone); 944 mDb.setTransactionSuccessful(); 945 } finally { 946 mDb.endTransaction(); 947 } 948 } 949 950 /** 951 * Ensure that the date range given has all elements in the instance 952 * table. The database lock must be held when calling this method. 953 * 954 * @param begin start of range (ms) 955 * @param end end of range (ms) 956 * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN 957 * @param forceExpansion force the Instance deletion and expansion if set to true 958 * @param instancesTimezone timezone we need to use for computing the instances 959 * @param isHomeTimezone if true, we are in the "home" timezone 960 */ 961 private void acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow, 962 boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone) { 963 long expandBegin = begin; 964 long expandEnd = end; 965 966 if (instancesTimezone == null) { 967 if (Log.isLoggable(TAG, Log.ERROR)) { 968 Log.e(TAG, "Cannot run acquireInstanceRangeLocked() " 969 + "because instancesTimezone is null"); 970 } 971 return; 972 } 973 974 if (useMinimumExpansionWindow) { 975 // if we end up having to expand events into the instances table, expand 976 // events for a minimal amount of time, so we do not have to perform 977 // expansions frequently. 978 long span = end - begin; 979 if (span < MINIMUM_EXPANSION_SPAN) { 980 long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2; 981 expandBegin -= additionalRange; 982 expandEnd += additionalRange; 983 } 984 } 985 986 // Check if the timezone has changed. 987 // We do this check here because the database is locked and we can 988 // safely delete all the entries in the Instances table. 989 MetaData.Fields fields = mMetaData.getFieldsLocked(); 990 long maxInstance = fields.maxInstance; 991 long minInstance = fields.minInstance; 992 boolean timezoneChanged; 993 if (isHomeTimezone) { 994 String previousTimezone = mCalendarCache.readTimezoneInstancesPrevious(); 995 timezoneChanged = !instancesTimezone.equals(previousTimezone); 996 } else { 997 String localTimezone = TimeZone.getDefault().getID(); 998 timezoneChanged = !instancesTimezone.equals(localTimezone); 999 // if we're in auto make sure we are using the device time zone 1000 if (timezoneChanged) { 1001 instancesTimezone = localTimezone; 1002 } 1003 } 1004 // if "home", then timezoneChanged only if current != previous 1005 // if "auto", then timezoneChanged, if !instancesTimezone.equals(localTimezone); 1006 if (maxInstance == 0 || timezoneChanged || forceExpansion) { 1007 // Empty the Instances table and expand from scratch. 1008 mDb.execSQL("DELETE FROM Instances;"); 1009 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1010 Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances," 1011 + " timezone changed: " + timezoneChanged); 1012 } 1013 expandInstanceRangeLocked(expandBegin, expandEnd, instancesTimezone); 1014 1015 mMetaData.writeLocked(instancesTimezone, expandBegin, expandEnd); 1016 1017 String timezoneType = mCalendarCache.readTimezoneType(); 1018 // This may cause some double writes but guarantees the time zone in 1019 // the db and the time zone the instances are in is the same, which 1020 // future changes may affect. 1021 mCalendarCache.writeTimezoneInstances(instancesTimezone); 1022 1023 // If we're in auto check if we need to fix the previous tz value 1024 if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) { 1025 String prevTZ = mCalendarCache.readTimezoneInstancesPrevious(); 1026 if (TextUtils.equals(TIMEZONE_GMT, prevTZ)) { 1027 mCalendarCache.writeTimezoneInstancesPrevious(instancesTimezone); 1028 } 1029 } 1030 return; 1031 } 1032 1033 // If the desired range [begin, end] has already been 1034 // expanded, then simply return. The range is inclusive, that is, 1035 // events that touch either endpoint are included in the expansion. 1036 // This means that a zero-duration event that starts and ends at 1037 // the endpoint will be included. 1038 // We use [begin, end] here and not [expandBegin, expandEnd] for 1039 // checking the range because a common case is for the client to 1040 // request successive days or weeks, for example. If we checked 1041 // that the expanded range [expandBegin, expandEnd] then we would 1042 // always be expanding because there would always be one more day 1043 // or week that hasn't been expanded. 1044 if ((begin >= minInstance) && (end <= maxInstance)) { 1045 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1046 Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd 1047 + ") falls within previously expanded range."); 1048 } 1049 return; 1050 } 1051 1052 // If the requested begin point has not been expanded, then include 1053 // more events than requested in the expansion (use "expandBegin"). 1054 if (begin < minInstance) { 1055 expandInstanceRangeLocked(expandBegin, minInstance, instancesTimezone); 1056 minInstance = expandBegin; 1057 } 1058 1059 // If the requested end point has not been expanded, then include 1060 // more events than requested in the expansion (use "expandEnd"). 1061 if (end > maxInstance) { 1062 expandInstanceRangeLocked(maxInstance, expandEnd, instancesTimezone); 1063 maxInstance = expandEnd; 1064 } 1065 1066 // Update the bounds on the Instances table (timezone is the same here) 1067 mMetaData.writeLocked(instancesTimezone, minInstance, maxInstance); 1068 } 1069 1070 private static final String[] EXPAND_COLUMNS = new String[] { 1071 Events._ID, 1072 Events._SYNC_ID, 1073 Events.STATUS, 1074 Events.DTSTART, 1075 Events.DTEND, 1076 Events.EVENT_TIMEZONE, 1077 Events.RRULE, 1078 Events.RDATE, 1079 Events.EXRULE, 1080 Events.EXDATE, 1081 Events.DURATION, 1082 Events.ALL_DAY, 1083 Events.ORIGINAL_EVENT, 1084 Events.ORIGINAL_INSTANCE_TIME, 1085 Events.CALENDAR_ID, 1086 Events.DELETED 1087 }; 1088 1089 /** 1090 * Make instances for the given range. 1091 */ 1092 private void expandInstanceRangeLocked(long begin, long end, String localTimezone) { 1093 1094 if (PROFILE) { 1095 Debug.startMethodTracing("expandInstanceRangeLocked"); 1096 } 1097 1098 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1099 Log.v(TAG, "Expanding events between " + begin + " and " + end); 1100 } 1101 1102 Cursor entries = getEntries(begin, end); 1103 try { 1104 performInstanceExpansion(begin, end, localTimezone, entries); 1105 } finally { 1106 if (entries != null) { 1107 entries.close(); 1108 } 1109 } 1110 if (PROFILE) { 1111 Debug.stopMethodTracing(); 1112 } 1113 } 1114 1115 /** 1116 * Get all entries affecting the given window. 1117 * @param begin Window start (ms). 1118 * @param end Window end (ms). 1119 * @return Cursor for the entries; caller must close it. 1120 */ 1121 private Cursor getEntries(long begin, long end) { 1122 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1123 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 1124 qb.setProjectionMap(sEventsProjectionMap); 1125 1126 String beginString = String.valueOf(begin); 1127 String endString = String.valueOf(end); 1128 1129 // grab recurrence exceptions that fall outside our expansion window but modify 1130 // recurrences that do fall within our window. we won't insert these into the output 1131 // set of instances, but instead will just add them to our cancellations list, so we 1132 // can cancel the correct recurrence expansion instances. 1133 // we don't have originalInstanceDuration or end time. for now, assume the original 1134 // instance lasts no longer than 1 week. 1135 // also filter with syncable state (we dont want the entries from a non syncable account) 1136 // TODO: compute the originalInstanceEndTime or get this from the server. 1137 qb.appendWhere("((dtstart <= ? AND (lastDate IS NULL OR lastDate >= ?)) OR " + 1138 "(originalInstanceTime IS NOT NULL AND originalInstanceTime <= ? AND " + 1139 "originalInstanceTime >= ?)) AND (sync_events != 0)"); 1140 String selectionArgs[] = new String[] {endString, beginString, endString, 1141 String.valueOf(begin - MAX_ASSUMED_DURATION)}; 1142 Cursor c = qb.query(mDb, EXPAND_COLUMNS, null /* selection */, 1143 selectionArgs, null /* groupBy */, 1144 null /* having */, null /* sortOrder */); 1145 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1146 Log.v(TAG, "Instance expansion: got " + c.getCount() + " entries"); 1147 } 1148 return c; 1149 } 1150 1151 /** 1152 * Generates a unique key from the syncId and calendarId. 1153 * The purpose of this is to prevent collisions if two different calendars use the 1154 * same sync id. This can happen if a Google calendar is accessed by two different accounts, 1155 * or with Exchange, where ids are not unique between calendars. 1156 * @param syncId Id for the event 1157 * @param calendarId Id for the calendar 1158 * @return key 1159 */ 1160 private String getSyncIdKey(String syncId, long calendarId) { 1161 return calendarId + ":" + syncId; 1162 } 1163 1164 /** 1165 * Perform instance expansion on the given entries. 1166 * @param begin Window start (ms). 1167 * @param end Window end (ms). 1168 * @param localTimezone 1169 * @param entries The entries to process. 1170 */ 1171 private void performInstanceExpansion(long begin, long end, String localTimezone, 1172 Cursor entries) { 1173 RecurrenceProcessor rp = new RecurrenceProcessor(); 1174 1175 // Key into the instance values to hold the original event concatenated with calendar id. 1176 final String ORIGINAL_EVENT_AND_CALENDAR = "ORIGINAL_EVENT_AND_CALENDAR"; 1177 1178 int statusColumn = entries.getColumnIndex(Events.STATUS); 1179 int dtstartColumn = entries.getColumnIndex(Events.DTSTART); 1180 int dtendColumn = entries.getColumnIndex(Events.DTEND); 1181 int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE); 1182 int durationColumn = entries.getColumnIndex(Events.DURATION); 1183 int rruleColumn = entries.getColumnIndex(Events.RRULE); 1184 int rdateColumn = entries.getColumnIndex(Events.RDATE); 1185 int exruleColumn = entries.getColumnIndex(Events.EXRULE); 1186 int exdateColumn = entries.getColumnIndex(Events.EXDATE); 1187 int allDayColumn = entries.getColumnIndex(Events.ALL_DAY); 1188 int idColumn = entries.getColumnIndex(Events._ID); 1189 int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID); 1190 int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT); 1191 int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME); 1192 int calendarIdColumn = entries.getColumnIndex(Events.CALENDAR_ID); 1193 int deletedColumn = entries.getColumnIndex(Events.DELETED); 1194 1195 ContentValues initialValues; 1196 EventInstancesMap instancesMap = new EventInstancesMap(); 1197 1198 Duration duration = new Duration(); 1199 Time eventTime = new Time(); 1200 1201 // Invariant: entries contains all events that affect the current 1202 // window. It consists of: 1203 // a) Individual events that fall in the window. These will be 1204 // displayed. 1205 // b) Recurrences that included the window. These will be displayed 1206 // if not canceled. 1207 // c) Recurrence exceptions that fall in the window. These will be 1208 // displayed if not cancellations. 1209 // d) Recurrence exceptions that modify an instance inside the 1210 // window (subject to 1 week assumption above), but are outside 1211 // the window. These will not be displayed. Cases c and d are 1212 // distingushed by the start / end time. 1213 1214 while (entries.moveToNext()) { 1215 try { 1216 initialValues = null; 1217 1218 boolean allDay = entries.getInt(allDayColumn) != 0; 1219 1220 String eventTimezone = entries.getString(eventTimezoneColumn); 1221 if (allDay || TextUtils.isEmpty(eventTimezone)) { 1222 // in the events table, allDay events start at midnight. 1223 // this forces them to stay at midnight for all day events 1224 // TODO: check that this actually does the right thing. 1225 eventTimezone = Time.TIMEZONE_UTC; 1226 } 1227 1228 long dtstartMillis = entries.getLong(dtstartColumn); 1229 Long eventId = Long.valueOf(entries.getLong(idColumn)); 1230 1231 String durationStr = entries.getString(durationColumn); 1232 if (durationStr != null) { 1233 try { 1234 duration.parse(durationStr); 1235 } 1236 catch (DateException e) { 1237 if (Log.isLoggable(TAG, Log.WARN)) { 1238 Log.w(TAG, "error parsing duration for event " 1239 + eventId + "'" + durationStr + "'", e); 1240 } 1241 duration.sign = 1; 1242 duration.weeks = 0; 1243 duration.days = 0; 1244 duration.hours = 0; 1245 duration.minutes = 0; 1246 duration.seconds = 0; 1247 durationStr = "+P0S"; 1248 } 1249 } 1250 1251 String syncId = entries.getString(syncIdColumn); 1252 String originalEvent = entries.getString(originalEventColumn); 1253 1254 long originalInstanceTimeMillis = -1; 1255 if (!entries.isNull(originalInstanceTimeColumn)) { 1256 originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn); 1257 } 1258 int status = entries.getInt(statusColumn); 1259 boolean deleted = (entries.getInt(deletedColumn) != 0); 1260 1261 String rruleStr = entries.getString(rruleColumn); 1262 String rdateStr = entries.getString(rdateColumn); 1263 String exruleStr = entries.getString(exruleColumn); 1264 String exdateStr = entries.getString(exdateColumn); 1265 long calendarId = entries.getLong(calendarIdColumn); 1266 String syncIdKey = getSyncIdKey(syncId, calendarId); // key into instancesMap 1267 1268 RecurrenceSet recur = null; 1269 try { 1270 recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr); 1271 } catch (EventRecurrence.InvalidFormatException e) { 1272 if (Log.isLoggable(TAG, Log.WARN)) { 1273 Log.w(TAG, "Could not parse RRULE recurrence string: " + rruleStr, e); 1274 } 1275 continue; 1276 } 1277 1278 if (null != recur && recur.hasRecurrence()) { 1279 // the event is repeating 1280 1281 if (status == Events.STATUS_CANCELED) { 1282 // should not happen! 1283 if (Log.isLoggable(TAG, Log.ERROR)) { 1284 Log.e(TAG, "Found canceled recurring event in " 1285 + "Events table. Ignoring."); 1286 } 1287 continue; 1288 } 1289 1290 if (deleted) { 1291 if (Log.isLoggable(TAG, Log.DEBUG)) { 1292 Log.d(TAG, "Found deleted recurring event in " 1293 + "Events table. Ignoring."); 1294 } 1295 continue; 1296 } 1297 1298 // need to parse the event into a local calendar. 1299 eventTime.timezone = eventTimezone; 1300 eventTime.set(dtstartMillis); 1301 eventTime.allDay = allDay; 1302 1303 if (durationStr == null) { 1304 // should not happen. 1305 if (Log.isLoggable(TAG, Log.ERROR)) { 1306 Log.e(TAG, "Repeating event has no duration -- " 1307 + "should not happen."); 1308 } 1309 if (allDay) { 1310 // set to one day. 1311 duration.sign = 1; 1312 duration.weeks = 0; 1313 duration.days = 1; 1314 duration.hours = 0; 1315 duration.minutes = 0; 1316 duration.seconds = 0; 1317 durationStr = "+P1D"; 1318 } else { 1319 // compute the duration from dtend, if we can. 1320 // otherwise, use 0s. 1321 duration.sign = 1; 1322 duration.weeks = 0; 1323 duration.days = 0; 1324 duration.hours = 0; 1325 duration.minutes = 0; 1326 if (!entries.isNull(dtendColumn)) { 1327 long dtendMillis = entries.getLong(dtendColumn); 1328 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000); 1329 durationStr = "+P" + duration.seconds + "S"; 1330 } else { 1331 duration.seconds = 0; 1332 durationStr = "+P0S"; 1333 } 1334 } 1335 } 1336 1337 long[] dates; 1338 dates = rp.expand(eventTime, recur, begin, end); 1339 1340 // Initialize the "eventTime" timezone outside the loop. 1341 // This is used in computeTimezoneDependentFields(). 1342 if (allDay) { 1343 eventTime.timezone = Time.TIMEZONE_UTC; 1344 } else { 1345 eventTime.timezone = localTimezone; 1346 } 1347 1348 long durationMillis = duration.getMillis(); 1349 for (long date : dates) { 1350 initialValues = new ContentValues(); 1351 initialValues.put(Instances.EVENT_ID, eventId); 1352 1353 initialValues.put(Instances.BEGIN, date); 1354 long dtendMillis = date + durationMillis; 1355 initialValues.put(Instances.END, dtendMillis); 1356 1357 computeTimezoneDependentFields(date, dtendMillis, 1358 eventTime, initialValues); 1359 instancesMap.add(syncIdKey, initialValues); 1360 } 1361 } else { 1362 // the event is not repeating 1363 initialValues = new ContentValues(); 1364 1365 // if this event has an "original" field, then record 1366 // that we need to cancel the original event (we can't 1367 // do that here because the order of this loop isn't 1368 // defined) 1369 if (originalEvent != null && originalInstanceTimeMillis != -1) { 1370 // The ORIGINAL_EVENT_AND_CALENDAR holds the 1371 // calendar id concatenated with the ORIGINAL_EVENT to form 1372 // a unique key, matching the keys for instancesMap. 1373 initialValues.put(ORIGINAL_EVENT_AND_CALENDAR, 1374 getSyncIdKey(originalEvent, calendarId)); 1375 initialValues.put(Events.ORIGINAL_INSTANCE_TIME, 1376 originalInstanceTimeMillis); 1377 initialValues.put(Events.STATUS, status); 1378 } 1379 1380 long dtendMillis = dtstartMillis; 1381 if (durationStr == null) { 1382 if (!entries.isNull(dtendColumn)) { 1383 dtendMillis = entries.getLong(dtendColumn); 1384 } 1385 } else { 1386 dtendMillis = duration.addTo(dtstartMillis); 1387 } 1388 1389 // this non-recurring event might be a recurrence exception that doesn't 1390 // actually fall within our expansion window, but instead was selected 1391 // so we can correctly cancel expanded recurrence instances below. do not 1392 // add events to the instances map if they don't actually fall within our 1393 // expansion window. 1394 if ((dtendMillis < begin) || (dtstartMillis > end)) { 1395 if (originalEvent != null && originalInstanceTimeMillis != -1) { 1396 initialValues.put(Events.STATUS, Events.STATUS_CANCELED); 1397 } else { 1398 if (Log.isLoggable(TAG, Log.WARN)) { 1399 Log.w(TAG, "Unexpected event outside window: " + syncId); 1400 } 1401 continue; 1402 } 1403 } 1404 1405 initialValues.put(Instances.EVENT_ID, eventId); 1406 1407 initialValues.put(Instances.BEGIN, dtstartMillis); 1408 initialValues.put(Instances.END, dtendMillis); 1409 1410 // we temporarily store the DELETED status (will be cleaned later) 1411 initialValues.put(Events.DELETED, deleted); 1412 1413 if (allDay) { 1414 eventTime.timezone = Time.TIMEZONE_UTC; 1415 } else { 1416 eventTime.timezone = localTimezone; 1417 } 1418 computeTimezoneDependentFields(dtstartMillis, dtendMillis, 1419 eventTime, initialValues); 1420 1421 instancesMap.add(syncIdKey, initialValues); 1422 } 1423 } catch (DateException e) { 1424 if (Log.isLoggable(TAG, Log.WARN)) { 1425 Log.w(TAG, "RecurrenceProcessor error ", e); 1426 } 1427 } catch (TimeFormatException e) { 1428 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1429 Log.w(TAG, "RecurrenceProcessor error ", e); 1430 } 1431 } 1432 } 1433 1434 // Invariant: instancesMap contains all instances that affect the 1435 // window, indexed by original sync id concatenated with calendar id. 1436 // It consists of: 1437 // a) Individual events that fall in the window. They have: 1438 // EVENT_ID, BEGIN, END 1439 // b) Instances of recurrences that fall in the window. They may 1440 // be subject to exceptions. They have: 1441 // EVENT_ID, BEGIN, END 1442 // c) Exceptions that fall in the window. They have: 1443 // ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS (since they can 1444 // be a modification or cancellation), EVENT_ID, BEGIN, END 1445 // d) Recurrence exceptions that modify an instance inside the 1446 // window but fall outside the window. They have: 1447 // ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS = 1448 // STATUS_CANCELED, EVENT_ID, BEGIN, END 1449 1450 // First, delete the original instances corresponding to recurrence 1451 // exceptions. We do this by iterating over the list and for each 1452 // recurrence exception, we search the list for an instance with a 1453 // matching "original instance time". If we find such an instance, 1454 // we remove it from the list. If we don't find such an instance 1455 // then we cancel the recurrence exception. 1456 Set<String> keys = instancesMap.keySet(); 1457 for (String syncIdKey : keys) { 1458 InstancesList list = instancesMap.get(syncIdKey); 1459 for (ContentValues values : list) { 1460 1461 // If this instance is not a recurrence exception, then 1462 // skip it. 1463 if (!values.containsKey(ORIGINAL_EVENT_AND_CALENDAR)) { 1464 continue; 1465 } 1466 1467 String originalEventPlusCalendar = values.getAsString(ORIGINAL_EVENT_AND_CALENDAR); 1468 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1469 InstancesList originalList = instancesMap.get(originalEventPlusCalendar); 1470 if (originalList == null) { 1471 // The original recurrence is not present, so don't try canceling it. 1472 continue; 1473 } 1474 1475 // Search the original event for a matching original 1476 // instance time. If there is a matching one, then remove 1477 // the original one. We do this both for exceptions that 1478 // change the original instance as well as for exceptions 1479 // that delete the original instance. 1480 for (int num = originalList.size() - 1; num >= 0; num--) { 1481 ContentValues originalValues = originalList.get(num); 1482 long beginTime = originalValues.getAsLong(Instances.BEGIN); 1483 if (beginTime == originalTime) { 1484 // We found the original instance, so remove it. 1485 originalList.remove(num); 1486 } 1487 } 1488 } 1489 } 1490 1491 // Invariant: instancesMap contains filtered instances. 1492 // It consists of: 1493 // a) Individual events that fall in the window. 1494 // b) Instances of recurrences that fall in the window and have not 1495 // been subject to exceptions. 1496 // c) Exceptions that fall in the window. They will have 1497 // STATUS_CANCELED if they are cancellations. 1498 // d) Recurrence exceptions that modify an instance inside the 1499 // window but fall outside the window. These are STATUS_CANCELED. 1500 1501 // Now do the inserts. Since the db lock is held when this method is executed, 1502 // this will be done in a transaction. 1503 // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db 1504 // while the calendar app is trying to query the db (expanding instances)), we will 1505 // not be "polite" and yield the lock until we're done. This will favor local query 1506 // operations over sync/write operations. 1507 for (String syncIdKey : keys) { 1508 InstancesList list = instancesMap.get(syncIdKey); 1509 for (ContentValues values : list) { 1510 1511 // If this instance was cancelled or deleted then don't create a new 1512 // instance. 1513 Integer status = values.getAsInteger(Events.STATUS); 1514 boolean deleted = values.containsKey(Events.DELETED) ? 1515 values.getAsBoolean(Events.DELETED) : false; 1516 if ((status != null && status == Events.STATUS_CANCELED) || deleted) { 1517 continue; 1518 } 1519 1520 // We remove this useless key (not valid in the context of Instances table) 1521 values.remove(Events.DELETED); 1522 1523 // Remove these fields before inserting a new instance 1524 values.remove(ORIGINAL_EVENT_AND_CALENDAR); 1525 values.remove(Events.ORIGINAL_INSTANCE_TIME); 1526 values.remove(Events.STATUS); 1527 1528 mDbHelper.instancesReplace(values); 1529 } 1530 } 1531 } 1532 1533 /** 1534 * Computes the timezone-dependent fields of an instance of an event and 1535 * updates the "values" map to contain those fields. 1536 * 1537 * @param begin the start time of the instance (in UTC milliseconds) 1538 * @param end the end time of the instance (in UTC milliseconds) 1539 * @param local a Time object with the timezone set to the local timezone 1540 * @param values a map that will contain the timezone-dependent fields 1541 */ 1542 private void computeTimezoneDependentFields(long begin, long end, 1543 Time local, ContentValues values) { 1544 local.set(begin); 1545 int startDay = Time.getJulianDay(begin, local.gmtoff); 1546 int startMinute = local.hour * 60 + local.minute; 1547 1548 local.set(end); 1549 int endDay = Time.getJulianDay(end, local.gmtoff); 1550 int endMinute = local.hour * 60 + local.minute; 1551 1552 // Special case for midnight, which has endMinute == 0. Change 1553 // that to +24 hours on the previous day to make everything simpler. 1554 // Exception: if start and end minute are both 0 on the same day, 1555 // then leave endMinute alone. 1556 if (endMinute == 0 && endDay > startDay) { 1557 endMinute = 24 * 60; 1558 endDay -= 1; 1559 } 1560 1561 values.put(Instances.START_DAY, startDay); 1562 values.put(Instances.END_DAY, endDay); 1563 values.put(Instances.START_MINUTE, startMinute); 1564 values.put(Instances.END_MINUTE, endMinute); 1565 } 1566 1567 @Override 1568 public String getType(Uri url) { 1569 int match = sUriMatcher.match(url); 1570 switch (match) { 1571 case EVENTS: 1572 return "vnd.android.cursor.dir/event"; 1573 case EVENTS_ID: 1574 return "vnd.android.cursor.item/event"; 1575 case REMINDERS: 1576 return "vnd.android.cursor.dir/reminder"; 1577 case REMINDERS_ID: 1578 return "vnd.android.cursor.item/reminder"; 1579 case CALENDAR_ALERTS: 1580 return "vnd.android.cursor.dir/calendar-alert"; 1581 case CALENDAR_ALERTS_BY_INSTANCE: 1582 return "vnd.android.cursor.dir/calendar-alert-by-instance"; 1583 case CALENDAR_ALERTS_ID: 1584 return "vnd.android.cursor.item/calendar-alert"; 1585 case INSTANCES: 1586 case INSTANCES_BY_DAY: 1587 case EVENT_DAYS: 1588 return "vnd.android.cursor.dir/event-instance"; 1589 case TIME: 1590 return "time/epoch"; 1591 case PROVIDER_PROPERTIES: 1592 return "vnd.android.cursor.dir/property"; 1593 default: 1594 throw new IllegalArgumentException("Unknown URL " + url); 1595 } 1596 } 1597 1598 public static boolean isRecurrenceEvent(String rrule, String rdate, String originalEvent) { 1599 return (!TextUtils.isEmpty(rrule)|| 1600 !TextUtils.isEmpty(rdate)|| 1601 !TextUtils.isEmpty(originalEvent)); 1602 } 1603 1604 /** 1605 * Takes an event and corrects the hrs, mins, secs if it is an allDay event. 1606 * 1607 * AllDay events should have hrs, mins, secs set to zero. This checks if this is true and 1608 * corrects the fields DTSTART, DTEND, and DURATION if necessary. Also checks to ensure that 1609 * either both DTSTART and DTEND or DTSTART and DURATION are set for each event. 1610 * 1611 * @param updatedValues The values to check and correct 1612 * @return Returns true if a correction was necessary, false otherwise 1613 */ 1614 private boolean fixAllDayTime(Uri uri, ContentValues updatedValues) { 1615 boolean neededCorrection = false; 1616 if (updatedValues.containsKey(Events.ALL_DAY) 1617 && updatedValues.getAsInteger(Events.ALL_DAY).intValue() == 1) { 1618 Long dtstart = updatedValues.getAsLong(Events.DTSTART); 1619 Long dtend = updatedValues.getAsLong(Events.DTEND); 1620 String duration = updatedValues.getAsString(Events.DURATION); 1621 Time time = new Time(); 1622 Cursor currentTimesCursor = null; 1623 String tempValue; 1624 // If a complete set of time fields doesn't exist query the db for them. A complete set 1625 // is dtstart and dtend for non-recurring events or dtstart and duration for recurring 1626 // events. 1627 if(dtstart == null || (dtend == null && duration == null)) { 1628 // Make sure we have an id to search for, if not this is probably a new event 1629 if (uri.getPathSegments().size() == 2) { 1630 currentTimesCursor = query(uri, 1631 ALLDAY_TIME_PROJECTION, 1632 null /* selection */, 1633 null /* selectionArgs */, 1634 null /* sort */); 1635 if (currentTimesCursor != null) { 1636 if (!currentTimesCursor.moveToFirst() || 1637 currentTimesCursor.getCount() != 1) { 1638 // Either this is a new event or the query is too general to get data 1639 // from the db. In either case don't try to use the query and catch 1640 // errors when trying to update the time fields. 1641 currentTimesCursor.close(); 1642 currentTimesCursor = null; 1643 } 1644 } 1645 } 1646 } 1647 1648 // Ensure dtstart exists for this event (always required) and set so h,m,s are 0 if 1649 // necessary. 1650 // TODO Move this somewhere to check all events, not just allDay events. 1651 if (dtstart == null) { 1652 if (currentTimesCursor != null) { 1653 // getLong returns 0 for empty fields, we'd like to know if a field is empty 1654 // so getString is used instead. 1655 tempValue = currentTimesCursor.getString(ALLDAY_DTSTART_INDEX); 1656 try { 1657 dtstart = Long.valueOf(tempValue); 1658 } catch (NumberFormatException e) { 1659 currentTimesCursor.close(); 1660 throw new IllegalArgumentException("Event has no DTSTART field, the db " + 1661 "may be damaged. Set DTSTART for this event to fix."); 1662 } 1663 } else { 1664 throw new IllegalArgumentException("DTSTART cannot be empty for new events."); 1665 } 1666 } 1667 time.clear(Time.TIMEZONE_UTC); 1668 time.set(dtstart.longValue()); 1669 if (time.hour != 0 || time.minute != 0 || time.second != 0) { 1670 time.hour = 0; 1671 time.minute = 0; 1672 time.second = 0; 1673 updatedValues.put(Events.DTSTART, time.toMillis(true)); 1674 neededCorrection = true; 1675 } 1676 1677 // If dtend exists for this event make sure it's h,m,s are 0. 1678 if (dtend == null && currentTimesCursor != null) { 1679 // getLong returns 0 for empty fields. We'd like to know if a field is empty 1680 // so getString is used instead. 1681 tempValue = currentTimesCursor.getString(ALLDAY_DTEND_INDEX); 1682 try { 1683 dtend = Long.valueOf(tempValue); 1684 } catch (NumberFormatException e) { 1685 dtend = null; 1686 } 1687 } 1688 if (dtend != null) { 1689 time.clear(Time.TIMEZONE_UTC); 1690 time.set(dtend.longValue()); 1691 if (time.hour != 0 || time.minute != 0 || time.second != 0) { 1692 time.hour = 0; 1693 time.minute = 0; 1694 time.second = 0; 1695 dtend = time.toMillis(true); 1696 updatedValues.put(Events.DTEND, dtend); 1697 neededCorrection = true; 1698 } 1699 } 1700 1701 if (currentTimesCursor != null) { 1702 if (duration == null) { 1703 duration = currentTimesCursor.getString(ALLDAY_DURATION_INDEX); 1704 } 1705 currentTimesCursor.close(); 1706 } 1707 1708 if (duration != null) { 1709 int len = duration.length(); 1710 /* duration is stored as either "P<seconds>S" or "P<days>D". This checks if it's 1711 * in the seconds format, and if so converts it to days. 1712 */ 1713 if (len == 0) { 1714 duration = null; 1715 } else if (duration.charAt(0) == 'P' && 1716 duration.charAt(len - 1) == 'S') { 1717 int seconds = Integer.parseInt(duration.substring(1, len - 1)); 1718 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS; 1719 duration = "P" + days + "D"; 1720 updatedValues.put(Events.DURATION, duration); 1721 neededCorrection = true; 1722 } else if (duration.charAt(0) != 'P' || 1723 duration.charAt(len - 1) != 'D') { 1724 throw new IllegalArgumentException("duration is not formatted correctly. " + 1725 "Should be 'P<seconds>S' or 'P<days>D'."); 1726 } 1727 } 1728 1729 if (duration == null && dtend == null) { 1730 throw new IllegalArgumentException("DTEND and DURATION cannot both be null for " + 1731 "an event."); 1732 } 1733 } 1734 return neededCorrection; 1735 } 1736 1737 @Override 1738 protected Uri insertInTransaction(Uri uri, ContentValues values) { 1739 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1740 Log.v(TAG, "insertInTransaction: " + uri); 1741 } 1742 1743 final boolean callerIsSyncAdapter = 1744 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false); 1745 1746 final int match = sUriMatcher.match(uri); 1747 long id = 0; 1748 1749 switch (match) { 1750 case SYNCSTATE: 1751 id = mDbHelper.getSyncState().insert(mDb, values); 1752 break; 1753 case EVENTS: 1754 if (!callerIsSyncAdapter) { 1755 values.put(Events._SYNC_DIRTY, 1); 1756 } 1757 if (!values.containsKey(Events.DTSTART)) { 1758 throw new RuntimeException("DTSTART field missing from event"); 1759 } 1760 // TODO: do we really need to make a copy? 1761 ContentValues updatedValues = new ContentValues(values); 1762 validateEventData(updatedValues); 1763 // updateLastDate must be after validation, to ensure proper last date computation 1764 updatedValues = updateLastDate(updatedValues); 1765 if (updatedValues == null) { 1766 throw new RuntimeException("Could not insert event."); 1767 // return null; 1768 } 1769 String owner = null; 1770 if (updatedValues.containsKey(Events.CALENDAR_ID) && 1771 !updatedValues.containsKey(Events.ORGANIZER)) { 1772 owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID)); 1773 // TODO: This isn't entirely correct. If a guest is adding a recurrence 1774 // exception to an event, the organizer should stay the original organizer. 1775 // This value doesn't go to the server and it will get fixed on sync, 1776 // so it shouldn't really matter. 1777 if (owner != null) { 1778 updatedValues.put(Events.ORGANIZER, owner); 1779 } 1780 } 1781 if (fixAllDayTime(uri, updatedValues)) { 1782 if (Log.isLoggable(TAG, Log.WARN)) { 1783 Log.w(TAG, "insertInTransaction: " + 1784 "allDay is true but sec, min, hour were not 0."); 1785 } 1786 } 1787 id = mDbHelper.eventsInsert(updatedValues); 1788 if (id != -1) { 1789 updateEventRawTimesLocked(id, updatedValues); 1790 updateInstancesLocked(updatedValues, id, true /* new event */, mDb); 1791 1792 // If we inserted a new event that specified the self-attendee 1793 // status, then we need to add an entry to the attendees table. 1794 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) { 1795 int status = values.getAsInteger(Events.SELF_ATTENDEE_STATUS); 1796 if (owner == null) { 1797 owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID)); 1798 } 1799 createAttendeeEntry(id, status, owner); 1800 } 1801 // if the Event Timezone is defined, store it as the original one in the 1802 // ExtendedProperties table 1803 if (values.containsKey(Events.EVENT_TIMEZONE) && !callerIsSyncAdapter) { 1804 String originalTimezone = values.getAsString(Events.EVENT_TIMEZONE); 1805 1806 ContentValues expropsValues = new ContentValues(); 1807 expropsValues.put(Calendar.ExtendedProperties.EVENT_ID, id); 1808 expropsValues.put(Calendar.ExtendedProperties.NAME, 1809 EXT_PROP_ORIGINAL_TIMEZONE); 1810 expropsValues.put(Calendar.ExtendedProperties.VALUE, originalTimezone); 1811 1812 // Insert the extended property 1813 long exPropId = mDbHelper.extendedPropertiesInsert(expropsValues); 1814 if (exPropId == -1) { 1815 if (Log.isLoggable(TAG, Log.ERROR)) { 1816 Log.e(TAG, "Cannot add the original Timezone in the " 1817 + "ExtendedProperties table for Event: " + id); 1818 } 1819 } else { 1820 // Update the Event for saying it has some extended properties 1821 ContentValues eventValues = new ContentValues(); 1822 eventValues.put(Events.HAS_EXTENDED_PROPERTIES, "1"); 1823 int result = mDb.update("Events", eventValues, "_id=?", 1824 new String[] {String.valueOf(id)}); 1825 if (result <= 0) { 1826 if (Log.isLoggable(TAG, Log.ERROR)) { 1827 Log.e(TAG, "Cannot update hasExtendedProperties column" 1828 + " for Event: " + id); 1829 } 1830 } 1831 } 1832 } 1833 triggerAppWidgetUpdate(id); 1834 } 1835 break; 1836 case CALENDARS: 1837 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS); 1838 if (syncEvents != null && syncEvents == 1) { 1839 String accountName = values.getAsString(Calendars._SYNC_ACCOUNT); 1840 String accountType = values.getAsString( 1841 Calendars._SYNC_ACCOUNT_TYPE); 1842 final Account account = new Account(accountName, accountType); 1843 String calendarUrl = values.getAsString(Calendars.URL); 1844 mDbHelper.scheduleSync(account, false /* two-way sync */, calendarUrl); 1845 } 1846 id = mDbHelper.calendarsInsert(values); 1847 break; 1848 case ATTENDEES: 1849 if (!values.containsKey(Attendees.EVENT_ID)) { 1850 throw new IllegalArgumentException("Attendees values must " 1851 + "contain an event_id"); 1852 } 1853 id = mDbHelper.attendeesInsert(values); 1854 if (!callerIsSyncAdapter) { 1855 setEventDirty(values.getAsInteger(Attendees.EVENT_ID)); 1856 } 1857 1858 // Copy the attendee status value to the Events table. 1859 updateEventAttendeeStatus(mDb, values); 1860 break; 1861 case REMINDERS: 1862 if (!values.containsKey(Reminders.EVENT_ID)) { 1863 throw new IllegalArgumentException("Reminders values must " 1864 + "contain an event_id"); 1865 } 1866 id = mDbHelper.remindersInsert(values); 1867 if (!callerIsSyncAdapter) { 1868 setEventDirty(values.getAsInteger(Reminders.EVENT_ID)); 1869 } 1870 1871 // Schedule another event alarm, if necessary 1872 if (Log.isLoggable(TAG, Log.DEBUG)) { 1873 Log.d(TAG, "insertInternal() changing reminder"); 1874 } 1875 scheduleNextAlarm(false /* do not remove alarms */); 1876 break; 1877 case CALENDAR_ALERTS: 1878 if (!values.containsKey(CalendarAlerts.EVENT_ID)) { 1879 throw new IllegalArgumentException("CalendarAlerts values must " 1880 + "contain an event_id"); 1881 } 1882 id = mDbHelper.calendarAlertsInsert(values); 1883 // Note: dirty bit is not set for Alerts because it is not synced. 1884 // It is generated from Reminders, which is synced. 1885 break; 1886 case EXTENDED_PROPERTIES: 1887 if (!values.containsKey(Calendar.ExtendedProperties.EVENT_ID)) { 1888 throw new IllegalArgumentException("ExtendedProperties values must " 1889 + "contain an event_id"); 1890 } 1891 id = mDbHelper.extendedPropertiesInsert(values); 1892 if (!callerIsSyncAdapter) { 1893 setEventDirty(values.getAsInteger(Calendar.ExtendedProperties.EVENT_ID)); 1894 } 1895 break; 1896 case DELETED_EVENTS: 1897 case EVENTS_ID: 1898 case REMINDERS_ID: 1899 case CALENDAR_ALERTS_ID: 1900 case EXTENDED_PROPERTIES_ID: 1901 case INSTANCES: 1902 case INSTANCES_BY_DAY: 1903 case EVENT_DAYS: 1904 case PROVIDER_PROPERTIES: 1905 throw new UnsupportedOperationException("Cannot insert into that URL: " + uri); 1906 default: 1907 throw new IllegalArgumentException("Unknown URL " + uri); 1908 } 1909 1910 if (id < 0) { 1911 return null; 1912 } 1913 1914 return ContentUris.withAppendedId(uri, id); 1915 } 1916 1917 /** 1918 * Do some validation on event data before inserting. 1919 * In particular make sure dtend, duration, etc make sense for 1920 * the type of event (regular, recurrence, exception). Remove 1921 * any unexpected fields. 1922 * 1923 * @param values the ContentValues to insert 1924 */ 1925 private void validateEventData(ContentValues values) { 1926 boolean hasDtend = values.getAsLong(Events.DTEND) != null; 1927 boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION)); 1928 boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE)); 1929 boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE)); 1930 boolean hasOriginalEvent = !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT)); 1931 boolean hasOriginalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME) != null; 1932 if (hasRrule || hasRdate) { 1933 // Recurrence: 1934 // dtstart is start time of first event 1935 // dtend is null 1936 // duration is the duration of the event 1937 // rrule is the recurrence rule 1938 // lastDate is the end of the last event or null if it repeats forever 1939 // originalEvent is null 1940 // originalInstanceTime is null 1941 if (hasDtend || !hasDuration || hasOriginalEvent || hasOriginalInstanceTime) { 1942 if (Log.isLoggable(TAG, Log.DEBUG)) { 1943 Log.e(TAG, "Invalid values for recurrence: " + values); 1944 } 1945 values.remove(Events.DTEND); 1946 values.remove(Events.ORIGINAL_EVENT); 1947 values.remove(Events.ORIGINAL_INSTANCE_TIME); 1948 } 1949 } else if (hasOriginalEvent || hasOriginalInstanceTime) { 1950 // Recurrence exception 1951 // dtstart is start time of exception event 1952 // dtend is end time of exception event 1953 // duration is null 1954 // rrule is null 1955 // lastdate is same as dtend 1956 // originalEvent is the _sync_id of the recurrence 1957 // originalInstanceTime is the start time of the event being replaced 1958 if (!hasDtend || hasDuration || !hasOriginalEvent || !hasOriginalInstanceTime) { 1959 if (Log.isLoggable(TAG, Log.DEBUG)) { 1960 Log.e(TAG, "Invalid values for recurrence exception: " + values); 1961 } 1962 values.remove(Events.DURATION); 1963 } 1964 } else { 1965 // Regular event 1966 // dtstart is the start time 1967 // dtend is the end time 1968 // duration is null 1969 // rrule is null 1970 // lastDate is the same as dtend 1971 // originalEvent is null 1972 // originalInstanceTime is null 1973 if (!hasDtend || hasDuration) { 1974 if (Log.isLoggable(TAG, Log.DEBUG)) { 1975 Log.e(TAG, "Invalid values for event: " + values); 1976 } 1977 values.remove(Events.DURATION); 1978 } 1979 } 1980 } 1981 1982 private void setEventDirty(int eventId) { 1983 mDb.execSQL("UPDATE Events SET _sync_dirty=1 where _id=?", new Integer[] {eventId}); 1984 } 1985 1986 /** 1987 * Gets the calendar's owner for an event. 1988 * @param calId 1989 * @return email of owner or null 1990 */ 1991 private String getOwner(long calId) { 1992 if (calId < 0) { 1993 if (Log.isLoggable(TAG, Log.ERROR)) { 1994 Log.e(TAG, "Calendar Id is not valid: " + calId); 1995 } 1996 return null; 1997 } 1998 // Get the email address of this user from this Calendar 1999 String emailAddress = null; 2000 Cursor cursor = null; 2001 try { 2002 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 2003 new String[] { Calendars.OWNER_ACCOUNT }, 2004 null /* selection */, 2005 null /* selectionArgs */, 2006 null /* sort */); 2007 if (cursor == null || !cursor.moveToFirst()) { 2008 if (Log.isLoggable(TAG, Log.DEBUG)) { 2009 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 2010 } 2011 return null; 2012 } 2013 emailAddress = cursor.getString(0); 2014 } finally { 2015 if (cursor != null) { 2016 cursor.close(); 2017 } 2018 } 2019 return emailAddress; 2020 } 2021 2022 /** 2023 * Creates an entry in the Attendees table that refers to the given event 2024 * and that has the given response status. 2025 * 2026 * @param eventId the event id that the new entry in the Attendees table 2027 * should refer to 2028 * @param status the response status 2029 * @param emailAddress the email of the attendee 2030 */ 2031 private void createAttendeeEntry(long eventId, int status, String emailAddress) { 2032 ContentValues values = new ContentValues(); 2033 values.put(Attendees.EVENT_ID, eventId); 2034 values.put(Attendees.ATTENDEE_STATUS, status); 2035 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 2036 // TODO: The relationship could actually be ORGANIZER, but it will get straightened out 2037 // on sync. 2038 values.put(Attendees.ATTENDEE_RELATIONSHIP, 2039 Attendees.RELATIONSHIP_ATTENDEE); 2040 values.put(Attendees.ATTENDEE_EMAIL, emailAddress); 2041 2042 // We don't know the ATTENDEE_NAME but that will be filled in by the 2043 // server and sent back to us. 2044 mDbHelper.attendeesInsert(values); 2045 } 2046 2047 /** 2048 * Updates the attendee status in the Events table to be consistent with 2049 * the value in the Attendees table. 2050 * 2051 * @param db the database 2052 * @param attendeeValues the column values for one row in the Attendees 2053 * table. 2054 */ 2055 private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) { 2056 // Get the event id for this attendee 2057 long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID); 2058 2059 if (MULTIPLE_ATTENDEES_PER_EVENT) { 2060 // Get the calendar id for this event 2061 Cursor cursor = null; 2062 long calId; 2063 try { 2064 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 2065 new String[] { Events.CALENDAR_ID }, 2066 null /* selection */, 2067 null /* selectionArgs */, 2068 null /* sort */); 2069 if (cursor == null || !cursor.moveToFirst()) { 2070 if (Log.isLoggable(TAG, Log.DEBUG)) { 2071 Log.d(TAG, "Couldn't find " + eventId + " in Events table"); 2072 } 2073 return; 2074 } 2075 calId = cursor.getLong(0); 2076 } finally { 2077 if (cursor != null) { 2078 cursor.close(); 2079 } 2080 } 2081 2082 // Get the owner email for this Calendar 2083 String calendarEmail = null; 2084 cursor = null; 2085 try { 2086 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 2087 new String[] { Calendars.OWNER_ACCOUNT }, 2088 null /* selection */, 2089 null /* selectionArgs */, 2090 null /* sort */); 2091 if (cursor == null || !cursor.moveToFirst()) { 2092 if (Log.isLoggable(TAG, Log.DEBUG)) { 2093 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 2094 } 2095 return; 2096 } 2097 calendarEmail = cursor.getString(0); 2098 } finally { 2099 if (cursor != null) { 2100 cursor.close(); 2101 } 2102 } 2103 2104 if (calendarEmail == null) { 2105 return; 2106 } 2107 2108 // Get the email address for this attendee 2109 String attendeeEmail = null; 2110 if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 2111 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL); 2112 } 2113 2114 // If the attendee email does not match the calendar email, then this 2115 // attendee is not the owner of this calendar so we don't update the 2116 // selfAttendeeStatus in the event. 2117 if (!calendarEmail.equals(attendeeEmail)) { 2118 return; 2119 } 2120 } 2121 2122 int status = Attendees.ATTENDEE_STATUS_NONE; 2123 if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) { 2124 int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 2125 if (rel == Attendees.RELATIONSHIP_ORGANIZER) { 2126 status = Attendees.ATTENDEE_STATUS_ACCEPTED; 2127 } 2128 } 2129 2130 if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) { 2131 status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS); 2132 } 2133 2134 ContentValues values = new ContentValues(); 2135 values.put(Events.SELF_ATTENDEE_STATUS, status); 2136 db.update("Events", values, "_id=?", new String[] {String.valueOf(eventId)}); 2137 } 2138 2139 /** 2140 * Updates the instances table when an event is added or updated. 2141 * @param values The new values of the event. 2142 * @param rowId The database row id of the event. 2143 * @param newEvent true if the event is new. 2144 * @param db The database 2145 */ 2146 private void updateInstancesLocked(ContentValues values, 2147 long rowId, 2148 boolean newEvent, 2149 SQLiteDatabase db) { 2150 2151 // If there are no expanded Instances, then return. 2152 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2153 if (fields.maxInstance == 0) { 2154 return; 2155 } 2156 2157 Long dtstartMillis = values.getAsLong(Events.DTSTART); 2158 if (dtstartMillis == null) { 2159 if (newEvent) { 2160 // must be present for a new event. 2161 throw new RuntimeException("DTSTART missing."); 2162 } 2163 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2164 Log.v(TAG, "Missing DTSTART. No need to update instance."); 2165 } 2166 return; 2167 } 2168 2169 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 2170 Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 2171 2172 if (!newEvent) { 2173 // Want to do this for regular event, recurrence, or exception. 2174 // For recurrence or exception, more deletion may happen below if we 2175 // do an instance expansion. This deletion will suffice if the exception 2176 // is moved outside the window, for instance. 2177 db.delete("Instances", "event_id=?", new String[] {String.valueOf(rowId)}); 2178 } 2179 2180 String rrule = values.getAsString(Events.RRULE); 2181 String rdate = values.getAsString(Events.RDATE); 2182 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 2183 if (isRecurrenceEvent(rrule, rdate, originalEvent)) { 2184 // The recurrence or exception needs to be (re-)expanded if: 2185 // a) Exception or recurrence that falls inside window 2186 boolean insideWindow = dtstartMillis <= fields.maxInstance && 2187 (lastDateMillis == null || lastDateMillis >= fields.minInstance); 2188 // b) Exception that affects instance inside window 2189 // These conditions match the query in getEntries 2190 // See getEntries comment for explanation of subtracting 1 week. 2191 boolean affectsWindow = originalInstanceTime != null && 2192 originalInstanceTime <= fields.maxInstance && 2193 originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION; 2194 if (insideWindow || affectsWindow) { 2195 updateRecurrenceInstancesLocked(values, rowId, db); 2196 } 2197 // TODO: an exception creation or update could be optimized by 2198 // updating just the affected instances, instead of regenerating 2199 // the recurrence. 2200 return; 2201 } 2202 2203 Long dtendMillis = values.getAsLong(Events.DTEND); 2204 if (dtendMillis == null) { 2205 dtendMillis = dtstartMillis; 2206 } 2207 2208 // if the event is in the expanded range, insert 2209 // into the instances table. 2210 // TODO: deal with durations. currently, durations are only used in 2211 // recurrences. 2212 2213 if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) { 2214 ContentValues instanceValues = new ContentValues(); 2215 instanceValues.put(Instances.EVENT_ID, rowId); 2216 instanceValues.put(Instances.BEGIN, dtstartMillis); 2217 instanceValues.put(Instances.END, dtendMillis); 2218 2219 boolean allDay = false; 2220 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2221 if (allDayInteger != null) { 2222 allDay = allDayInteger != 0; 2223 } 2224 2225 // Update the timezone-dependent fields. 2226 Time local = new Time(); 2227 if (allDay) { 2228 local.timezone = Time.TIMEZONE_UTC; 2229 } else { 2230 local.timezone = fields.timezone; 2231 } 2232 2233 computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues); 2234 mDbHelper.instancesInsert(instanceValues); 2235 } 2236 } 2237 2238 /** 2239 * Determines the recurrence entries associated with a particular recurrence. 2240 * This set is the base recurrence and any exception. 2241 * 2242 * Normally the entries are indicated by the sync id of the base recurrence 2243 * (which is the originalEvent in the exceptions). 2244 * However, a complication is that a recurrence may not yet have a sync id. 2245 * In that case, the recurrence is specified by the rowId. 2246 * 2247 * @param recurrenceSyncId The sync id of the base recurrence, or null. 2248 * @param rowId The row id of the base recurrence. 2249 * @return the relevant entries. 2250 */ 2251 private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) { 2252 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2253 2254 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 2255 qb.setProjectionMap(sEventsProjectionMap); 2256 String selectionArgs[]; 2257 if (recurrenceSyncId == null) { 2258 String where = "_id =?"; 2259 qb.appendWhere(where); 2260 selectionArgs = new String[] {String.valueOf(rowId)}; 2261 } else { 2262 String where = "_sync_id = ? OR originalEvent = ?"; 2263 qb.appendWhere(where); 2264 selectionArgs = new String[] {recurrenceSyncId, recurrenceSyncId}; 2265 } 2266 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2267 Log.v(TAG, "Retrieving events to expand: " + qb.toString()); 2268 } 2269 2270 return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs, 2271 null /* groupBy */, null /* having */, null /* sortOrder */); 2272 } 2273 2274 /** 2275 * Do incremental Instances update of a recurrence or recurrence exception. 2276 * 2277 * This method does performInstanceExpansion on just the modified recurrence, 2278 * to avoid the overhead of recomputing the entire instance table. 2279 * 2280 * @param values The new values of the event. 2281 * @param rowId The database row id of the event. 2282 * @param db The database 2283 */ 2284 private void updateRecurrenceInstancesLocked(ContentValues values, 2285 long rowId, 2286 SQLiteDatabase db) { 2287 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2288 String instancesTimezone = mCalendarCache.readTimezoneInstances(); 2289 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 2290 String recurrenceSyncId; 2291 if (originalEvent != null) { 2292 recurrenceSyncId = originalEvent; 2293 } else { 2294 // Get the recurrence's sync id from the database 2295 recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events" 2296 + " WHERE _id=?", new String[] {String.valueOf(rowId)}); 2297 } 2298 // recurrenceSyncId is the _sync_id of the underlying recurrence 2299 // If the recurrence hasn't gone to the server, it will be null. 2300 2301 // Need to clear out old instances 2302 if (recurrenceSyncId == null) { 2303 // Creating updating a recurrence that hasn't gone to the server. 2304 // Need to delete based on row id 2305 String where = "_id IN (SELECT Instances._id as _id" 2306 + " FROM Instances INNER JOIN Events" 2307 + " ON (Events._id = Instances.event_id)" 2308 + " WHERE Events._id =?)"; 2309 db.delete("Instances", where, new String[]{"" + rowId}); 2310 } else { 2311 // Creating or modifying a recurrence or exception. 2312 // Delete instances for recurrence (_sync_id = recurrenceSyncId) 2313 // and all exceptions (originalEvent = recurrenceSyncId) 2314 String where = "_id IN (SELECT Instances._id as _id" 2315 + " FROM Instances INNER JOIN Events" 2316 + " ON (Events._id = Instances.event_id)" 2317 + " WHERE Events._sync_id =?" 2318 + " OR Events.originalEvent =?)"; 2319 db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId}); 2320 } 2321 2322 // Now do instance expansion 2323 Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId); 2324 try { 2325 performInstanceExpansion(fields.minInstance, fields.maxInstance, instancesTimezone, 2326 entries); 2327 } finally { 2328 if (entries != null) { 2329 entries.close(); 2330 } 2331 } 2332 } 2333 2334 long calculateLastDate(ContentValues values) 2335 throws DateException { 2336 // Allow updates to some event fields like the title or hasAlarm 2337 // without requiring DTSTART. 2338 if (!values.containsKey(Events.DTSTART)) { 2339 if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE) 2340 || values.containsKey(Events.DURATION) 2341 || values.containsKey(Events.EVENT_TIMEZONE) 2342 || values.containsKey(Events.RDATE) 2343 || values.containsKey(Events.EXRULE) 2344 || values.containsKey(Events.EXDATE)) { 2345 throw new RuntimeException("DTSTART field missing from event"); 2346 } 2347 return -1; 2348 } 2349 long dtstartMillis = values.getAsLong(Events.DTSTART); 2350 long lastMillis = -1; 2351 2352 // Can we use dtend with a repeating event? What does that even 2353 // mean? 2354 // NOTE: if the repeating event has a dtend, we convert it to a 2355 // duration during event processing, so this situation should not 2356 // occur. 2357 Long dtEnd = values.getAsLong(Events.DTEND); 2358 if (dtEnd != null) { 2359 lastMillis = dtEnd; 2360 } else { 2361 // find out how long it is 2362 Duration duration = new Duration(); 2363 String durationStr = values.getAsString(Events.DURATION); 2364 if (durationStr != null) { 2365 duration.parse(durationStr); 2366 } 2367 2368 RecurrenceSet recur = null; 2369 try { 2370 recur = new RecurrenceSet(values); 2371 } catch (EventRecurrence.InvalidFormatException e) { 2372 if (Log.isLoggable(TAG, Log.WARN)) { 2373 Log.w(TAG, "Could not parse RRULE recurrence string: " + 2374 values.get(Calendar.Events.RRULE), e); 2375 } 2376 return lastMillis; // -1 2377 } 2378 2379 if (null != recur && recur.hasRecurrence()) { 2380 // the event is repeating, so find the last date it 2381 // could appear on 2382 2383 String tz = values.getAsString(Events.EVENT_TIMEZONE); 2384 2385 if (TextUtils.isEmpty(tz)) { 2386 // floating timezone 2387 tz = Time.TIMEZONE_UTC; 2388 } 2389 Time dtstartLocal = new Time(tz); 2390 2391 dtstartLocal.set(dtstartMillis); 2392 2393 RecurrenceProcessor rp = new RecurrenceProcessor(); 2394 lastMillis = rp.getLastOccurence(dtstartLocal, recur); 2395 if (lastMillis == -1) { 2396 return lastMillis; // -1 2397 } 2398 } else { 2399 // the event is not repeating, just use dtstartMillis 2400 lastMillis = dtstartMillis; 2401 } 2402 2403 // that was the beginning of the event. this is the end. 2404 lastMillis = duration.addTo(lastMillis); 2405 } 2406 return lastMillis; 2407 } 2408 2409 /** 2410 * Add LAST_DATE to values. 2411 * @param values the ContentValues (in/out) 2412 * @return values on success, null on failure 2413 */ 2414 private ContentValues updateLastDate(ContentValues values) { 2415 try { 2416 long last = calculateLastDate(values); 2417 if (last != -1) { 2418 values.put(Events.LAST_DATE, last); 2419 } 2420 2421 return values; 2422 } catch (DateException e) { 2423 // don't add it if there was an error 2424 if (Log.isLoggable(TAG, Log.WARN)) { 2425 Log.w(TAG, "Could not calculate last date.", e); 2426 } 2427 return null; 2428 } 2429 } 2430 2431 private void updateEventRawTimesLocked(long eventId, ContentValues values) { 2432 ContentValues rawValues = new ContentValues(); 2433 2434 rawValues.put("event_id", eventId); 2435 2436 String timezone = values.getAsString(Events.EVENT_TIMEZONE); 2437 2438 boolean allDay = false; 2439 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2440 if (allDayInteger != null) { 2441 allDay = allDayInteger != 0; 2442 } 2443 2444 if (allDay || TextUtils.isEmpty(timezone)) { 2445 // floating timezone 2446 timezone = Time.TIMEZONE_UTC; 2447 } 2448 2449 Time time = new Time(timezone); 2450 time.allDay = allDay; 2451 Long dtstartMillis = values.getAsLong(Events.DTSTART); 2452 if (dtstartMillis != null) { 2453 time.set(dtstartMillis); 2454 rawValues.put("dtstart2445", time.format2445()); 2455 } 2456 2457 Long dtendMillis = values.getAsLong(Events.DTEND); 2458 if (dtendMillis != null) { 2459 time.set(dtendMillis); 2460 rawValues.put("dtend2445", time.format2445()); 2461 } 2462 2463 Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 2464 if (originalInstanceMillis != null) { 2465 // This is a recurrence exception so we need to get the all-day 2466 // status of the original recurring event in order to format the 2467 // date correctly. 2468 allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY); 2469 if (allDayInteger != null) { 2470 time.allDay = allDayInteger != 0; 2471 } 2472 time.set(originalInstanceMillis); 2473 rawValues.put("originalInstanceTime2445", time.format2445()); 2474 } 2475 2476 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 2477 if (lastDateMillis != null) { 2478 time.allDay = allDay; 2479 time.set(lastDateMillis); 2480 rawValues.put("lastDate2445", time.format2445()); 2481 } 2482 2483 mDbHelper.eventsRawTimesReplace(rawValues); 2484 } 2485 2486 @Override 2487 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 2488 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2489 Log.v(TAG, "deleteInTransaction: " + uri); 2490 } 2491 final boolean callerIsSyncAdapter = 2492 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false); 2493 final int match = sUriMatcher.match(uri); 2494 switch (match) { 2495 case SYNCSTATE: 2496 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 2497 2498 case SYNCSTATE_ID: 2499 String selectionWithId = (BaseColumns._ID + "=?") 2500 + (selection == null ? "" : " AND (" + selection + ")"); 2501 // Prepend id to selectionArgs 2502 selectionArgs = insertSelectionArg(selectionArgs, 2503 String.valueOf(ContentUris.parseId(uri))); 2504 return mDbHelper.getSyncState().delete(mDb, selectionWithId, 2505 selectionArgs); 2506 2507 case EVENTS: 2508 { 2509 int result = 0; 2510 selection = appendAccountToSelection(uri, selection); 2511 2512 // Query this event to get the ids to delete. 2513 Cursor cursor = mDb.query("Events", ID_ONLY_PROJECTION, 2514 selection, selectionArgs, null /* groupBy */, 2515 null /* having */, null /* sortOrder */); 2516 try { 2517 while (cursor.moveToNext()) { 2518 long id = cursor.getLong(0); 2519 result += deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */); 2520 } 2521 scheduleNextAlarm(false /* do not remove alarms */); 2522 triggerAppWidgetUpdate(-1 /* changedEventId */); 2523 } finally { 2524 cursor.close(); 2525 cursor = null; 2526 } 2527 return result; 2528 } 2529 case EVENTS_ID: 2530 { 2531 long id = ContentUris.parseId(uri); 2532 if (selection != null) { 2533 throw new UnsupportedOperationException("CalendarProvider2 " 2534 + "doesn't support selection based deletion for type " 2535 + match); 2536 } 2537 return deleteEventInternal(id, callerIsSyncAdapter, false /* isBatch */); 2538 } 2539 case ATTENDEES: 2540 { 2541 if (callerIsSyncAdapter) { 2542 return mDb.delete("Attendees", selection, selectionArgs); 2543 } else { 2544 return deleteFromTable("Attendees", uri, selection, selectionArgs); 2545 } 2546 } 2547 case ATTENDEES_ID: 2548 { 2549 if (selection != null) { 2550 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2551 } 2552 if (callerIsSyncAdapter) { 2553 long id = ContentUris.parseId(uri); 2554 return mDb.delete("Attendees", "_id=?", new String[] {String.valueOf(id)}); 2555 } else { 2556 return deleteFromTable("Attendees", uri, null /* selection */, 2557 null /* selectionArgs */); 2558 } 2559 } 2560 case REMINDERS: 2561 { 2562 if (callerIsSyncAdapter) { 2563 return mDb.delete("Reminders", selection, selectionArgs); 2564 } else { 2565 return deleteFromTable("Reminders", uri, selection, selectionArgs); 2566 } 2567 } 2568 case REMINDERS_ID: 2569 { 2570 if (selection != null) { 2571 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2572 } 2573 if (callerIsSyncAdapter) { 2574 long id = ContentUris.parseId(uri); 2575 return mDb.delete("Reminders", "_id=?", new String[] {String.valueOf(id)}); 2576 } else { 2577 return deleteFromTable("Reminders", uri, null /* selection */, 2578 null /* selectionArgs */); 2579 } 2580 } 2581 case EXTENDED_PROPERTIES: 2582 { 2583 if (callerIsSyncAdapter) { 2584 return mDb.delete("ExtendedProperties", selection, selectionArgs); 2585 } else { 2586 return deleteFromTable("ExtendedProperties", uri, selection, selectionArgs); 2587 } 2588 } 2589 case EXTENDED_PROPERTIES_ID: 2590 { 2591 if (selection != null) { 2592 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2593 } 2594 if (callerIsSyncAdapter) { 2595 long id = ContentUris.parseId(uri); 2596 return mDb.delete("ExtendedProperties", "_id=?", 2597 new String[] {String.valueOf(id)}); 2598 } else { 2599 return deleteFromTable("ExtendedProperties", uri, null /* selection */, 2600 null /* selectionArgs */); 2601 } 2602 } 2603 case CALENDAR_ALERTS: 2604 { 2605 if (callerIsSyncAdapter) { 2606 return mDb.delete("CalendarAlerts", selection, selectionArgs); 2607 } else { 2608 return deleteFromTable("CalendarAlerts", uri, selection, selectionArgs); 2609 } 2610 } 2611 case CALENDAR_ALERTS_ID: 2612 { 2613 if (selection != null) { 2614 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2615 } 2616 // Note: dirty bit is not set for Alerts because it is not synced. 2617 // It is generated from Reminders, which is synced. 2618 long id = ContentUris.parseId(uri); 2619 return mDb.delete("CalendarAlerts", "_id=?", new String[] {String.valueOf(id)}); 2620 } 2621 case DELETED_EVENTS: 2622 throw new UnsupportedOperationException("Cannot delete that URL: " + uri); 2623 case CALENDARS_ID: 2624 StringBuilder selectionSb = new StringBuilder("_id="); 2625 selectionSb.append(uri.getPathSegments().get(1)); 2626 if (!TextUtils.isEmpty(selection)) { 2627 selectionSb.append(" AND ("); 2628 selectionSb.append(selection); 2629 selectionSb.append(')'); 2630 } 2631 selection = selectionSb.toString(); 2632 // fall through to CALENDARS for the actual delete 2633 case CALENDARS: 2634 selection = appendAccountToSelection(uri, selection); 2635 return deleteMatchingCalendars(selection); // TODO: handle in sync adapter 2636 case INSTANCES: 2637 case INSTANCES_BY_DAY: 2638 case EVENT_DAYS: 2639 case PROVIDER_PROPERTIES: 2640 throw new UnsupportedOperationException("Cannot delete that URL"); 2641 default: 2642 throw new IllegalArgumentException("Unknown URL " + uri); 2643 } 2644 } 2645 2646 private int deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch) { 2647 int result = 0; 2648 String selectionArgs[] = new String[] {String.valueOf(id)}; 2649 2650 // Query this event to get the fields needed for deleting. 2651 Cursor cursor = mDb.query("Events", EVENTS_PROJECTION, 2652 "_id=?", selectionArgs, 2653 null /* groupBy */, 2654 null /* having */, null /* sortOrder */); 2655 try { 2656 if (cursor.moveToNext()) { 2657 result = 1; 2658 String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX); 2659 boolean emptySyncId = TextUtils.isEmpty(syncId); 2660 2661 // If this was a recurring event or a recurrence 2662 // exception, then force a recalculation of the 2663 // instances. 2664 String rrule = cursor.getString(EVENTS_RRULE_INDEX); 2665 String rdate = cursor.getString(EVENTS_RDATE_INDEX); 2666 String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX); 2667 if (isRecurrenceEvent(rrule, rdate, origEvent)) { 2668 mMetaData.clearInstanceRange(); 2669 } 2670 2671 // we clean the Events and Attendees table if the caller is CalendarSyncAdapter 2672 // or if the event is local (no syncId) 2673 if (callerIsSyncAdapter || emptySyncId) { 2674 mDb.delete("Events", "_id=?", selectionArgs); 2675 } else { 2676 ContentValues values = new ContentValues(); 2677 values.put(Events.DELETED, 1); 2678 values.put(Events._SYNC_DIRTY, 1); 2679 mDb.update("Events", values, "_id=?", selectionArgs); 2680 2681 // Delete associated data; attendees, however, are deleted with the actual event 2682 // so that the sync adapter is able to notify attendees of the cancellation. 2683 mDb.delete("Instances", "event_id=?", selectionArgs); 2684 mDb.delete("EventsRawTimes", "event_id=?", selectionArgs); 2685 mDb.delete("Reminders", "event_id=?", selectionArgs); 2686 mDb.delete("CalendarAlerts", "event_id=?", selectionArgs); 2687 mDb.delete("ExtendedProperties", "event_id=?", selectionArgs); 2688 } 2689 } 2690 } finally { 2691 cursor.close(); 2692 cursor = null; 2693 } 2694 2695 if (!isBatch) { 2696 scheduleNextAlarm(false /* do not remove alarms */); 2697 triggerAppWidgetUpdate(-1 /* changedEventId */); 2698 } 2699 2700 return result; 2701 } 2702 2703 /** 2704 * Delete rows from a table and mark corresponding events as dirty. 2705 * @param table The table to delete from 2706 * @param uri The URI specifying the rows 2707 * @param selection for the query 2708 * @param selectionArgs for the query 2709 */ 2710 private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) { 2711 // Note that the query will return data according to the access restrictions, 2712 // so we don't need to worry about deleting data we don't have permission to read. 2713 Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null); 2714 ContentValues values = new ContentValues(); 2715 values.put(Events._SYNC_DIRTY, "1"); 2716 int count = 0; 2717 try { 2718 while(c.moveToNext()) { 2719 long id = c.getLong(ID_INDEX); 2720 long event_id = c.getLong(EVENT_ID_INDEX); 2721 mDb.delete(table, "_id=?", new String[] {String.valueOf(id)}); 2722 mDb.update("Events", values, "_id=?", new String[] {String.valueOf(event_id)}); 2723 count++; 2724 } 2725 } finally { 2726 c.close(); 2727 } 2728 return count; 2729 } 2730 2731 /** 2732 * Update rows in a table and mark corresponding events as dirty. 2733 * @param table The table to delete from 2734 * @param values The values to update 2735 * @param uri The URI specifying the rows 2736 * @param selection for the query 2737 * @param selectionArgs for the query 2738 */ 2739 private int updateInTable(String table, ContentValues values, Uri uri, String selection, 2740 String[] selectionArgs) { 2741 // Note that the query will return data according to the access restrictions, 2742 // so we don't need to worry about deleting data we don't have permission to read. 2743 Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null); 2744 ContentValues dirtyValues = new ContentValues(); 2745 dirtyValues.put(Events._SYNC_DIRTY, "1"); 2746 int count = 0; 2747 try { 2748 while(c.moveToNext()) { 2749 long id = c.getLong(ID_INDEX); 2750 long event_id = c.getLong(EVENT_ID_INDEX); 2751 mDb.update(table, values, "_id=?", new String[] {String.valueOf(id)}); 2752 mDb.update("Events", dirtyValues, "_id=?", new String[] {String.valueOf(event_id)}); 2753 count++; 2754 } 2755 } finally { 2756 c.close(); 2757 } 2758 return count; 2759 } 2760 2761 private int deleteMatchingCalendars(String where) { 2762 // query to find all the calendars that match, for each 2763 // - delete calendar subscription 2764 // - delete calendar 2765 2766 Cursor c = mDb.query("Calendars", sCalendarsIdProjection, where, 2767 null /* selectionArgs */, null /* groupBy */, 2768 null /* having */, null /* sortOrder */); 2769 if (c == null) { 2770 return 0; 2771 } 2772 try { 2773 while (c.moveToNext()) { 2774 long id = c.getLong(CALENDARS_INDEX_ID); 2775 modifyCalendarSubscription(id, false /* not selected */); 2776 } 2777 } finally { 2778 c.close(); 2779 } 2780 return mDb.delete("Calendars", where, null /* whereArgs */); 2781 } 2782 2783 private Cursor getCursorForEventIdAndProjection(String eventId, String[] projection) { 2784 return mDb.query(Tables.EVENTS, 2785 projection, 2786 SQL_WHERE_ID, 2787 new String[] { eventId }, 2788 null /* group by */, 2789 null /* having */, 2790 null /* order by*/); 2791 } 2792 2793 private boolean doesEventExistForSyncId(String syncId) { 2794 if (syncId == null) { 2795 if (Log.isLoggable(TAG, Log.WARN)) { 2796 Log.w(TAG, "SyncID cannot be null: " + syncId); 2797 } 2798 return false; 2799 } 2800 long count = DatabaseUtils.longForQuery(mDb, SQL_SELECT_COUNT_FOR_SYNC_ID, 2801 new String[] { syncId }); 2802 return (count > 0); 2803 } 2804 2805 // Check if an UPDATE with STATUS_CANCEL means that we will need to do an Update (instead of 2806 // a Deletion) 2807 // 2808 // Deletion will be done only and only if: 2809 // - event status = canceled 2810 // - event is a recurrence exception that does not have its original (parent) event anymore 2811 // 2812 // This is due to the Server semantics that generate STATUS_CANCELED for both creation 2813 // and deletion of a recurrence exception 2814 // See bug #3218104 2815 private boolean doesStatusCancelUpdateMeanUpdate(String eventId, ContentValues values) { 2816 boolean isStatusCanceled = values.containsKey(Events.STATUS) && 2817 (values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED); 2818 if (isStatusCanceled) { 2819 Cursor cursor = null; 2820 try { 2821 cursor = getCursorForEventIdAndProjection(eventId, 2822 new String[] { Events.RRULE, Events.RDATE, Events.ORIGINAL_EVENT }); 2823 if (!cursor.moveToFirst()) { 2824 if (Log.isLoggable(TAG, Log.WARN)) { 2825 Log.w(TAG, "Cannot find Event with id: " + eventId); 2826 } 2827 return false; 2828 } 2829 String rrule = cursor.getString(0); 2830 String rdate = cursor.getString(1); 2831 String originalEvent = cursor.getString(2); 2832 2833 boolean isRecurrenceException = 2834 isRecurrenceEvent(rrule, rdate, originalEvent) && 2835 !TextUtils.isEmpty(originalEvent); 2836 2837 if (isRecurrenceException) { 2838 return doesEventExistForSyncId(originalEvent); 2839 } 2840 } finally { 2841 cursor.close(); 2842 } 2843 } 2844 // This is the normal case, we just want an UPDATE 2845 return true; 2846 } 2847 2848 // TODO: call calculateLastDate()! 2849 @Override 2850 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2851 String[] selectionArgs) { 2852 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2853 Log.v(TAG, "updateInTransaction: " + uri); 2854 } 2855 2856 int count = 0; 2857 2858 final int match = sUriMatcher.match(uri); 2859 2860 final boolean callerIsSyncAdapter = 2861 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false); 2862 2863 // TODO: remove this restriction 2864 if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS 2865 && match != EVENTS && match != PROVIDER_PROPERTIES) { 2866 throw new IllegalArgumentException( 2867 "WHERE based updates not supported"); 2868 } 2869 switch (match) { 2870 case SYNCSTATE: 2871 return mDbHelper.getSyncState().update(mDb, values, 2872 appendAccountToSelection(uri, selection), selectionArgs); 2873 2874 case SYNCSTATE_ID: { 2875 selection = appendAccountToSelection(uri, selection); 2876 String selectionWithId = (BaseColumns._ID + "=?") 2877 + (selection == null ? "" : " AND (" + selection + ")"); 2878 // Prepend id to selectionArgs 2879 selectionArgs = insertSelectionArg(selectionArgs, 2880 String.valueOf(ContentUris.parseId(uri))); 2881 return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs); 2882 } 2883 2884 case CALENDARS_ID: 2885 { 2886 if (selection != null) { 2887 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2888 } 2889 long id = ContentUris.parseId(uri); 2890 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS); 2891 if (syncEvents != null) { 2892 modifyCalendarSubscription(id, syncEvents == 1); 2893 } 2894 2895 int result = mDb.update("Calendars", values, "_id=?", 2896 new String[] {String.valueOf(id)}); 2897 2898 // The calendar should not be displayed in widget either. 2899 final Integer selected = values.getAsInteger(Calendars.SELECTED); 2900 if (selected != null && selected == 0) { 2901 triggerAppWidgetUpdate(-1); 2902 } 2903 2904 return result; 2905 } 2906 case EVENTS: 2907 case EVENTS_ID: 2908 { 2909 long id = 0; 2910 if (match == EVENTS_ID) { 2911 id = ContentUris.parseId(uri); 2912 } else if (callerIsSyncAdapter) { 2913 if (selection != null && selection.startsWith("_id=")) { 2914 // The ContentProviderOperation generates an _id=n string instead of 2915 // adding the id to the URL, so parse that out here. 2916 id = Long.parseLong(selection.substring(4)); 2917 } else { 2918 // Sync adapter Events operation affects just Events table, not associated 2919 // tables. 2920 if (fixAllDayTime(uri, values)) { 2921 if (Log.isLoggable(TAG, Log.WARN)) { 2922 Log.w(TAG, "updateInTransaction: Caller is sync adapter. " + 2923 "allDay is true but sec, min, hour were not 0."); 2924 } 2925 } 2926 return mDb.update("Events", values, selection, selectionArgs); 2927 } 2928 } else { 2929 throw new IllegalArgumentException("Unknown URL " + uri); 2930 } 2931 if (!callerIsSyncAdapter) { 2932 values.put(Events._SYNC_DIRTY, 1); 2933 } 2934 // Disallow updating the attendee status in the Events 2935 // table. In the future, we could support this but we 2936 // would have to query and update the attendees table 2937 // to keep the values consistent. 2938 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) { 2939 throw new IllegalArgumentException("Updating " 2940 + Events.SELF_ATTENDEE_STATUS 2941 + " in Events table is not allowed."); 2942 } 2943 2944 // TODO: should we allow this? 2945 if (values.containsKey(Events.HTML_URI) && !callerIsSyncAdapter) { 2946 throw new IllegalArgumentException("Updating " 2947 + Events.HTML_URI 2948 + " in Events table is not allowed."); 2949 } 2950 String strId = String.valueOf(id); 2951 // For taking care about recurrences exceptions cancelations, check if this needs 2952 // to be an UPDATE or a DELETE 2953 boolean isUpdate = doesStatusCancelUpdateMeanUpdate(strId, values); 2954 ContentValues updatedValues = new ContentValues(values); 2955 // TODO: should extend validateEventData to work with updates and call it here 2956 updatedValues = updateLastDate(updatedValues); 2957 if (updatedValues == null) { 2958 if (Log.isLoggable(TAG, Log.WARN)) { 2959 Log.w(TAG, "Could not update event."); 2960 } 2961 return 0; 2962 } 2963 // Make sure we pass in a uri with the id appended to fixAllDayTime 2964 Uri allDayUri; 2965 if (uri.getPathSegments().size() == 1) { 2966 allDayUri = ContentUris.withAppendedId(uri, id); 2967 } else { 2968 allDayUri = uri; 2969 } 2970 if (fixAllDayTime(allDayUri, updatedValues)) { 2971 if (Log.isLoggable(TAG, Log.WARN)) { 2972 Log.w(TAG, "updateInTransaction: " + 2973 "allDay is true but sec, min, hour were not 0."); 2974 } 2975 } 2976 2977 int result; 2978 2979 if (isUpdate) { 2980 result = mDb.update("Events", updatedValues, "_id=?", 2981 new String[] {String.valueOf(id)}); 2982 if (result > 0) { 2983 updateEventRawTimesLocked(id, updatedValues); 2984 updateInstancesLocked(updatedValues, id, false /* not a new event */, mDb); 2985 2986 if (values.containsKey(Events.DTSTART)) { 2987 // The start time of the event changed, so run the 2988 // event alarm scheduler. 2989 if (Log.isLoggable(TAG, Log.DEBUG)) { 2990 Log.d(TAG, "updateInternal() changing event"); 2991 } 2992 scheduleNextAlarm(false /* do not remove alarms */); 2993 triggerAppWidgetUpdate(id); 2994 } 2995 } 2996 } else { 2997 result = deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */); 2998 scheduleNextAlarm(false /* do not remove alarms */); 2999 triggerAppWidgetUpdate(id); 3000 } 3001 return result; 3002 } 3003 case ATTENDEES_ID: { 3004 if (selection != null) { 3005 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3006 } 3007 // Copy the attendee status value to the Events table. 3008 updateEventAttendeeStatus(mDb, values); 3009 3010 if (callerIsSyncAdapter) { 3011 long id = ContentUris.parseId(uri); 3012 return mDb.update("Attendees", values, "_id=?", 3013 new String[] {String.valueOf(id)}); 3014 } else { 3015 return updateInTable("Attendees", values, uri, null /* selection */, 3016 null /* selectionArgs */); 3017 } 3018 } 3019 case CALENDAR_ALERTS_ID: { 3020 if (selection != null) { 3021 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3022 } 3023 // Note: dirty bit is not set for Alerts because it is not synced. 3024 // It is generated from Reminders, which is synced. 3025 long id = ContentUris.parseId(uri); 3026 return mDb.update("CalendarAlerts", values, "_id=?", 3027 new String[] {String.valueOf(id)}); 3028 } 3029 case CALENDAR_ALERTS: { 3030 // Note: dirty bit is not set for Alerts because it is not synced. 3031 // It is generated from Reminders, which is synced. 3032 return mDb.update("CalendarAlerts", values, selection, selectionArgs); 3033 } 3034 case REMINDERS_ID: { 3035 if (selection != null) { 3036 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3037 } 3038 if (callerIsSyncAdapter) { 3039 long id = ContentUris.parseId(uri); 3040 count = mDb.update("Reminders", values, "_id=?", 3041 new String[] {String.valueOf(id)}); 3042 } else { 3043 count = updateInTable("Reminders", values, uri, null /* selection */, 3044 null /* selectionArgs */); 3045 } 3046 3047 // Reschedule the event alarms because the 3048 // "minutes" field may have changed. 3049 if (Log.isLoggable(TAG, Log.DEBUG)) { 3050 Log.d(TAG, "updateInternal() changing reminder"); 3051 } 3052 scheduleNextAlarm(false /* do not remove alarms */); 3053 return count; 3054 } 3055 case EXTENDED_PROPERTIES_ID: { 3056 if (selection != null) { 3057 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3058 } 3059 if (callerIsSyncAdapter) { 3060 long id = ContentUris.parseId(uri); 3061 return mDb.update("ExtendedProperties", values, "_id=?", 3062 new String[] {String.valueOf(id)}); 3063 } else { 3064 return updateInTable("ExtendedProperties", values, uri, null /* selection */, 3065 null /* selectionArgs */); 3066 } 3067 } 3068 // TODO: replace the SCHEDULE_ALARM private URIs with a 3069 // service 3070 case SCHEDULE_ALARM: { 3071 scheduleNextAlarm(false); 3072 return 0; 3073 } 3074 case SCHEDULE_ALARM_REMOVE: { 3075 scheduleNextAlarm(true); 3076 return 0; 3077 } 3078 3079 case PROVIDER_PROPERTIES: { 3080 if (selection == null) { 3081 throw new UnsupportedOperationException("Selection cannot be null for " + uri); 3082 } 3083 if (!selection.equals("key=?")) { 3084 throw new UnsupportedOperationException("Selection should be key=? for " + uri); 3085 } 3086 3087 List<String> list = Arrays.asList(selectionArgs); 3088 3089 if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) { 3090 throw new UnsupportedOperationException("Invalid selection key: " + 3091 CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + " for " + uri); 3092 } 3093 3094 // Before it may be changed, save current Instances timezone for later use 3095 String timezoneInstancesBeforeUpdate = mCalendarCache.readTimezoneInstances(); 3096 3097 // Update the database with the provided values (this call may change the value 3098 // of timezone Instances) 3099 int result = mDb.update("CalendarCache", values, selection, selectionArgs); 3100 3101 // if successful, do some house cleaning: 3102 // if the timezone type is set to "home", set the Instances timezone to the previous 3103 // if the timezone type is set to "auto", set the Instances timezone to the current 3104 // device one 3105 // if the timezone Instances is set AND if we are in "home" timezone type, then 3106 // save the timezone Instance into "previous" too 3107 if (result > 0) { 3108 // If we are changing timezone type... 3109 if (list.contains(CalendarCache.KEY_TIMEZONE_TYPE)) { 3110 String value = values.getAsString(CalendarCache.COLUMN_NAME_VALUE); 3111 if (value != null) { 3112 // if we are setting timezone type to "home" 3113 if (value.equals(CalendarCache.TIMEZONE_TYPE_HOME)) { 3114 String previousTimezone = 3115 mCalendarCache.readTimezoneInstancesPrevious(); 3116 if (previousTimezone != null) { 3117 mCalendarCache.writeTimezoneInstances(previousTimezone); 3118 } 3119 // Regenerate Instances if the "home" timezone has changed 3120 if (!timezoneInstancesBeforeUpdate.equals(previousTimezone) ) { 3121 regenerateInstancesTable(); 3122 } 3123 } 3124 // if we are setting timezone type to "auto" 3125 else if (value.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) { 3126 String localTimezone = TimeZone.getDefault().getID(); 3127 mCalendarCache.writeTimezoneInstances(localTimezone); 3128 if (!timezoneInstancesBeforeUpdate.equals(localTimezone)) { 3129 regenerateInstancesTable(); 3130 } 3131 } 3132 } 3133 } 3134 // If we are changing timezone Instances... 3135 else if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES)) { 3136 // if we are in "home" timezone type... 3137 if (isHomeTimezone()) { 3138 String timezoneInstances = mCalendarCache.readTimezoneInstances(); 3139 // Update the previous value 3140 mCalendarCache.writeTimezoneInstancesPrevious(timezoneInstances); 3141 // Recompute Instances if the "home" timezone has changed 3142 if (timezoneInstancesBeforeUpdate != null && 3143 !timezoneInstancesBeforeUpdate.equals(timezoneInstances)) { 3144 regenerateInstancesTable(); 3145 } 3146 } 3147 } 3148 triggerAppWidgetUpdate(-1); 3149 } 3150 return result; 3151 } 3152 3153 default: 3154 throw new IllegalArgumentException("Unknown URL " + uri); 3155 } 3156 } 3157 3158 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 3159 final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME); 3160 final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE); 3161 if (!TextUtils.isEmpty(accountName)) { 3162 qb.appendWhere(Calendar.Calendars._SYNC_ACCOUNT + "=" 3163 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3164 + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=" 3165 + DatabaseUtils.sqlEscapeString(accountType)); 3166 } else { 3167 qb.appendWhere("1"); // I.e. always true 3168 } 3169 } 3170 3171 private String appendAccountToSelection(Uri uri, String selection) { 3172 final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME); 3173 final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE); 3174 if (!TextUtils.isEmpty(accountName)) { 3175 StringBuilder selectionSb = new StringBuilder(Calendar.Calendars._SYNC_ACCOUNT + "=" 3176 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3177 + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=" 3178 + DatabaseUtils.sqlEscapeString(accountType)); 3179 if (!TextUtils.isEmpty(selection)) { 3180 selectionSb.append(" AND ("); 3181 selectionSb.append(selection); 3182 selectionSb.append(')'); 3183 } 3184 return selectionSb.toString(); 3185 } else { 3186 return selection; 3187 } 3188 } 3189 3190 private void modifyCalendarSubscription(long id, boolean syncEvents) { 3191 // get the account, url, and current selected state 3192 // for this calendar. 3193 Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id), 3194 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE, 3195 Calendars.URL, Calendars.SYNC_EVENTS}, 3196 null /* selection */, 3197 null /* selectionArgs */, 3198 null /* sort */); 3199 3200 Account account = null; 3201 String calendarUrl = null; 3202 boolean oldSyncEvents = false; 3203 if (cursor != null) { 3204 try { 3205 if (cursor.moveToFirst()) { 3206 final String accountName = cursor.getString(0); 3207 final String accountType = cursor.getString(1); 3208 account = new Account(accountName, accountType); 3209 calendarUrl = cursor.getString(2); 3210 oldSyncEvents = (cursor.getInt(3) != 0); 3211 } 3212 } finally { 3213 cursor.close(); 3214 } 3215 } 3216 3217 if (account == null) { 3218 // should not happen? 3219 if (Log.isLoggable(TAG, Log.WARN)) { 3220 Log.w(TAG, "Cannot update subscription because account " 3221 + "is empty -- should not happen."); 3222 } 3223 return; 3224 } 3225 3226 if (TextUtils.isEmpty(calendarUrl)) { 3227 // Passing in a null Url will cause it to not add any extras 3228 // Should only happen for non-google calendars. 3229 calendarUrl = null; 3230 } 3231 3232 if (oldSyncEvents == syncEvents) { 3233 // nothing to do 3234 return; 3235 } 3236 3237 // If the calendar is not selected for syncing, then don't download 3238 // events. 3239 mDbHelper.scheduleSync(account, !syncEvents, calendarUrl); 3240 } 3241 3242 // TODO: is this needed 3243 // @Override 3244 // public void onSyncStop(SyncContext context, boolean success) { 3245 // super.onSyncStop(context, success); 3246 // if (Log.isLoggable(TAG, Log.DEBUG)) { 3247 // Log.d(TAG, "onSyncStop() success: " + success); 3248 // } 3249 // scheduleNextAlarm(false /* do not remove alarms */); 3250 // triggerAppWidgetUpdate(-1); 3251 // } 3252 3253 /** 3254 * Update any existing widgets with the changed events. 3255 * 3256 * @param changedEventId Specific event known to be changed, otherwise -1. 3257 * If present, we use it to decide if an update is necessary. 3258 */ 3259 private synchronized void triggerAppWidgetUpdate(long changedEventId) { 3260 Context context = getContext(); 3261 if (context != null) { 3262 mAppWidgetProvider.providerUpdated(context, changedEventId); 3263 } 3264 } 3265 3266 /* Retrieve and cache the alarm manager */ 3267 private AlarmManager getAlarmManager() { 3268 synchronized(mAlarmLock) { 3269 if (mAlarmManager == null) { 3270 Context context = getContext(); 3271 if (context == null) { 3272 if (Log.isLoggable(TAG, Log.ERROR)) { 3273 Log.e(TAG, "getAlarmManager() cannot get Context"); 3274 } 3275 return null; 3276 } 3277 Object service = context.getSystemService(Context.ALARM_SERVICE); 3278 mAlarmManager = (AlarmManager) service; 3279 } 3280 return mAlarmManager; 3281 } 3282 } 3283 3284 void scheduleNextAlarmCheck(long triggerTime) { 3285 AlarmManager manager = getAlarmManager(); 3286 if (manager == null) { 3287 if (Log.isLoggable(TAG, Log.ERROR)) { 3288 Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager"); 3289 } 3290 return; 3291 } 3292 Context context = getContext(); 3293 Intent intent = new Intent(CalendarReceiver.SCHEDULE); 3294 intent.setClass(context, CalendarReceiver.class); 3295 PendingIntent pending = PendingIntent.getBroadcast(context, 3296 0, intent, PendingIntent.FLAG_NO_CREATE); 3297 if (pending != null) { 3298 // Cancel any previous alarms that do the same thing. 3299 manager.cancel(pending); 3300 } 3301 pending = PendingIntent.getBroadcast(context, 3302 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 3303 3304 if (Log.isLoggable(TAG, Log.DEBUG)) { 3305 Time time = new Time(); 3306 time.set(triggerTime); 3307 String timeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3308 Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr); 3309 } 3310 3311 manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending); 3312 } 3313 3314 /* 3315 * This method runs the alarm scheduler in a background thread. 3316 */ 3317 void scheduleNextAlarm(boolean removeAlarms) { 3318 synchronized (mAlarmLock) { 3319 if (mAlarmScheduler == null) { 3320 mAlarmScheduler = new AlarmScheduler(removeAlarms); 3321 mAlarmScheduler.start(); 3322 } else { 3323 mRerunAlarmScheduler = true; 3324 // removing the alarms is a stronger action so it has 3325 // precedence. 3326 mRemoveAlarmsOnRerun = mRemoveAlarmsOnRerun || removeAlarms; 3327 } 3328 } 3329 } 3330 3331 /** 3332 * This method runs in a background thread and schedules an alarm for 3333 * the next calendar event, if necessary. 3334 */ 3335 private void runScheduleNextAlarm(boolean removeAlarms) { 3336 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 3337 db.beginTransaction(); 3338 try { 3339 if (removeAlarms) { 3340 removeScheduledAlarmsLocked(db); 3341 } 3342 scheduleNextAlarmLocked(db); 3343 db.setTransactionSuccessful(); 3344 } finally { 3345 db.endTransaction(); 3346 } 3347 } 3348 3349 /** 3350 * This method looks at the 24-hour window from now for any events that it 3351 * needs to schedule. This method runs within a database transaction. It 3352 * also runs in a background thread. 3353 * 3354 * The CalendarProvider2 keeps track of which alarms it has already scheduled 3355 * to avoid scheduling them more than once and for debugging problems with 3356 * alarms. It stores this knowledge in a database table called CalendarAlerts 3357 * which persists across reboots. But the actual alarm list is in memory 3358 * and disappears if the phone loses power. To avoid missing an alarm, we 3359 * clear the entries in the CalendarAlerts table when we start up the 3360 * CalendarProvider2. 3361 * 3362 * Scheduling an alarm multiple times is not tragic -- we filter out the 3363 * extra ones when we receive them. But we still need to keep track of the 3364 * scheduled alarms. The main reason is that we need to prevent multiple 3365 * notifications for the same alarm (on the receive side) in case we 3366 * accidentally schedule the same alarm multiple times. We don't have 3367 * visibility into the system's alarm list so we can never know for sure if 3368 * we have already scheduled an alarm and it's better to err on scheduling 3369 * an alarm twice rather than missing an alarm. Another reason we keep 3370 * track of scheduled alarms in a database table is that it makes it easy to 3371 * run an SQL query to find the next reminder that we haven't scheduled. 3372 * 3373 * @param db the database 3374 */ 3375 private void scheduleNextAlarmLocked(SQLiteDatabase db) { 3376 AlarmManager alarmManager = getAlarmManager(); 3377 if (alarmManager == null) { 3378 if (Log.isLoggable(TAG, Log.ERROR)) { 3379 Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!"); 3380 } 3381 return; 3382 } 3383 3384 final long currentMillis = System.currentTimeMillis(); 3385 final long start = currentMillis - SCHEDULE_ALARM_SLACK; 3386 final long end = start + (24 * 60 * 60 * 1000); 3387 ContentResolver cr = getContext().getContentResolver(); 3388 if (Log.isLoggable(TAG, Log.DEBUG)) { 3389 Time time = new Time(); 3390 time.set(start); 3391 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3392 Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr); 3393 } 3394 3395 // Delete rows in CalendarAlert where the corresponding Instance or 3396 // Reminder no longer exist. 3397 // Also clear old alarms but keep alarms around for a while to prevent 3398 // multiple alerts for the same reminder. The "clearUpToTime' 3399 // should be further in the past than the point in time where 3400 // we start searching for events (the "start" variable defined above). 3401 String selectArg[] = new String[] { 3402 Long.toString(currentMillis - CLEAR_OLD_ALARM_THRESHOLD) 3403 }; 3404 3405 int rowsDeleted = 3406 db.delete(CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg); 3407 3408 long nextAlarmTime = end; 3409 final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis); 3410 if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) { 3411 nextAlarmTime = tmpAlarmTime; 3412 } 3413 3414 // Extract events from the database sorted by alarm time. The 3415 // alarm times are computed from Instances.begin (whose units 3416 // are milliseconds) and Reminders.minutes (whose units are 3417 // minutes). 3418 // 3419 // Also, ignore events whose end time is already in the past. 3420 // Also, ignore events alarms that we have already scheduled. 3421 // 3422 // Note 1: we can add support for the case where Reminders.minutes 3423 // equals -1 to mean use Calendars.minutes by adding a UNION for 3424 // that case where the two halves restrict the WHERE clause on 3425 // Reminders.minutes != -1 and Reminders.minutes = 1, respectively. 3426 // 3427 // Note 2: we have to name "myAlarmTime" different from the 3428 // "alarmTime" column in CalendarAlerts because otherwise the 3429 // query won't find multiple alarms for the same event. 3430 // 3431 // The CAST is needed in the query because otherwise the expression 3432 // will be untyped and sqlite3's manifest typing will not convert the 3433 // string query parameter to an int in myAlarmtime>=?, so the comparison 3434 // will fail. This could be simplified if bug 2464440 is resolved. 3435 String query = "SELECT begin-(minutes*60000) AS myAlarmTime," 3436 + " Instances.event_id AS eventId, begin, end," 3437 + " title, allDay, method, minutes" 3438 + " FROM Instances INNER JOIN Events" 3439 + " ON (Events._id = Instances.event_id)" 3440 + " INNER JOIN Reminders" 3441 + " ON (Instances.event_id = Reminders.event_id)" 3442 + " WHERE method=" + Reminders.METHOD_ALERT 3443 + " AND myAlarmTime>=CAST(? AS INT)" 3444 + " AND myAlarmTime<=CAST(? AS INT)" 3445 + " AND end>=?" 3446 + " AND 0=(SELECT count(*) from CalendarAlerts CA" 3447 + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin" 3448 + " AND CA.alarmTime=myAlarmTime)" 3449 + " ORDER BY myAlarmTime,begin,title"; 3450 String queryParams[] = new String[] {String.valueOf(start), String.valueOf(nextAlarmTime), 3451 String.valueOf(currentMillis)}; 3452 3453 String instancesTimezone = mCalendarCache.readTimezoneInstances(); 3454 boolean isHomeTimezone = mCalendarCache.readTimezoneType().equals( 3455 CalendarCache.TIMEZONE_TYPE_HOME); 3456 acquireInstanceRangeLocked(start, 3457 end, 3458 false /* don't use minimum expansion windows */, 3459 false /* do not force Instances deletion and expansion */, 3460 instancesTimezone, 3461 isHomeTimezone); 3462 Cursor cursor = null; 3463 try { 3464 cursor = db.rawQuery(query, queryParams); 3465 3466 final int beginIndex = cursor.getColumnIndex(Instances.BEGIN); 3467 final int endIndex = cursor.getColumnIndex(Instances.END); 3468 final int eventIdIndex = cursor.getColumnIndex("eventId"); 3469 final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime"); 3470 final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES); 3471 3472 if (Log.isLoggable(TAG, Log.DEBUG)) { 3473 Time time = new Time(); 3474 time.set(nextAlarmTime); 3475 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3476 Log.d(TAG, "cursor results: " + cursor.getCount() + " nextAlarmTime: " 3477 + alarmTimeStr); 3478 } 3479 3480 while (cursor.moveToNext()) { 3481 // Schedule all alarms whose alarm time is as early as any 3482 // scheduled alarm. For example, if the earliest alarm is at 3483 // 1pm, then we will schedule all alarms that occur at 1pm 3484 // but no alarms that occur later than 1pm. 3485 // Actually, we allow alarms up to a minute later to also 3486 // be scheduled so that we don't have to check immediately 3487 // again after an event alarm goes off. 3488 final long alarmTime = cursor.getLong(alarmTimeIndex); 3489 final long eventId = cursor.getLong(eventIdIndex); 3490 final int minutes = cursor.getInt(minutesIndex); 3491 final long startTime = cursor.getLong(beginIndex); 3492 final long endTime = cursor.getLong(endIndex); 3493 3494 if (Log.isLoggable(TAG, Log.DEBUG)) { 3495 Time time = new Time(); 3496 time.set(alarmTime); 3497 String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); 3498 time.set(startTime); 3499 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3500 3501 Log.d(TAG, " looking at id: " + eventId + " " + startTime + startTimeStr 3502 + " alarm: " + alarmTime + schedTime); 3503 } 3504 3505 if (alarmTime < nextAlarmTime) { 3506 nextAlarmTime = alarmTime; 3507 } else if (alarmTime > 3508 nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) { 3509 // This event alarm (and all later ones) will be scheduled 3510 // later. 3511 if (Log.isLoggable(TAG, Log.DEBUG)) { 3512 Log.d(TAG, "This event alarm (and all later ones) will be scheduled later"); 3513 } 3514 break; 3515 } 3516 3517 // Avoid an SQLiteContraintException by checking if this alarm 3518 // already exists in the table. 3519 if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) { 3520 if (Log.isLoggable(TAG, Log.DEBUG)) { 3521 int titleIndex = cursor.getColumnIndex(Events.TITLE); 3522 String title = cursor.getString(titleIndex); 3523 Log.d(TAG, " alarm exists for id: " + eventId + " " + title); 3524 } 3525 continue; 3526 } 3527 3528 // Insert this alarm into the CalendarAlerts table 3529 Uri uri = CalendarAlerts.insert(cr, eventId, startTime, 3530 endTime, alarmTime, minutes); 3531 if (uri == null) { 3532 if (Log.isLoggable(TAG, Log.ERROR)) { 3533 Log.e(TAG, "runScheduleNextAlarm() insert into " 3534 + "CalendarAlerts table failed"); 3535 } 3536 continue; 3537 } 3538 3539 CalendarAlerts.scheduleAlarm(getContext(), alarmManager, alarmTime); 3540 } 3541 } finally { 3542 if (cursor != null) { 3543 cursor.close(); 3544 } 3545 } 3546 3547 // Refresh notification bar 3548 if (rowsDeleted > 0) { 3549 CalendarAlerts.scheduleAlarm(getContext(), alarmManager, currentMillis); 3550 } 3551 3552 // If we scheduled an event alarm, then schedule the next alarm check 3553 // for one minute past that alarm. Otherwise, if there were no 3554 // event alarms scheduled, then check again in 24 hours. If a new 3555 // event is inserted before the next alarm check, then this method 3556 // will be run again when the new event is inserted. 3557 if (nextAlarmTime != Long.MAX_VALUE) { 3558 scheduleNextAlarmCheck(nextAlarmTime + DateUtils.MINUTE_IN_MILLIS); 3559 } else { 3560 scheduleNextAlarmCheck(currentMillis + DateUtils.DAY_IN_MILLIS); 3561 } 3562 } 3563 3564 /** 3565 * Removes the entries in the CalendarAlerts table for alarms that we have 3566 * scheduled but that have not fired yet. We do this to ensure that we 3567 * don't miss an alarm. The CalendarAlerts table keeps track of the 3568 * alarms that we have scheduled but the actual alarm list is in memory 3569 * and will be cleared if the phone reboots. 3570 * 3571 * We don't need to remove entries that have already fired, and in fact 3572 * we should not remove them because we need to display the notifications 3573 * until the user dismisses them. 3574 * 3575 * We could remove entries that have fired and been dismissed, but we leave 3576 * them around for a while because it makes it easier to debug problems. 3577 * Entries that are old enough will be cleaned up later when we schedule 3578 * new alarms. 3579 */ 3580 private void removeScheduledAlarmsLocked(SQLiteDatabase db) { 3581 if (Log.isLoggable(TAG, Log.DEBUG)) { 3582 Log.d(TAG, "removing scheduled alarms"); 3583 } 3584 db.delete(CalendarAlerts.TABLE_NAME, 3585 CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */); 3586 } 3587 3588 private static String sEventsTable = "Events"; 3589 private static String sAttendeesTable = "Attendees"; 3590 private static String sRemindersTable = "Reminders"; 3591 private static String sCalendarAlertsTable = "CalendarAlerts"; 3592 private static String sExtendedPropertiesTable = "ExtendedProperties"; 3593 3594 private static final int EVENTS = 1; 3595 private static final int EVENTS_ID = 2; 3596 private static final int INSTANCES = 3; 3597 private static final int DELETED_EVENTS = 4; 3598 private static final int CALENDARS = 5; 3599 private static final int CALENDARS_ID = 6; 3600 private static final int ATTENDEES = 7; 3601 private static final int ATTENDEES_ID = 8; 3602 private static final int REMINDERS = 9; 3603 private static final int REMINDERS_ID = 10; 3604 private static final int EXTENDED_PROPERTIES = 11; 3605 private static final int EXTENDED_PROPERTIES_ID = 12; 3606 private static final int CALENDAR_ALERTS = 13; 3607 private static final int CALENDAR_ALERTS_ID = 14; 3608 private static final int CALENDAR_ALERTS_BY_INSTANCE = 15; 3609 private static final int INSTANCES_BY_DAY = 16; 3610 private static final int SYNCSTATE = 17; 3611 private static final int SYNCSTATE_ID = 18; 3612 private static final int EVENT_ENTITIES = 19; 3613 private static final int EVENT_ENTITIES_ID = 20; 3614 private static final int EVENT_DAYS = 21; 3615 private static final int SCHEDULE_ALARM = 22; 3616 private static final int SCHEDULE_ALARM_REMOVE = 23; 3617 private static final int TIME = 24; 3618 private static final int PROVIDER_PROPERTIES = 25; 3619 3620 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 3621 private static final HashMap<String, String> sInstancesProjectionMap; 3622 private static final HashMap<String, String> sEventsProjectionMap; 3623 private static final HashMap<String, String> sEventEntitiesProjectionMap; 3624 private static final HashMap<String, String> sAttendeesProjectionMap; 3625 private static final HashMap<String, String> sRemindersProjectionMap; 3626 private static final HashMap<String, String> sCalendarAlertsProjectionMap; 3627 private static final HashMap<String, String> sCalendarCacheProjectionMap; 3628 3629 static { 3630 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES); 3631 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY); 3632 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS); 3633 sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS); 3634 sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID); 3635 sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES); 3636 sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID); 3637 sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS); 3638 sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID); 3639 sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS); 3640 sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES); 3641 sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID); 3642 sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS); 3643 sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID); 3644 sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES); 3645 sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID); 3646 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS); 3647 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID); 3648 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance", 3649 CALENDAR_ALERTS_BY_INSTANCE); 3650 sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE); 3651 sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID); 3652 sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM); 3653 sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE); 3654 sUriMatcher.addURI(Calendar.AUTHORITY, "time/#", TIME); 3655 sUriMatcher.addURI(Calendar.AUTHORITY, "time", TIME); 3656 sUriMatcher.addURI(Calendar.AUTHORITY, "properties", PROVIDER_PROPERTIES); 3657 3658 sEventsProjectionMap = new HashMap<String, String>(); 3659 // Events columns 3660 sEventsProjectionMap.put(Events.HTML_URI, "htmlUri"); 3661 sEventsProjectionMap.put(Events.TITLE, "title"); 3662 sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); 3663 sEventsProjectionMap.put(Events.DESCRIPTION, "description"); 3664 sEventsProjectionMap.put(Events.STATUS, "eventStatus"); 3665 sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); 3666 sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); 3667 sEventsProjectionMap.put(Events.DTSTART, "dtstart"); 3668 sEventsProjectionMap.put(Events.DTEND, "dtend"); 3669 sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); 3670 sEventsProjectionMap.put(Events.DURATION, "duration"); 3671 sEventsProjectionMap.put(Events.ALL_DAY, "allDay"); 3672 sEventsProjectionMap.put(Events.VISIBILITY, "visibility"); 3673 sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency"); 3674 sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); 3675 sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); 3676 sEventsProjectionMap.put(Events.RRULE, "rrule"); 3677 sEventsProjectionMap.put(Events.RDATE, "rdate"); 3678 sEventsProjectionMap.put(Events.EXRULE, "exrule"); 3679 sEventsProjectionMap.put(Events.EXDATE, "exdate"); 3680 sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); 3681 sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); 3682 sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); 3683 sEventsProjectionMap.put(Events.LAST_DATE, "lastDate"); 3684 sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); 3685 sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); 3686 sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); 3687 sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); 3688 sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); 3689 sEventsProjectionMap.put(Events.ORGANIZER, "organizer"); 3690 sEventsProjectionMap.put(Events.DELETED, "deleted"); 3691 3692 // Put the shared items into the Attendees, Reminders projection map 3693 sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3694 sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3695 3696 // Calendar columns 3697 sEventsProjectionMap.put(Calendars.COLOR, "color"); 3698 sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level"); 3699 sEventsProjectionMap.put(Calendars.SELECTED, "selected"); 3700 sEventsProjectionMap.put(Calendars.URL, "url"); 3701 sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone"); 3702 sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount"); 3703 3704 // Put the shared items into the Instances projection map 3705 // The Instances and CalendarAlerts are joined with Calendars, so the projections include 3706 // the above Calendar columns. 3707 sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3708 sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3709 3710 sEventsProjectionMap.put(Events._ID, "_id"); 3711 sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id"); 3712 sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version"); 3713 sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time"); 3714 sEventsProjectionMap.put(Events._SYNC_DATA, "_sync_local_id"); 3715 sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty"); 3716 sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account"); 3717 sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, 3718 "_sync_account_type"); 3719 3720 sEventEntitiesProjectionMap = new HashMap<String, String>(); 3721 sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri"); 3722 sEventEntitiesProjectionMap.put(Events.TITLE, "title"); 3723 sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description"); 3724 sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); 3725 sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus"); 3726 sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); 3727 sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); 3728 sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart"); 3729 sEventEntitiesProjectionMap.put(Events.DTEND, "dtend"); 3730 sEventEntitiesProjectionMap.put(Events.DURATION, "duration"); 3731 sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); 3732 sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay"); 3733 sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility"); 3734 sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency"); 3735 sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); 3736 sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); 3737 sEventEntitiesProjectionMap.put(Events.RRULE, "rrule"); 3738 sEventEntitiesProjectionMap.put(Events.RDATE, "rdate"); 3739 sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule"); 3740 sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate"); 3741 sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); 3742 sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); 3743 sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); 3744 sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate"); 3745 sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); 3746 sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); 3747 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); 3748 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); 3749 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); 3750 sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer"); 3751 sEventEntitiesProjectionMap.put(Events.DELETED, "deleted"); 3752 sEventEntitiesProjectionMap.put(Events._ID, Events._ID); 3753 sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID); 3754 sEventEntitiesProjectionMap.put(Events._SYNC_DATA, Events._SYNC_DATA); 3755 sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION); 3756 sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY); 3757 sEventEntitiesProjectionMap.put(Calendars.URL, "url"); 3758 3759 // Instances columns 3760 sInstancesProjectionMap.put(Instances.BEGIN, "begin"); 3761 sInstancesProjectionMap.put(Instances.END, "end"); 3762 sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id"); 3763 sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id"); 3764 sInstancesProjectionMap.put(Instances.START_DAY, "startDay"); 3765 sInstancesProjectionMap.put(Instances.END_DAY, "endDay"); 3766 sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute"); 3767 sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute"); 3768 3769 // Attendees columns 3770 sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id"); 3771 sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id"); 3772 sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName"); 3773 sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail"); 3774 sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus"); 3775 sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship"); 3776 sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType"); 3777 3778 // Reminders columns 3779 sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id"); 3780 sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id"); 3781 sRemindersProjectionMap.put(Reminders.MINUTES, "minutes"); 3782 sRemindersProjectionMap.put(Reminders.METHOD, "method"); 3783 3784 // CalendarAlerts columns 3785 sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id"); 3786 sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id"); 3787 sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin"); 3788 sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end"); 3789 sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime"); 3790 sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state"); 3791 sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes"); 3792 3793 // CalendarCache columns 3794 sCalendarCacheProjectionMap = new HashMap<String, String>(); 3795 sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key"); 3796 sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value"); 3797 } 3798 3799 /** 3800 * Make sure that there are no entries for accounts that no longer 3801 * exist. We are overriding this since we need to delete from the 3802 * Calendars table, which is not syncable, which has triggers that 3803 * will delete from the Events and tables, which are 3804 * syncable. TODO: update comment, make sure deletes don't get synced. 3805 */ 3806 public void onAccountsUpdated(Account[] accounts) { 3807 mDb = mDbHelper.getWritableDatabase(); 3808 if (mDb == null) return; 3809 3810 HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>(); 3811 HashSet<Account> validAccounts = new HashSet<Account>(); 3812 for (Account account : accounts) { 3813 validAccounts.add(new Account(account.name, account.type)); 3814 accountHasCalendar.put(account, false); 3815 } 3816 ArrayList<Account> accountsToDelete = new ArrayList<Account>(); 3817 3818 mDb.beginTransaction(); 3819 try { 3820 3821 for (String table : new String[]{"Calendars"}) { 3822 // Find all the accounts the contacts DB knows about, mark the ones that aren't 3823 // in the valid set for deletion. 3824 Cursor c = mDb.rawQuery("SELECT DISTINCT " + CalendarDatabaseHelper.ACCOUNT_NAME 3825 + "," 3826 + CalendarDatabaseHelper.ACCOUNT_TYPE + " from " 3827 + table, null); 3828 while (c.moveToNext()) { 3829 if (c.getString(0) != null && c.getString(1) != null) { 3830 Account currAccount = new Account(c.getString(0), c.getString(1)); 3831 if (!validAccounts.contains(currAccount)) { 3832 accountsToDelete.add(currAccount); 3833 } 3834 } 3835 } 3836 c.close(); 3837 } 3838 3839 for (Account account : accountsToDelete) { 3840 if (Log.isLoggable(TAG, Log.DEBUG)) { 3841 Log.d(TAG, "removing data for removed account " + account); 3842 } 3843 String[] params = new String[]{account.name, account.type}; 3844 mDb.execSQL("DELETE FROM Calendars" 3845 + " WHERE " + CalendarDatabaseHelper.ACCOUNT_NAME + "= ? AND " 3846 + CalendarDatabaseHelper.ACCOUNT_TYPE 3847 + "= ?", params); 3848 } 3849 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 3850 mDb.setTransactionSuccessful(); 3851 } finally { 3852 mDb.endTransaction(); 3853 } 3854 } 3855 3856 /* package */ static boolean readBooleanQueryParameter(Uri uri, String name, 3857 boolean defaultValue) { 3858 final String flag = getQueryParameter(uri, name); 3859 return flag == null 3860 ? defaultValue 3861 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase())); 3862 } 3863 3864 // Duplicated from ContactsProvider2. TODO: a utility class for shared code 3865 /** 3866 * A fast re-implementation of {@link Uri#getQueryParameter} 3867 */ 3868 /* package */ static String getQueryParameter(Uri uri, String parameter) { 3869 String query = uri.getEncodedQuery(); 3870 if (query == null) { 3871 return null; 3872 } 3873 3874 int queryLength = query.length(); 3875 int parameterLength = parameter.length(); 3876 3877 String value; 3878 int index = 0; 3879 while (true) { 3880 index = query.indexOf(parameter, index); 3881 if (index == -1) { 3882 return null; 3883 } 3884 3885 index += parameterLength; 3886 3887 if (queryLength == index) { 3888 return null; 3889 } 3890 3891 if (query.charAt(index) == '=') { 3892 index++; 3893 break; 3894 } 3895 } 3896 3897 int ampIndex = query.indexOf('&', index); 3898 if (ampIndex == -1) { 3899 value = query.substring(index); 3900 } else { 3901 value = query.substring(index, ampIndex); 3902 } 3903 3904 return Uri.decode(value); 3905 } 3906 3907 /** 3908 * Inserts an argument at the beginning of the selection arg list. 3909 * 3910 * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is 3911 * prepended to the user's where clause (combined with 'AND') to generate 3912 * the final where close, so arguments associated with the QueryBuilder are 3913 * prepended before any user selection args to keep them in the right order. 3914 */ 3915 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 3916 if (selectionArgs == null) { 3917 return new String[] {arg}; 3918 } else { 3919 int newLength = selectionArgs.length + 1; 3920 String[] newSelectionArgs = new String[newLength]; 3921 newSelectionArgs[0] = arg; 3922 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 3923 return newSelectionArgs; 3924 } 3925 } 3926 } 3927