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