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