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 InstancesRange(long begin, long end)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> { add(String syncIdKey, ContentValues values)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 AlarmScheduler(boolean removeAlarms)218 public AlarmScheduler(boolean removeAlarms) { 219 mRemoveAlarms = removeAlarms; 220 } 221 222 @Override run()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 verifyAccounts()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 getDatabaseHelper(final Context context)398 protected CalendarDatabaseHelper getDatabaseHelper(final Context context) { 399 return CalendarDatabaseHelper.getInstance(context); 400 } 401 402 @Override onCreate()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 */ updateTimezoneDependentFields()436 protected void updateTimezoneDependentFields() { 437 Thread thread = new TimezoneCheckerThread(); 438 thread.start(); 439 } 440 441 private class TimezoneCheckerThread extends Thread { 442 @Override run()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 */ isLocalSameAsInstancesTimezone()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 */ doUpdateTimezoneDependentFields()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 doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion)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 updateEventsStartEndFromEventRawTimesLocked()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 get2445ToMillis(String timezone, String dt2445)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 updateEventsStartEndLocked(long eventId, String timezone, String dtStart2445, String dtEnd2445)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 updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion)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 */ isSameTimezoneDatabaseVersion()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 getTimezoneDatabaseVersion()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 isHomeTimezone()611 private boolean isHomeTimezone() { 612 String type = mCalendarCache.readTimezoneType(); 613 return type.equals(CalendarCache.TIMEZONE_TYPE_HOME); 614 } 615 regenerateInstancesTable()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 rescheduleMissedAlarms()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 */ appendIds(StringBuilder sb, HashSet<Long> ids)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 notifyChange()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 query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit)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 */ handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, long rangeEnd, String[] projection, String selection, String sort, boolean searchByDay, boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone)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 handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end, String[] projection, String selection, String instancesTimezone, boolean isHomeTimezone)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 */ acquireInstanceRange(final long begin, final long end, final boolean useMinimumExpansionWindow, final boolean forceExpansion, final String instancesTimezone, final boolean isHomeTimezone)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 */ acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow, boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone)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 */ expandInstanceRangeLocked(long begin, long end, String localTimezone)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 */ getEntries(long begin, long end)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 */ getSyncIdKey(String syncId, long calendarId)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 */ performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries)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 */ computeTimezoneDependentFields(long begin, long end, Time local, ContentValues values)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 getType(Uri url)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 isRecurrenceEvent(String rrule, String rdate, String originalEvent)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 */ fixAllDayTime(Uri uri, ContentValues updatedValues)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 insertInTransaction(Uri uri, ContentValues values)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 */ validateEventData(ContentValues values)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 setEventDirty(int eventId)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 */ getOwner(long calId)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 */ createAttendeeEntry(long eventId, int status, String emailAddress)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 */ updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues)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 */ updateInstancesLocked(ContentValues values, long rowId, boolean newEvent, SQLiteDatabase db)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 */ getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId)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 */ updateRecurrenceInstancesLocked(ContentValues values, long rowId, SQLiteDatabase db)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 calculateLastDate(ContentValues values)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 */ updateLastDate(ContentValues values)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 updateEventRawTimesLocked(long eventId, ContentValues values)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 deleteInTransaction(Uri uri, String selection, String[] selectionArgs)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 deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch)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 */ deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs)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 */ updateInTable(String table, ContentValues values, Uri uri, String selection, String[] selectionArgs)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 deleteMatchingCalendars(String where)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 getCursorForEventIdAndProjection(String eventId, String[] projection)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 doesEventExistForSyncId(String syncId)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 doesStatusCancelUpdateMeanUpdate(String eventId, ContentValues values)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 updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs)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 appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri)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 appendAccountToSelection(Uri uri, String selection)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 modifyCalendarSubscription(long id, boolean syncEvents)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 */ triggerAppWidgetUpdate(long changedEventId)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 */ getAlarmManager()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 scheduleNextAlarmCheck(long triggerTime)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 */ scheduleNextAlarm(boolean removeAlarms)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 */ runScheduleNextAlarm(boolean removeAlarms)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 */ scheduleNextAlarmLocked(SQLiteDatabase db)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 */ removeScheduledAlarmsLocked(SQLiteDatabase db)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 { sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES)3630 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES); sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY)3631 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY); sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS)3632 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS); sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS)3633 sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS); sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID)3634 sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES)3635 sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES); sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID)3636 sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS)3637 sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS); sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID)3638 sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS)3639 sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS); sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES)3640 sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES); sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID)3641 sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS)3642 sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS); sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID)3643 sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES)3644 sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES); sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID)3645 sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS)3646 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS); sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID)3647 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID); sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance", CALENDAR_ALERTS_BY_INSTANCE)3648 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance", 3649 CALENDAR_ALERTS_BY_INSTANCE); sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE)3650 sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE); sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID)3651 sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID); sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM)3652 sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM); sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE)3653 sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE); sUriMatcher.addURI(Calendar.AUTHORITY, "time/#", TIME)3654 sUriMatcher.addURI(Calendar.AUTHORITY, "time/#", TIME); sUriMatcher.addURI(Calendar.AUTHORITY, "time", TIME)3655 sUriMatcher.addURI(Calendar.AUTHORITY, "time", TIME); sUriMatcher.addURI(Calendar.AUTHORITY, "properties", PROVIDER_PROPERTIES)3656 sUriMatcher.addURI(Calendar.AUTHORITY, "properties", PROVIDER_PROPERTIES); 3657 3658 sEventsProjectionMap = new HashMap<String, String>(); 3659 // Events columns sEventsProjectionMap.put(Events.HTML_URI, "htmlUri")3660 sEventsProjectionMap.put(Events.HTML_URI, "htmlUri"); sEventsProjectionMap.put(Events.TITLE, "title")3661 sEventsProjectionMap.put(Events.TITLE, "title"); sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation")3662 sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); sEventsProjectionMap.put(Events.DESCRIPTION, "description")3663 sEventsProjectionMap.put(Events.DESCRIPTION, "description"); sEventsProjectionMap.put(Events.STATUS, "eventStatus")3664 sEventsProjectionMap.put(Events.STATUS, "eventStatus"); sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus")3665 sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri")3666 sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); sEventsProjectionMap.put(Events.DTSTART, "dtstart")3667 sEventsProjectionMap.put(Events.DTSTART, "dtstart"); sEventsProjectionMap.put(Events.DTEND, "dtend")3668 sEventsProjectionMap.put(Events.DTEND, "dtend"); sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone")3669 sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); sEventsProjectionMap.put(Events.DURATION, "duration")3670 sEventsProjectionMap.put(Events.DURATION, "duration"); sEventsProjectionMap.put(Events.ALL_DAY, "allDay")3671 sEventsProjectionMap.put(Events.ALL_DAY, "allDay"); sEventsProjectionMap.put(Events.VISIBILITY, "visibility")3672 sEventsProjectionMap.put(Events.VISIBILITY, "visibility"); sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency")3673 sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency"); sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm")3674 sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties")3675 sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); sEventsProjectionMap.put(Events.RRULE, "rrule")3676 sEventsProjectionMap.put(Events.RRULE, "rrule"); sEventsProjectionMap.put(Events.RDATE, "rdate")3677 sEventsProjectionMap.put(Events.RDATE, "rdate"); sEventsProjectionMap.put(Events.EXRULE, "exrule")3678 sEventsProjectionMap.put(Events.EXRULE, "exrule"); sEventsProjectionMap.put(Events.EXDATE, "exdate")3679 sEventsProjectionMap.put(Events.EXDATE, "exdate"); sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent")3680 sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime")3681 sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay")3682 sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); sEventsProjectionMap.put(Events.LAST_DATE, "lastDate")3683 sEventsProjectionMap.put(Events.LAST_DATE, "lastDate"); sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData")3684 sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id")3685 sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers")3686 sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify")3687 sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests")3688 sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); sEventsProjectionMap.put(Events.ORGANIZER, "organizer")3689 sEventsProjectionMap.put(Events.ORGANIZER, "organizer"); sEventsProjectionMap.put(Events.DELETED, "deleted")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 sEventsProjectionMap.put(Calendars.COLOR, "color")3697 sEventsProjectionMap.put(Calendars.COLOR, "color"); sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level")3698 sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level"); sEventsProjectionMap.put(Calendars.SELECTED, "selected")3699 sEventsProjectionMap.put(Calendars.SELECTED, "selected"); sEventsProjectionMap.put(Calendars.URL, "url")3700 sEventsProjectionMap.put(Calendars.URL, "url"); sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone")3701 sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone"); sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount")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 sEventsProjectionMap.put(Events._ID, "_id")3710 sEventsProjectionMap.put(Events._ID, "_id"); sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id")3711 sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id"); sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version")3712 sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version"); sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time")3713 sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time"); sEventsProjectionMap.put(Events._SYNC_DATA, "_sync_local_id")3714 sEventsProjectionMap.put(Events._SYNC_DATA, "_sync_local_id"); sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty")3715 sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty"); sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account")3716 sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account"); sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, "_sync_account_type")3717 sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, 3718 "_sync_account_type"); 3719 3720 sEventEntitiesProjectionMap = new HashMap<String, String>(); sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri")3721 sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri"); sEventEntitiesProjectionMap.put(Events.TITLE, "title")3722 sEventEntitiesProjectionMap.put(Events.TITLE, "title"); sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description")3723 sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description"); sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation")3724 sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus")3725 sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus"); sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus")3726 sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri")3727 sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart")3728 sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart"); sEventEntitiesProjectionMap.put(Events.DTEND, "dtend")3729 sEventEntitiesProjectionMap.put(Events.DTEND, "dtend"); sEventEntitiesProjectionMap.put(Events.DURATION, "duration")3730 sEventEntitiesProjectionMap.put(Events.DURATION, "duration"); sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone")3731 sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay")3732 sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay"); sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility")3733 sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility"); sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency")3734 sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency"); sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm")3735 sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties")3736 sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); sEventEntitiesProjectionMap.put(Events.RRULE, "rrule")3737 sEventEntitiesProjectionMap.put(Events.RRULE, "rrule"); sEventEntitiesProjectionMap.put(Events.RDATE, "rdate")3738 sEventEntitiesProjectionMap.put(Events.RDATE, "rdate"); sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule")3739 sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule"); sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate")3740 sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate"); sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent")3741 sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime")3742 sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay")3743 sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate")3744 sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate"); sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData")3745 sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id")3746 sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers")3747 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify")3748 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests")3749 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer")3750 sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer"); sEventEntitiesProjectionMap.put(Events.DELETED, "deleted")3751 sEventEntitiesProjectionMap.put(Events.DELETED, "deleted"); sEventEntitiesProjectionMap.put(Events._ID, Events._ID)3752 sEventEntitiesProjectionMap.put(Events._ID, Events._ID); sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID)3753 sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID); sEventEntitiesProjectionMap.put(Events._SYNC_DATA, Events._SYNC_DATA)3754 sEventEntitiesProjectionMap.put(Events._SYNC_DATA, Events._SYNC_DATA); sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION)3755 sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION); sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY)3756 sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY); sEventEntitiesProjectionMap.put(Calendars.URL, "url")3757 sEventEntitiesProjectionMap.put(Calendars.URL, "url"); 3758 3759 // Instances columns sInstancesProjectionMap.put(Instances.BEGIN, "begin")3760 sInstancesProjectionMap.put(Instances.BEGIN, "begin"); sInstancesProjectionMap.put(Instances.END, "end")3761 sInstancesProjectionMap.put(Instances.END, "end"); sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id")3762 sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id"); sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id")3763 sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id"); sInstancesProjectionMap.put(Instances.START_DAY, "startDay")3764 sInstancesProjectionMap.put(Instances.START_DAY, "startDay"); sInstancesProjectionMap.put(Instances.END_DAY, "endDay")3765 sInstancesProjectionMap.put(Instances.END_DAY, "endDay"); sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute")3766 sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute"); sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute")3767 sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute"); 3768 3769 // Attendees columns sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id")3770 sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id"); sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id")3771 sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName")3772 sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail")3773 sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus")3774 sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship")3775 sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType")3776 sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType"); 3777 3778 // Reminders columns sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id")3779 sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id"); sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id")3780 sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id"); sRemindersProjectionMap.put(Reminders.MINUTES, "minutes")3781 sRemindersProjectionMap.put(Reminders.MINUTES, "minutes"); sRemindersProjectionMap.put(Reminders.METHOD, "method")3782 sRemindersProjectionMap.put(Reminders.METHOD, "method"); 3783 3784 // CalendarAlerts columns sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id")3785 sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id"); sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id")3786 sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id"); sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin")3787 sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin"); sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end")3788 sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end"); sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime")3789 sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime"); sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state")3790 sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state"); sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes")3791 sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes"); 3792 3793 // CalendarCache columns 3794 sCalendarCacheProjectionMap = new HashMap<String, String>(); sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key")3795 sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key"); sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value")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 */ onAccountsUpdated(Account[] accounts)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 readBooleanQueryParameter(Uri uri, String name, boolean defaultValue)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 */ getQueryParameter(Uri uri, String parameter)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 */ insertSelectionArg(String[] selectionArgs, String arg)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