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