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