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