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