CalendarProvider2.java revision f09652d7327e45711f0e5b210e4df9c4c4c78ac4
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 if (calId < 0) { 1500 Log.e(TAG, "Calendar Id is not valid: " + calId); 1501 return null; 1502 } 1503 // Get the email address of this user from this Calendar 1504 String emailAddress = null; 1505 Cursor cursor = null; 1506 try { 1507 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 1508 new String[] { Calendars.OWNER_ACCOUNT }, 1509 null /* selection */, 1510 null /* selectionArgs */, 1511 null /* sort */); 1512 if (cursor == null || !cursor.moveToFirst()) { 1513 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 1514 return null; 1515 } 1516 emailAddress = cursor.getString(0); 1517 } finally { 1518 if (cursor != null) { 1519 cursor.close(); 1520 } 1521 } 1522 return emailAddress; 1523 } 1524 1525 /** 1526 * Creates an entry in the Attendees table that refers to the given event 1527 * and that has the given response status. 1528 * 1529 * @param eventId the event id that the new entry in the Attendees table 1530 * should refer to 1531 * @param status the response status 1532 * @param emailAddress the email of the attendee 1533 */ 1534 private void createAttendeeEntry(long eventId, int status, String emailAddress) { 1535 ContentValues values = new ContentValues(); 1536 values.put(Attendees.EVENT_ID, eventId); 1537 values.put(Attendees.ATTENDEE_STATUS, status); 1538 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 1539 // TODO: The relationship could actually be ORGANIZER, but it will get straightened out 1540 // on sync. 1541 values.put(Attendees.ATTENDEE_RELATIONSHIP, 1542 Attendees.RELATIONSHIP_ATTENDEE); 1543 values.put(Attendees.ATTENDEE_EMAIL, emailAddress); 1544 1545 // We don't know the ATTENDEE_NAME but that will be filled in by the 1546 // server and sent back to us. 1547 mDbHelper.attendeesInsert(values); 1548 } 1549 1550 /** 1551 * Updates the attendee status in the Events table to be consistent with 1552 * the value in the Attendees table. 1553 * 1554 * @param db the database 1555 * @param attendeeValues the column values for one row in the Attendees 1556 * table. 1557 */ 1558 private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) { 1559 // Get the event id for this attendee 1560 long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID); 1561 1562 if (MULTIPLE_ATTENDEES_PER_EVENT) { 1563 // Get the calendar id for this event 1564 Cursor cursor = null; 1565 long calId; 1566 try { 1567 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 1568 new String[] { Events.CALENDAR_ID }, 1569 null /* selection */, 1570 null /* selectionArgs */, 1571 null /* sort */); 1572 if (cursor == null || !cursor.moveToFirst()) { 1573 Log.d(TAG, "Couldn't find " + eventId + " in Events table"); 1574 return; 1575 } 1576 calId = cursor.getLong(0); 1577 } finally { 1578 if (cursor != null) { 1579 cursor.close(); 1580 } 1581 } 1582 1583 // Get the owner email for this Calendar 1584 String calendarEmail = null; 1585 cursor = null; 1586 try { 1587 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 1588 new String[] { Calendars.OWNER_ACCOUNT }, 1589 null /* selection */, 1590 null /* selectionArgs */, 1591 null /* sort */); 1592 if (cursor == null || !cursor.moveToFirst()) { 1593 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 1594 return; 1595 } 1596 calendarEmail = cursor.getString(0); 1597 } finally { 1598 if (cursor != null) { 1599 cursor.close(); 1600 } 1601 } 1602 1603 if (calendarEmail == null) { 1604 return; 1605 } 1606 1607 // Get the email address for this attendee 1608 String attendeeEmail = null; 1609 if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 1610 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL); 1611 } 1612 1613 // If the attendee email does not match the calendar email, then this 1614 // attendee is not the owner of this calendar so we don't update the 1615 // selfAttendeeStatus in the event. 1616 if (!calendarEmail.equals(attendeeEmail)) { 1617 return; 1618 } 1619 } 1620 1621 int status = Attendees.ATTENDEE_STATUS_NONE; 1622 if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) { 1623 int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 1624 if (rel == Attendees.RELATIONSHIP_ORGANIZER) { 1625 status = Attendees.ATTENDEE_STATUS_ACCEPTED; 1626 } 1627 } 1628 1629 if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) { 1630 status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS); 1631 } 1632 1633 ContentValues values = new ContentValues(); 1634 values.put(Events.SELF_ATTENDEE_STATUS, status); 1635 db.update("Events", values, "_id=?", new String[] {String.valueOf(eventId)}); 1636 } 1637 1638 /** 1639 * Updates the instances table when an event is added or updated. 1640 * @param values The new values of the event. 1641 * @param rowId The database row id of the event. 1642 * @param newEvent true if the event is new. 1643 * @param db The database 1644 */ 1645 private void updateInstancesLocked(ContentValues values, 1646 long rowId, 1647 boolean newEvent, 1648 SQLiteDatabase db) { 1649 1650 // If there are no expanded Instances, then return. 1651 MetaData.Fields fields = mMetaData.getFieldsLocked(); 1652 if (fields.maxInstance == 0) { 1653 return; 1654 } 1655 1656 Long dtstartMillis = values.getAsLong(Events.DTSTART); 1657 if (dtstartMillis == null) { 1658 if (newEvent) { 1659 // must be present for a new event. 1660 throw new RuntimeException("DTSTART missing."); 1661 } 1662 if (Config.LOGV) Log.v(TAG, "Missing DTSTART. " 1663 + "No need to update instance."); 1664 return; 1665 } 1666 1667 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 1668 Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1669 1670 if (!newEvent) { 1671 // Want to do this for regular event, recurrence, or exception. 1672 // For recurrence or exception, more deletion may happen below if we 1673 // do an instance expansion. This deletion will suffice if the exception 1674 // is moved outside the window, for instance. 1675 db.delete("Instances", "event_id=?", new String[] {String.valueOf(rowId)}); 1676 } 1677 1678 if (isRecurrenceEvent(values)) { 1679 // The recurrence or exception needs to be (re-)expanded if: 1680 // a) Exception or recurrence that falls inside window 1681 boolean insideWindow = dtstartMillis <= fields.maxInstance && 1682 (lastDateMillis == null || lastDateMillis >= fields.minInstance); 1683 // b) Exception that affects instance inside window 1684 // These conditions match the query in getEntries 1685 // See getEntries comment for explanation of subtracting 1 week. 1686 boolean affectsWindow = originalInstanceTime != null && 1687 originalInstanceTime <= fields.maxInstance && 1688 originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION; 1689 if (insideWindow || affectsWindow) { 1690 updateRecurrenceInstancesLocked(values, rowId, db); 1691 } 1692 // TODO: an exception creation or update could be optimized by 1693 // updating just the affected instances, instead of regenerating 1694 // the recurrence. 1695 return; 1696 } 1697 1698 Long dtendMillis = values.getAsLong(Events.DTEND); 1699 if (dtendMillis == null) { 1700 dtendMillis = dtstartMillis; 1701 } 1702 1703 // if the event is in the expanded range, insert 1704 // into the instances table. 1705 // TODO: deal with durations. currently, durations are only used in 1706 // recurrences. 1707 1708 if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) { 1709 ContentValues instanceValues = new ContentValues(); 1710 instanceValues.put(Instances.EVENT_ID, rowId); 1711 instanceValues.put(Instances.BEGIN, dtstartMillis); 1712 instanceValues.put(Instances.END, dtendMillis); 1713 1714 boolean allDay = false; 1715 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 1716 if (allDayInteger != null) { 1717 allDay = allDayInteger != 0; 1718 } 1719 1720 // Update the timezone-dependent fields. 1721 Time local = new Time(); 1722 if (allDay) { 1723 local.timezone = Time.TIMEZONE_UTC; 1724 } else { 1725 local.timezone = fields.timezone; 1726 } 1727 1728 computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues); 1729 mDbHelper.instancesInsert(instanceValues); 1730 } 1731 } 1732 1733 /** 1734 * Determines the recurrence entries associated with a particular recurrence. 1735 * This set is the base recurrence and any exception. 1736 * 1737 * Normally the entries are indicated by the sync id of the base recurrence 1738 * (which is the originalEvent in the exceptions). 1739 * However, a complication is that a recurrence may not yet have a sync id. 1740 * In that case, the recurrence is specified by the rowId. 1741 * 1742 * @param recurrenceSyncId The sync id of the base recurrence, or null. 1743 * @param rowId The row id of the base recurrence. 1744 * @return the relevant entries. 1745 */ 1746 private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) { 1747 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1748 1749 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 1750 qb.setProjectionMap(sEventsProjectionMap); 1751 String selectionArgs[]; 1752 if (recurrenceSyncId == null) { 1753 String where = "_id =?"; 1754 qb.appendWhere(where); 1755 selectionArgs = new String[] {String.valueOf(rowId)}; 1756 } else { 1757 String where = "_sync_id = ? OR originalEvent = ?"; 1758 qb.appendWhere(where); 1759 selectionArgs = new String[] {recurrenceSyncId, recurrenceSyncId}; 1760 } 1761 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1762 Log.v(TAG, "Retrieving events to expand: " + qb.toString()); 1763 } 1764 1765 return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs, 1766 null /* groupBy */, null /* having */, null /* sortOrder */); 1767 } 1768 1769 /** 1770 * Do incremental Instances update of a recurrence or recurrence exception. 1771 * 1772 * This method does performInstanceExpansion on just the modified recurrence, 1773 * to avoid the overhead of recomputing the entire instance table. 1774 * 1775 * @param values The new values of the event. 1776 * @param rowId The database row id of the event. 1777 * @param db The database 1778 */ 1779 private void updateRecurrenceInstancesLocked(ContentValues values, 1780 long rowId, 1781 SQLiteDatabase db) { 1782 MetaData.Fields fields = mMetaData.getFieldsLocked(); 1783 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 1784 String recurrenceSyncId = null; 1785 if (originalEvent != null) { 1786 recurrenceSyncId = originalEvent; 1787 } else { 1788 // Get the recurrence's sync id from the database 1789 recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events" 1790 + " WHERE _id=?", new String[] {String.valueOf(rowId)}); 1791 } 1792 // recurrenceSyncId is the _sync_id of the underlying recurrence 1793 // If the recurrence hasn't gone to the server, it will be null. 1794 1795 // Need to clear out old instances 1796 if (recurrenceSyncId == null) { 1797 // Creating updating a recurrence that hasn't gone to the server. 1798 // Need to delete based on row id 1799 String where = "_id IN (SELECT Instances._id as _id" 1800 + " FROM Instances INNER JOIN Events" 1801 + " ON (Events._id = Instances.event_id)" 1802 + " WHERE Events._id =?)"; 1803 db.delete("Instances", where, new String[]{"" + rowId}); 1804 } else { 1805 // Creating or modifying a recurrence or exception. 1806 // Delete instances for recurrence (_sync_id = recurrenceSyncId) 1807 // and all exceptions (originalEvent = recurrenceSyncId) 1808 String where = "_id IN (SELECT Instances._id as _id" 1809 + " FROM Instances INNER JOIN Events" 1810 + " ON (Events._id = Instances.event_id)" 1811 + " WHERE Events._sync_id =?" 1812 + " OR Events.originalEvent =?)"; 1813 db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId}); 1814 } 1815 1816 // Now do instance expansion 1817 Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId); 1818 try { 1819 performInstanceExpansion(fields.minInstance, fields.maxInstance, fields.timezone, 1820 entries); 1821 } finally { 1822 if (entries != null) { 1823 entries.close(); 1824 } 1825 } 1826 1827 // Clear busy bits (is this still needed?) 1828 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance); 1829 } 1830 1831 long calculateLastDate(ContentValues values) 1832 throws DateException { 1833 // Allow updates to some event fields like the title or hasAlarm 1834 // without requiring DTSTART. 1835 if (!values.containsKey(Events.DTSTART)) { 1836 if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE) 1837 || values.containsKey(Events.DURATION) 1838 || values.containsKey(Events.EVENT_TIMEZONE) 1839 || values.containsKey(Events.RDATE) 1840 || values.containsKey(Events.EXRULE) 1841 || values.containsKey(Events.EXDATE)) { 1842 throw new RuntimeException("DTSTART field missing from event"); 1843 } 1844 return -1; 1845 } 1846 long dtstartMillis = values.getAsLong(Events.DTSTART); 1847 long lastMillis = -1; 1848 1849 // Can we use dtend with a repeating event? What does that even 1850 // mean? 1851 // NOTE: if the repeating event has a dtend, we convert it to a 1852 // duration during event processing, so this situation should not 1853 // occur. 1854 Long dtEnd = values.getAsLong(Events.DTEND); 1855 if (dtEnd != null) { 1856 lastMillis = dtEnd; 1857 } else { 1858 // find out how long it is 1859 Duration duration = new Duration(); 1860 String durationStr = values.getAsString(Events.DURATION); 1861 if (durationStr != null) { 1862 duration.parse(durationStr); 1863 } 1864 1865 RecurrenceSet recur = new RecurrenceSet(values); 1866 1867 if (recur.hasRecurrence()) { 1868 // the event is repeating, so find the last date it 1869 // could appear on 1870 1871 String tz = values.getAsString(Events.EVENT_TIMEZONE); 1872 1873 if (TextUtils.isEmpty(tz)) { 1874 // floating timezone 1875 tz = Time.TIMEZONE_UTC; 1876 } 1877 Time dtstartLocal = new Time(tz); 1878 1879 dtstartLocal.set(dtstartMillis); 1880 1881 RecurrenceProcessor rp = new RecurrenceProcessor(); 1882 lastMillis = rp.getLastOccurence(dtstartLocal, recur); 1883 if (lastMillis == -1) { 1884 return lastMillis; // -1 1885 } 1886 } else { 1887 // the event is not repeating, just use dtstartMillis 1888 lastMillis = dtstartMillis; 1889 } 1890 1891 // that was the beginning of the event. this is the end. 1892 lastMillis = duration.addTo(lastMillis); 1893 } 1894 return lastMillis; 1895 } 1896 1897 private ContentValues updateContentValuesFromEvent(ContentValues initialValues) { 1898 try { 1899 ContentValues values = new ContentValues(initialValues); 1900 1901 long last = calculateLastDate(values); 1902 if (last != -1) { 1903 values.put(Events.LAST_DATE, last); 1904 } 1905 1906 return values; 1907 } catch (DateException e) { 1908 // don't add it if there was an error 1909 Log.w(TAG, "Could not calculate last date.", e); 1910 return null; 1911 } 1912 } 1913 1914 private void updateEventRawTimesLocked(long eventId, ContentValues values) { 1915 ContentValues rawValues = new ContentValues(); 1916 1917 rawValues.put("event_id", eventId); 1918 1919 String timezone = values.getAsString(Events.EVENT_TIMEZONE); 1920 1921 boolean allDay = false; 1922 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 1923 if (allDayInteger != null) { 1924 allDay = allDayInteger != 0; 1925 } 1926 1927 if (allDay || TextUtils.isEmpty(timezone)) { 1928 // floating timezone 1929 timezone = Time.TIMEZONE_UTC; 1930 } 1931 1932 Time time = new Time(timezone); 1933 time.allDay = allDay; 1934 Long dtstartMillis = values.getAsLong(Events.DTSTART); 1935 if (dtstartMillis != null) { 1936 time.set(dtstartMillis); 1937 rawValues.put("dtstart2445", time.format2445()); 1938 } 1939 1940 Long dtendMillis = values.getAsLong(Events.DTEND); 1941 if (dtendMillis != null) { 1942 time.set(dtendMillis); 1943 rawValues.put("dtend2445", time.format2445()); 1944 } 1945 1946 Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1947 if (originalInstanceMillis != null) { 1948 // This is a recurrence exception so we need to get the all-day 1949 // status of the original recurring event in order to format the 1950 // date correctly. 1951 allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY); 1952 if (allDayInteger != null) { 1953 time.allDay = allDayInteger != 0; 1954 } 1955 time.set(originalInstanceMillis); 1956 rawValues.put("originalInstanceTime2445", time.format2445()); 1957 } 1958 1959 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 1960 if (lastDateMillis != null) { 1961 time.allDay = allDay; 1962 time.set(lastDateMillis); 1963 rawValues.put("lastDate2445", time.format2445()); 1964 } 1965 1966 mDbHelper.eventsRawTimesReplace(rawValues); 1967 } 1968 1969 @Override 1970 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 1971 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1972 Log.v(TAG, "deleteInTransaction: " + uri); 1973 } 1974 final boolean callerIsSyncAdapter = 1975 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false); 1976 final int match = sUriMatcher.match(uri); 1977 switch (match) { 1978 case SYNCSTATE: 1979 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 1980 1981 case SYNCSTATE_ID: 1982 String selectionWithId = 1983 (BaseColumns._ID + "=" + ContentUris.parseId(uri) + " ") 1984 + (selection == null ? "" : " AND (" + selection + ")"); 1985 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 1986 1987 case EVENTS: 1988 { 1989 int result = 0; 1990 selection = appendAccountToSelection(uri, selection); 1991 1992 // Query this event to get the ids to delete. 1993 Cursor cursor = mDb.query("Events", ID_ONLY_PROJECTION, 1994 selection, selectionArgs, null /* groupBy */, 1995 null /* having */, null /* sortOrder */); 1996 try { 1997 while (cursor.moveToNext()) { 1998 long id = cursor.getLong(0); 1999 result += deleteEventInternal(id, callerIsSyncAdapter); 2000 } 2001 } finally { 2002 cursor.close(); 2003 cursor = null; 2004 } 2005 return result; 2006 } 2007 case EVENTS_ID: 2008 { 2009 long id = ContentUris.parseId(uri); 2010 if (selection != null) { 2011 throw new UnsupportedOperationException("CalendarProvider2 " 2012 + "doesn't support selection based deletion for type " 2013 + match); 2014 } 2015 return deleteEventInternal(id, callerIsSyncAdapter); 2016 } 2017 case ATTENDEES: 2018 { 2019 if (callerIsSyncAdapter) { 2020 return mDb.delete("Attendees", selection, selectionArgs); 2021 } else { 2022 return deleteFromTable("Attendees", uri, selection, selectionArgs); 2023 } 2024 } 2025 case ATTENDEES_ID: 2026 { 2027 if (selection != null) { 2028 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2029 } 2030 if (callerIsSyncAdapter) { 2031 long id = ContentUris.parseId(uri); 2032 return mDb.delete("Attendees", "_id=?", new String[] {String.valueOf(id)}); 2033 } else { 2034 return deleteFromTable("Attendees", uri, null /* selection */, 2035 null /* selectionArgs */); 2036 } 2037 } 2038 case REMINDERS: 2039 { 2040 if (callerIsSyncAdapter) { 2041 return mDb.delete("Reminders", selection, selectionArgs); 2042 } else { 2043 return deleteFromTable("Reminders", uri, selection, selectionArgs); 2044 } 2045 } 2046 case REMINDERS_ID: 2047 { 2048 if (selection != null) { 2049 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2050 } 2051 if (callerIsSyncAdapter) { 2052 long id = ContentUris.parseId(uri); 2053 return mDb.delete("Reminders", "_id=?", new String[] {String.valueOf(id)}); 2054 } else { 2055 return deleteFromTable("Reminders", uri, null /* selection */, 2056 null /* selectionArgs */); 2057 } 2058 } 2059 case EXTENDED_PROPERTIES: 2060 { 2061 if (callerIsSyncAdapter) { 2062 return mDb.delete("ExtendedProperties", selection, selectionArgs); 2063 } else { 2064 return deleteFromTable("ExtendedProperties", uri, selection, selectionArgs); 2065 } 2066 } 2067 case EXTENDED_PROPERTIES_ID: 2068 { 2069 if (selection != null) { 2070 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2071 } 2072 if (callerIsSyncAdapter) { 2073 long id = ContentUris.parseId(uri); 2074 return mDb.delete("ExtendedProperties", "_id=?", 2075 new String[] {String.valueOf(id)}); 2076 } else { 2077 return deleteFromTable("ExtendedProperties", uri, null /* selection */, 2078 null /* selectionArgs */); 2079 } 2080 } 2081 case CALENDAR_ALERTS: 2082 { 2083 if (callerIsSyncAdapter) { 2084 return mDb.delete("CalendarAlerts", selection, selectionArgs); 2085 } else { 2086 return deleteFromTable("CalendarAlerts", uri, selection, selectionArgs); 2087 } 2088 } 2089 case CALENDAR_ALERTS_ID: 2090 { 2091 if (selection != null) { 2092 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2093 } 2094 // Note: dirty bit is not set for Alerts because it is not synced. 2095 // It is generated from Reminders, which is synced. 2096 long id = ContentUris.parseId(uri); 2097 return mDb.delete("CalendarAlerts", "_id=?", new String[] {String.valueOf(id)}); 2098 } 2099 case DELETED_EVENTS: 2100 throw new UnsupportedOperationException("Cannot delete that URL: " + uri); 2101 case CALENDARS_ID: 2102 StringBuilder selectionSb = new StringBuilder("_id="); 2103 selectionSb.append(uri.getPathSegments().get(1)); 2104 if (!TextUtils.isEmpty(selection)) { 2105 selectionSb.append(" AND ("); 2106 selectionSb.append(selection); 2107 selectionSb.append(')'); 2108 } 2109 selection = selectionSb.toString(); 2110 // fall through to CALENDARS for the actual delete 2111 case CALENDARS: 2112 selection = appendAccountToSelection(uri, selection); 2113 return deleteMatchingCalendars(selection); // TODO: handle in sync adapter 2114 case INSTANCES: 2115 case INSTANCES_BY_DAY: 2116 case EVENT_DAYS: 2117 throw new UnsupportedOperationException("Cannot delete that URL"); 2118 default: 2119 throw new IllegalArgumentException("Unknown URL " + uri); 2120 } 2121 } 2122 2123 private int deleteEventInternal(long id, boolean callerIsSyncAdapter) { 2124 int result = 0; 2125 2126 // Query this event to get the fields needed for deleting. 2127 Cursor cursor = mDb.query("Events", EVENTS_PROJECTION, 2128 "_id=?", new String[] {String.valueOf(id)}, 2129 null /* groupBy */, 2130 null /* having */, null /* sortOrder */); 2131 try { 2132 if (cursor.moveToNext()) { 2133 result = 1; 2134 String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX); 2135 if (!TextUtils.isEmpty(syncId)) { 2136 2137 // TODO: we may also want to delete exception 2138 // events for this event (in case this was a 2139 // recurring event). We can do that with the 2140 // following code: 2141 // mDb.delete("Events", "originalEvent=?", new String[] {syncId}); 2142 } 2143 2144 // If this was a recurring event or a recurrence 2145 // exception, then force a recalculation of the 2146 // instances. 2147 String rrule = cursor.getString(EVENTS_RRULE_INDEX); 2148 String rdate = cursor.getString(EVENTS_RDATE_INDEX); 2149 String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX); 2150 if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate) 2151 || !TextUtils.isEmpty(origEvent)) { 2152 mMetaData.clearInstanceRange(); 2153 } 2154 2155 if (callerIsSyncAdapter) { 2156 mDb.delete("Events", "_id=?", new String[] {String.valueOf(id)}); 2157 } else { 2158 ContentValues values = new ContentValues(); 2159 values.put(Events.DELETED, 1); 2160 values.put(Events._SYNC_DIRTY, 1); 2161 mDb.update("Events", values, "_id=?", new String[] {String.valueOf(id)}); 2162 } 2163 } 2164 } finally { 2165 cursor.close(); 2166 cursor = null; 2167 } 2168 2169 scheduleNextAlarm(false /* do not remove alarms */); 2170 triggerAppWidgetUpdate(-1); 2171 2172 String selectionArgs[] = new String[] {String.valueOf(id)}; 2173 mDb.delete("Instances", "event_id=?", selectionArgs); 2174 mDb.delete("EventsRawTimes", "event_id=?", selectionArgs); 2175 mDb.delete("Attendees", "event_id=?", selectionArgs); 2176 mDb.delete("Reminders", "event_id=?", selectionArgs); 2177 mDb.delete("CalendarAlerts", "event_id=?", selectionArgs); 2178 mDb.delete("ExtendedProperties", "event_id=?", selectionArgs); 2179 return result; 2180 } 2181 2182 /** 2183 * Delete rows from a table and mark corresponding events as dirty. 2184 * @param table The table to delete from 2185 * @param uri The URI specifying the rows 2186 * @param selection for the query 2187 * @param selectionArgs for the query 2188 */ 2189 private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) { 2190 // Note that the query will return data according to the access restrictions, 2191 // so we don't need to worry about deleting data we don't have permission to read. 2192 Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null); 2193 ContentValues values = new ContentValues(); 2194 values.put(Events._SYNC_DIRTY, "1"); 2195 int count = 0; 2196 try { 2197 while(c.moveToNext()) { 2198 long id = c.getLong(ID_INDEX); 2199 long event_id = c.getLong(EVENT_ID_INDEX); 2200 mDb.delete(table, "_id=?", new String[] {String.valueOf(id)}); 2201 mDb.update("Events", values, "_id=?", new String[] {String.valueOf(event_id)}); 2202 count++; 2203 } 2204 } finally { 2205 c.close(); 2206 } 2207 return count; 2208 } 2209 2210 /** 2211 * Update rows in a table and mark corresponding events as dirty. 2212 * @param table The table to delete from 2213 * @param values The values to update 2214 * @param uri The URI specifying the rows 2215 * @param selection for the query 2216 * @param selectionArgs for the query 2217 */ 2218 private int updateInTable(String table, ContentValues values, Uri uri, String selection, 2219 String[] selectionArgs) { 2220 // Note that the query will return data according to the access restrictions, 2221 // so we don't need to worry about deleting data we don't have permission to read. 2222 Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null); 2223 ContentValues dirtyValues = new ContentValues(); 2224 dirtyValues.put(Events._SYNC_DIRTY, "1"); 2225 int count = 0; 2226 try { 2227 while(c.moveToNext()) { 2228 long id = c.getLong(ID_INDEX); 2229 long event_id = c.getLong(EVENT_ID_INDEX); 2230 mDb.update(table, values, "_id=?", new String[] {String.valueOf(id)}); 2231 mDb.update("Events", dirtyValues, "_id=?", new String[] {String.valueOf(event_id)}); 2232 count++; 2233 } 2234 } finally { 2235 c.close(); 2236 } 2237 return count; 2238 } 2239 2240 private int deleteMatchingCalendars(String where) { 2241 // query to find all the calendars that match, for each 2242 // - delete calendar subscription 2243 // - delete calendar 2244 2245 int numDeleted = 0; 2246 Cursor c = mDb.query("Calendars", sCalendarsIdProjection, where, 2247 null /* selectionArgs */, null /* groupBy */, 2248 null /* having */, null /* sortOrder */); 2249 if (c == null) { 2250 return 0; 2251 } 2252 try { 2253 while (c.moveToNext()) { 2254 long id = c.getLong(CALENDARS_INDEX_ID); 2255 modifyCalendarSubscription(id, false /* not selected */); 2256 c.deleteRow(); 2257 numDeleted++; 2258 } 2259 } finally { 2260 c.close(); 2261 } 2262 return numDeleted; 2263 } 2264 2265 // TODO: call calculateLastDate()! 2266 @Override 2267 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2268 String[] selectionArgs) { 2269 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2270 Log.v(TAG, "updateInTransaction: " + uri); 2271 } 2272 2273 int count = 0; 2274 2275 final int match = sUriMatcher.match(uri); 2276 2277 final boolean callerIsSyncAdapter = 2278 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false); 2279 2280 // TODO: remove this restriction 2281 if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS && match != EVENTS) { 2282 throw new IllegalArgumentException( 2283 "WHERE based updates not supported"); 2284 } 2285 switch (match) { 2286 case SYNCSTATE: 2287 return mDbHelper.getSyncState().update(mDb, values, 2288 appendAccountToSelection(uri, selection), selectionArgs); 2289 2290 case SYNCSTATE_ID: { 2291 selection = appendAccountToSelection(uri, selection); 2292 String selectionWithId = 2293 (BaseColumns._ID + "=" + ContentUris.parseId(uri) + " ") 2294 + (selection == null ? "" : " AND (" + selection + ")"); 2295 return mDbHelper.getSyncState().update(mDb, values, 2296 selectionWithId, selectionArgs); 2297 } 2298 2299 case CALENDARS_ID: 2300 { 2301 if (selection != null) { 2302 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2303 } 2304 long id = ContentUris.parseId(uri); 2305 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS); 2306 if (syncEvents != null) { 2307 modifyCalendarSubscription(id, syncEvents == 1); 2308 } 2309 2310 int result = mDb.update("Calendars", values, "_id=?", 2311 new String[] {String.valueOf(id)}); 2312 2313 return result; 2314 } 2315 case EVENTS: 2316 case EVENTS_ID: 2317 { 2318 long id = 0; 2319 if (match == EVENTS_ID) { 2320 id = ContentUris.parseId(uri); 2321 } else if (callerIsSyncAdapter) { 2322 if (selection != null && selection.startsWith("_id=")) { 2323 // The ContentProviderOperation generates an _id=n string instead of 2324 // adding the id to the URL, so parse that out here. 2325 id = Long.parseLong(selection.substring(4)); 2326 } else { 2327 // Sync adapter Events operation affects just Events table, not associated 2328 // tables. 2329 return mDb.update("Events", values, selection, selectionArgs); 2330 } 2331 } else { 2332 throw new IllegalArgumentException("Unknown URL " + uri); 2333 } 2334 if (!callerIsSyncAdapter) { 2335 values.put(Events._SYNC_DIRTY, 1); 2336 } 2337 // Disallow updating the attendee status in the Events 2338 // table. In the future, we could support this but we 2339 // would have to query and update the attendees table 2340 // to keep the values consistent. 2341 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) { 2342 throw new IllegalArgumentException("Updating " 2343 + Events.SELF_ATTENDEE_STATUS 2344 + " in Events table is not allowed."); 2345 } 2346 2347 // TODO: should we allow this? 2348 if (values.containsKey(Events.HTML_URI) && !callerIsSyncAdapter) { 2349 throw new IllegalArgumentException("Updating " 2350 + Events.HTML_URI 2351 + " in Events table is not allowed."); 2352 } 2353 2354 ContentValues updatedValues = updateContentValuesFromEvent(values); 2355 if (updatedValues == null) { 2356 Log.w(TAG, "Could not update event."); 2357 return 0; 2358 } 2359 2360 int result = mDb.update("Events", updatedValues, "_id=?", 2361 new String[] {String.valueOf(id)}); 2362 if (result > 0) { 2363 updateEventRawTimesLocked(id, updatedValues); 2364 updateInstancesLocked(updatedValues, id, false /* not a new event */, mDb); 2365 2366 if (values.containsKey(Events.DTSTART)) { 2367 // The start time of the event changed, so run the 2368 // event alarm scheduler. 2369 if (Log.isLoggable(TAG, Log.DEBUG)) { 2370 Log.d(TAG, "updateInternal() changing event"); 2371 } 2372 scheduleNextAlarm(false /* do not remove alarms */); 2373 triggerAppWidgetUpdate(id); 2374 } 2375 } 2376 return result; 2377 } 2378 case ATTENDEES_ID: { 2379 if (selection != null) { 2380 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2381 } 2382 // Copy the attendee status value to the Events table. 2383 updateEventAttendeeStatus(mDb, values); 2384 2385 if (callerIsSyncAdapter) { 2386 long id = ContentUris.parseId(uri); 2387 return mDb.update("Attendees", values, "_id=?", 2388 new String[] {String.valueOf(id)}); 2389 } else { 2390 return updateInTable("Attendees", values, uri, null /* selection */, 2391 null /* selectionArgs */); 2392 } 2393 } 2394 case CALENDAR_ALERTS_ID: { 2395 if (selection != null) { 2396 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2397 } 2398 // Note: dirty bit is not set for Alerts because it is not synced. 2399 // It is generated from Reminders, which is synced. 2400 long id = ContentUris.parseId(uri); 2401 return mDb.update("CalendarAlerts", values, "_id=?", 2402 new String[] {String.valueOf(id)}); 2403 } 2404 case CALENDAR_ALERTS: { 2405 // Note: dirty bit is not set for Alerts because it is not synced. 2406 // It is generated from Reminders, which is synced. 2407 return mDb.update("CalendarAlerts", values, selection, selectionArgs); 2408 } 2409 case REMINDERS_ID: { 2410 if (selection != null) { 2411 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2412 } 2413 if (callerIsSyncAdapter) { 2414 long id = ContentUris.parseId(uri); 2415 count = mDb.update("Reminders", values, "_id=?", 2416 new String[] {String.valueOf(id)}); 2417 } else { 2418 count = updateInTable("Reminders", values, uri, null /* selection */, 2419 null /* selectionArgs */); 2420 } 2421 2422 // Reschedule the event alarms because the 2423 // "minutes" field may have changed. 2424 if (Log.isLoggable(TAG, Log.DEBUG)) { 2425 Log.d(TAG, "updateInternal() changing reminder"); 2426 } 2427 scheduleNextAlarm(false /* do not remove alarms */); 2428 return count; 2429 } 2430 case EXTENDED_PROPERTIES_ID: { 2431 if (selection != null) { 2432 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2433 } 2434 if (callerIsSyncAdapter) { 2435 long id = ContentUris.parseId(uri); 2436 return mDb.update("ExtendedProperties", values, "_id=?", 2437 new String[] {String.valueOf(id)}); 2438 } else { 2439 return updateInTable("ExtendedProperties", values, uri, null /* selection */, 2440 null /* selectionArgs */); 2441 } 2442 } 2443 // TODO: replace the SCHEDULE_ALARM private URIs with a 2444 // service 2445 case SCHEDULE_ALARM: { 2446 scheduleNextAlarm(false); 2447 return 0; 2448 } 2449 case SCHEDULE_ALARM_REMOVE: { 2450 scheduleNextAlarm(true); 2451 return 0; 2452 } 2453 2454 default: 2455 throw new IllegalArgumentException("Unknown URL " + uri); 2456 } 2457 } 2458 2459 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 2460 final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME); 2461 final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE); 2462 if (!TextUtils.isEmpty(accountName)) { 2463 qb.appendWhere(Calendar.Calendars._SYNC_ACCOUNT + "=" 2464 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 2465 + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=" 2466 + DatabaseUtils.sqlEscapeString(accountType)); 2467 } else { 2468 qb.appendWhere("1"); // I.e. always true 2469 } 2470 } 2471 2472 private String appendAccountToSelection(Uri uri, String selection) { 2473 final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME); 2474 final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE); 2475 if (!TextUtils.isEmpty(accountName)) { 2476 StringBuilder selectionSb = new StringBuilder(Calendar.Calendars._SYNC_ACCOUNT + "=" 2477 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 2478 + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=" 2479 + DatabaseUtils.sqlEscapeString(accountType)); 2480 if (!TextUtils.isEmpty(selection)) { 2481 selectionSb.append(" AND ("); 2482 selectionSb.append(selection); 2483 selectionSb.append(')'); 2484 } 2485 return selectionSb.toString(); 2486 } else { 2487 return selection; 2488 } 2489 } 2490 2491 private void modifyCalendarSubscription(long id, boolean syncEvents) { 2492 // get the account, url, and current selected state 2493 // for this calendar. 2494 Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id), 2495 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE, 2496 Calendars.URL, Calendars.SYNC_EVENTS}, 2497 null /* selection */, 2498 null /* selectionArgs */, 2499 null /* sort */); 2500 2501 Account account = null; 2502 String calendarUrl = null; 2503 boolean oldSyncEvents = false; 2504 if (cursor != null && cursor.moveToFirst()) { 2505 try { 2506 final String accountName = cursor.getString(0); 2507 final String accountType = cursor.getString(1); 2508 account = new Account(accountName, accountType); 2509 calendarUrl = cursor.getString(2); 2510 oldSyncEvents = (cursor.getInt(3) != 0); 2511 } finally { 2512 cursor.close(); 2513 } 2514 } 2515 2516 if (account == null || TextUtils.isEmpty(calendarUrl)) { 2517 // should not happen? 2518 Log.w(TAG, "Cannot update subscription because account " 2519 + "or calendar url empty -- should not happen."); 2520 return; 2521 } 2522 2523 if (oldSyncEvents == syncEvents) { 2524 // nothing to do 2525 return; 2526 } 2527 2528 // If we are no longer syncing a calendar then make sure that the 2529 // old calendar sync data is cleared. Then if we later add this 2530 // calendar back, we will sync all the events. 2531 if (!syncEvents) { 2532 // TODO: clear out the SyncState 2533// byte[] data = readSyncDataBytes(account); 2534// GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data); 2535// if (syncData != null) { 2536// syncData.feedData.remove(calendarUrl); 2537// data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData); 2538// writeSyncDataBytes(account, data); 2539// } 2540 2541 // Delete all of the events in this calendar to save space. 2542 // This is the closest we can come to deleting a calendar. 2543 // Clients should never actually delete a calendar. That won't 2544 // work. We need to keep the calendar entry in the Calendars table 2545 // in order to know not to sync the events for that calendar from 2546 // the server. 2547 String[] args = new String[] {String.valueOf(id)}; 2548 mDb.delete("Events", CALENDAR_ID_SELECTION, args); 2549 2550 // TODO: cancel any pending/ongoing syncs for this calendar. 2551 2552 // TODO: there is a corner case to deal with here: namely, if 2553 // we edit or delete an event on the phone and then remove 2554 // (that is, stop syncing) a calendar, and if we also make a 2555 // change on the server to that event at about the same time, 2556 // then we will never propagate the changes from the phone to 2557 // the server. 2558 } 2559 2560 // If the calendar is not selected for syncing, then don't download 2561 // events. 2562 mDbHelper.scheduleSync(account, !syncEvents, calendarUrl); 2563 } 2564 2565 // TODO: is this needed 2566// @Override 2567// public void onSyncStop(SyncContext context, boolean success) { 2568// super.onSyncStop(context, success); 2569// if (Log.isLoggable(TAG, Log.DEBUG)) { 2570// Log.d(TAG, "onSyncStop() success: " + success); 2571// } 2572// scheduleNextAlarm(false /* do not remove alarms */); 2573// triggerAppWidgetUpdate(-1); 2574// } 2575 2576 /** 2577 * Update any existing widgets with the changed events. 2578 * 2579 * @param changedEventId Specific event known to be changed, otherwise -1. 2580 * If present, we use it to decide if an update is necessary. 2581 */ 2582 private synchronized void triggerAppWidgetUpdate(long changedEventId) { 2583 Context context = getContext(); 2584 if (context != null) { 2585 mAppWidgetProvider.providerUpdated(context, changedEventId); 2586 } 2587 } 2588 2589 /* Retrieve and cache the alarm manager */ 2590 private AlarmManager getAlarmManager() { 2591 synchronized(mAlarmLock) { 2592 if (mAlarmManager == null) { 2593 Context context = getContext(); 2594 if (context == null) { 2595 Log.e(TAG, "getAlarmManager() cannot get Context"); 2596 return null; 2597 } 2598 Object service = context.getSystemService(Context.ALARM_SERVICE); 2599 mAlarmManager = (AlarmManager) service; 2600 } 2601 return mAlarmManager; 2602 } 2603 } 2604 2605 void scheduleNextAlarmCheck(long triggerTime) { 2606 AlarmManager manager = getAlarmManager(); 2607 if (manager == null) { 2608 Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager"); 2609 return; 2610 } 2611 Context context = getContext(); 2612 Intent intent = new Intent(CalendarReceiver.SCHEDULE); 2613 intent.setClass(context, CalendarReceiver.class); 2614 PendingIntent pending = PendingIntent.getBroadcast(context, 2615 0, intent, PendingIntent.FLAG_NO_CREATE); 2616 if (pending != null) { 2617 // Cancel any previous alarms that do the same thing. 2618 manager.cancel(pending); 2619 } 2620 pending = PendingIntent.getBroadcast(context, 2621 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 2622 2623 if (Log.isLoggable(TAG, Log.DEBUG)) { 2624 Time time = new Time(); 2625 time.set(triggerTime); 2626 String timeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 2627 Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr); 2628 } 2629 2630 manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending); 2631 } 2632 2633 /* 2634 * This method runs the alarm scheduler in a background thread. 2635 */ 2636 void scheduleNextAlarm(boolean removeAlarms) { 2637 Thread thread = new AlarmScheduler(removeAlarms); 2638 thread.start(); 2639 } 2640 2641 /** 2642 * This method runs in a background thread and schedules an alarm for 2643 * the next calendar event, if necessary. 2644 */ 2645 private void runScheduleNextAlarm(boolean removeAlarms) { 2646 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 2647 db.beginTransaction(); 2648 try { 2649 if (removeAlarms) { 2650 removeScheduledAlarmsLocked(db); 2651 } 2652 scheduleNextAlarmLocked(db); 2653 db.setTransactionSuccessful(); 2654 } finally { 2655 db.endTransaction(); 2656 } 2657 } 2658 2659 /** 2660 * This method looks at the 24-hour window from now for any events that it 2661 * needs to schedule. This method runs within a database transaction. It 2662 * also runs in a background thread. 2663 * 2664 * The CalendarProvider2 keeps track of which alarms it has already scheduled 2665 * to avoid scheduling them more than once and for debugging problems with 2666 * alarms. It stores this knowledge in a database table called CalendarAlerts 2667 * which persists across reboots. But the actual alarm list is in memory 2668 * and disappears if the phone loses power. To avoid missing an alarm, we 2669 * clear the entries in the CalendarAlerts table when we start up the 2670 * CalendarProvider2. 2671 * 2672 * Scheduling an alarm multiple times is not tragic -- we filter out the 2673 * extra ones when we receive them. But we still need to keep track of the 2674 * scheduled alarms. The main reason is that we need to prevent multiple 2675 * notifications for the same alarm (on the receive side) in case we 2676 * accidentally schedule the same alarm multiple times. We don't have 2677 * visibility into the system's alarm list so we can never know for sure if 2678 * we have already scheduled an alarm and it's better to err on scheduling 2679 * an alarm twice rather than missing an alarm. Another reason we keep 2680 * track of scheduled alarms in a database table is that it makes it easy to 2681 * run an SQL query to find the next reminder that we haven't scheduled. 2682 * 2683 * @param db the database 2684 */ 2685 private void scheduleNextAlarmLocked(SQLiteDatabase db) { 2686 AlarmManager alarmManager = getAlarmManager(); 2687 if (alarmManager == null) { 2688 Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!"); 2689 return; 2690 } 2691 2692 final long currentMillis = System.currentTimeMillis(); 2693 final long start = currentMillis - SCHEDULE_ALARM_SLACK; 2694 final long end = start + (24 * 60 * 60 * 1000); 2695 ContentResolver cr = getContext().getContentResolver(); 2696 if (Log.isLoggable(TAG, Log.DEBUG)) { 2697 Time time = new Time(); 2698 time.set(start); 2699 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 2700 Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr); 2701 } 2702 2703 // Delete rows in CalendarAlert where the corresponding Instance or 2704 // Reminder no longer exist. 2705 // Also clear old alarms but keep alarms around for a while to prevent 2706 // multiple alerts for the same reminder. The "clearUpToTime' 2707 // should be further in the past than the point in time where 2708 // we start searching for events (the "start" variable defined above). 2709 String selectArg[] = new String[] { 2710 Long.toString(currentMillis - CLEAR_OLD_ALARM_THRESHOLD) 2711 }; 2712 2713 int rowsDeleted = 2714 db.delete(CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg); 2715 2716 long nextAlarmTime = end; 2717 final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis); 2718 if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) { 2719 nextAlarmTime = tmpAlarmTime; 2720 } 2721 2722 // Extract events from the database sorted by alarm time. The 2723 // alarm times are computed from Instances.begin (whose units 2724 // are milliseconds) and Reminders.minutes (whose units are 2725 // minutes). 2726 // 2727 // Also, ignore events whose end time is already in the past. 2728 // Also, ignore events alarms that we have already scheduled. 2729 // 2730 // Note 1: we can add support for the case where Reminders.minutes 2731 // equals -1 to mean use Calendars.minutes by adding a UNION for 2732 // that case where the two halves restrict the WHERE clause on 2733 // Reminders.minutes != -1 and Reminders.minutes = 1, respectively. 2734 // 2735 // Note 2: we have to name "myAlarmTime" different from the 2736 // "alarmTime" column in CalendarAlerts because otherwise the 2737 // query won't find multiple alarms for the same event. 2738 String query = "SELECT begin-(minutes*60000) AS myAlarmTime," 2739 + " Instances.event_id AS eventId, begin, end," 2740 + " title, allDay, method, minutes" 2741 + " FROM Instances INNER JOIN Events" 2742 + " ON (Events._id = Instances.event_id)" 2743 + " INNER JOIN Reminders" 2744 + " ON (Instances.event_id = Reminders.event_id)" 2745 + " WHERE method=" + Reminders.METHOD_ALERT 2746 + " AND myAlarmTime>=" + start 2747 + " AND myAlarmTime<=" + nextAlarmTime 2748 + " AND end>=" + currentMillis 2749 + " AND 0=(SELECT count(*) from CalendarAlerts CA" 2750 + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin" 2751 + " AND CA.alarmTime=myAlarmTime)" 2752 + " ORDER BY myAlarmTime,begin,title"; 2753 2754 acquireInstanceRangeLocked(start, end, false /* don't use minimum expansion windows */); 2755 Cursor cursor = null; 2756 try { 2757 cursor = db.rawQuery(query, null); 2758 2759 final int beginIndex = cursor.getColumnIndex(Instances.BEGIN); 2760 final int endIndex = cursor.getColumnIndex(Instances.END); 2761 final int eventIdIndex = cursor.getColumnIndex("eventId"); 2762 final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime"); 2763 final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES); 2764 2765 if (Log.isLoggable(TAG, Log.DEBUG)) { 2766 Time time = new Time(); 2767 time.set(nextAlarmTime); 2768 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 2769 Log.d(TAG, "cursor results: " + cursor.getCount() + " nextAlarmTime: " 2770 + alarmTimeStr); 2771 } 2772 2773 while (cursor.moveToNext()) { 2774 // Schedule all alarms whose alarm time is as early as any 2775 // scheduled alarm. For example, if the earliest alarm is at 2776 // 1pm, then we will schedule all alarms that occur at 1pm 2777 // but no alarms that occur later than 1pm. 2778 // Actually, we allow alarms up to a minute later to also 2779 // be scheduled so that we don't have to check immediately 2780 // again after an event alarm goes off. 2781 final long alarmTime = cursor.getLong(alarmTimeIndex); 2782 final long eventId = cursor.getLong(eventIdIndex); 2783 final int minutes = cursor.getInt(minutesIndex); 2784 final long startTime = cursor.getLong(beginIndex); 2785 final long endTime = cursor.getLong(endIndex); 2786 2787 if (Log.isLoggable(TAG, Log.DEBUG)) { 2788 Time time = new Time(); 2789 time.set(alarmTime); 2790 String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); 2791 time.set(startTime); 2792 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 2793 2794 Log.d(TAG, " looking at id: " + eventId + " " + startTime + startTimeStr 2795 + " alarm: " + alarmTime + schedTime); 2796 } 2797 2798 if (alarmTime < nextAlarmTime) { 2799 nextAlarmTime = alarmTime; 2800 } else if (alarmTime > 2801 nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) { 2802 // This event alarm (and all later ones) will be scheduled 2803 // later. 2804 if (Log.isLoggable(TAG, Log.DEBUG)) { 2805 Log.d(TAG, "This event alarm (and all later ones) will be scheduled later"); 2806 } 2807 break; 2808 } 2809 2810 // Avoid an SQLiteContraintException by checking if this alarm 2811 // already exists in the table. 2812 if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) { 2813 if (Log.isLoggable(TAG, Log.DEBUG)) { 2814 int titleIndex = cursor.getColumnIndex(Events.TITLE); 2815 String title = cursor.getString(titleIndex); 2816 Log.d(TAG, " alarm exists for id: " + eventId + " " + title); 2817 } 2818 continue; 2819 } 2820 2821 // Insert this alarm into the CalendarAlerts table 2822 Uri uri = CalendarAlerts.insert(cr, eventId, startTime, 2823 endTime, alarmTime, minutes); 2824 if (uri == null) { 2825 Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed"); 2826 continue; 2827 } 2828 2829 CalendarAlerts.scheduleAlarm(getContext(), alarmManager, alarmTime); 2830 } 2831 } finally { 2832 if (cursor != null) { 2833 cursor.close(); 2834 } 2835 } 2836 2837 // Refresh notification bar 2838 if (rowsDeleted > 0) { 2839 CalendarAlerts.scheduleAlarm(getContext(), alarmManager, currentMillis); 2840 } 2841 2842 // If we scheduled an event alarm, then schedule the next alarm check 2843 // for one minute past that alarm. Otherwise, if there were no 2844 // event alarms scheduled, then check again in 24 hours. If a new 2845 // event is inserted before the next alarm check, then this method 2846 // will be run again when the new event is inserted. 2847 if (nextAlarmTime != Long.MAX_VALUE) { 2848 scheduleNextAlarmCheck(nextAlarmTime + DateUtils.MINUTE_IN_MILLIS); 2849 } else { 2850 scheduleNextAlarmCheck(currentMillis + DateUtils.DAY_IN_MILLIS); 2851 } 2852 } 2853 2854 /** 2855 * Removes the entries in the CalendarAlerts table for alarms that we have 2856 * scheduled but that have not fired yet. We do this to ensure that we 2857 * don't miss an alarm. The CalendarAlerts table keeps track of the 2858 * alarms that we have scheduled but the actual alarm list is in memory 2859 * and will be cleared if the phone reboots. 2860 * 2861 * We don't need to remove entries that have already fired, and in fact 2862 * we should not remove them because we need to display the notifications 2863 * until the user dismisses them. 2864 * 2865 * We could remove entries that have fired and been dismissed, but we leave 2866 * them around for a while because it makes it easier to debug problems. 2867 * Entries that are old enough will be cleaned up later when we schedule 2868 * new alarms. 2869 */ 2870 private void removeScheduledAlarmsLocked(SQLiteDatabase db) { 2871 if (Log.isLoggable(TAG, Log.DEBUG)) { 2872 Log.d(TAG, "removing scheduled alarms"); 2873 } 2874 db.delete(CalendarAlerts.TABLE_NAME, 2875 CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */); 2876 } 2877 2878 private static String sEventsTable = "Events"; 2879 private static String sAttendeesTable = "Attendees"; 2880 private static String sRemindersTable = "Reminders"; 2881 private static String sCalendarAlertsTable = "CalendarAlerts"; 2882 private static String sExtendedPropertiesTable = "ExtendedProperties"; 2883 2884 private static final int EVENTS = 1; 2885 private static final int EVENTS_ID = 2; 2886 private static final int INSTANCES = 3; 2887 private static final int DELETED_EVENTS = 4; 2888 private static final int CALENDARS = 5; 2889 private static final int CALENDARS_ID = 6; 2890 private static final int ATTENDEES = 7; 2891 private static final int ATTENDEES_ID = 8; 2892 private static final int REMINDERS = 9; 2893 private static final int REMINDERS_ID = 10; 2894 private static final int EXTENDED_PROPERTIES = 11; 2895 private static final int EXTENDED_PROPERTIES_ID = 12; 2896 private static final int CALENDAR_ALERTS = 13; 2897 private static final int CALENDAR_ALERTS_ID = 14; 2898 private static final int CALENDAR_ALERTS_BY_INSTANCE = 15; 2899 private static final int INSTANCES_BY_DAY = 16; 2900 private static final int SYNCSTATE = 17; 2901 private static final int SYNCSTATE_ID = 18; 2902 private static final int EVENT_ENTITIES = 19; 2903 private static final int EVENT_ENTITIES_ID = 20; 2904 private static final int EVENT_DAYS = 21; 2905 private static final int SCHEDULE_ALARM = 22; 2906 private static final int SCHEDULE_ALARM_REMOVE = 23; 2907 2908 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 2909 private static final HashMap<String, String> sInstancesProjectionMap; 2910 private static final HashMap<String, String> sEventsProjectionMap; 2911 private static final HashMap<String, String> sEventEntitiesProjectionMap; 2912 private static final HashMap<String, String> sAttendeesProjectionMap; 2913 private static final HashMap<String, String> sRemindersProjectionMap; 2914 private static final HashMap<String, String> sCalendarAlertsProjectionMap; 2915 2916 static { 2917 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES); 2918 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY); 2919 sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS); 2920 sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS); 2921 sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID); 2922 sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES); 2923 sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID); 2924 sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS); 2925 sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID); 2926 sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS); 2927 sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES); 2928 sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID); 2929 sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS); 2930 sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID); 2931 sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES); 2932 sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID); 2933 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS); 2934 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID); 2935 sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance", 2936 CALENDAR_ALERTS_BY_INSTANCE); 2937 sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE); 2938 sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID); 2939 sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM); 2940 sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE); 2941 2942 sEventsProjectionMap = new HashMap<String, String>(); 2943 // Events columns 2944 sEventsProjectionMap.put(Events.HTML_URI, "htmlUri"); 2945 sEventsProjectionMap.put(Events.TITLE, "title"); 2946 sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); 2947 sEventsProjectionMap.put(Events.DESCRIPTION, "description"); 2948 sEventsProjectionMap.put(Events.STATUS, "eventStatus"); 2949 sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); 2950 sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); 2951 sEventsProjectionMap.put(Events.DTSTART, "dtstart"); 2952 sEventsProjectionMap.put(Events.DTEND, "dtend"); 2953 sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); 2954 sEventsProjectionMap.put(Events.DURATION, "duration"); 2955 sEventsProjectionMap.put(Events.ALL_DAY, "allDay"); 2956 sEventsProjectionMap.put(Events.VISIBILITY, "visibility"); 2957 sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency"); 2958 sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); 2959 sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); 2960 sEventsProjectionMap.put(Events.RRULE, "rrule"); 2961 sEventsProjectionMap.put(Events.RDATE, "rdate"); 2962 sEventsProjectionMap.put(Events.EXRULE, "exrule"); 2963 sEventsProjectionMap.put(Events.EXDATE, "exdate"); 2964 sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); 2965 sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); 2966 sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); 2967 sEventsProjectionMap.put(Events.LAST_DATE, "lastDate"); 2968 sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); 2969 sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); 2970 sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); 2971 sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); 2972 sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); 2973 sEventsProjectionMap.put(Events.ORGANIZER, "organizer"); 2974 sEventsProjectionMap.put(Events.DELETED, "deleted"); 2975 2976 // Put the shared items into the Attendees, Reminders projection map 2977 sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 2978 sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 2979 2980 // Calendar columns 2981 sEventsProjectionMap.put(Calendars.COLOR, "color"); 2982 sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level"); 2983 sEventsProjectionMap.put(Calendars.SELECTED, "selected"); 2984 sEventsProjectionMap.put(Calendars.URL, "url"); 2985 sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone"); 2986 sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount"); 2987 2988 // Put the shared items into the Instances projection map 2989 // The Instances and CalendarAlerts are joined with Calendars, so the projections include 2990 // the above Calendar columns. 2991 sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 2992 sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 2993 2994 sEventsProjectionMap.put(Events._ID, "_id"); 2995 sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id"); 2996 sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version"); 2997 sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time"); 2998 sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "_sync_local_id"); 2999 sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty"); 3000 sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account"); 3001 sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, 3002 "_sync_account_type"); 3003 3004 sEventEntitiesProjectionMap = new HashMap<String, String>(); 3005 sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri"); 3006 sEventEntitiesProjectionMap.put(Events.TITLE, "title"); 3007 sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description"); 3008 sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); 3009 sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus"); 3010 sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); 3011 sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); 3012 sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart"); 3013 sEventEntitiesProjectionMap.put(Events.DTEND, "dtend"); 3014 sEventEntitiesProjectionMap.put(Events.DURATION, "duration"); 3015 sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); 3016 sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay"); 3017 sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility"); 3018 sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency"); 3019 sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); 3020 sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); 3021 sEventEntitiesProjectionMap.put(Events.RRULE, "rrule"); 3022 sEventEntitiesProjectionMap.put(Events.RDATE, "rdate"); 3023 sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule"); 3024 sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate"); 3025 sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); 3026 sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); 3027 sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); 3028 sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate"); 3029 sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); 3030 sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); 3031 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); 3032 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); 3033 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); 3034 sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer"); 3035 sEventEntitiesProjectionMap.put(Events.DELETED, "deleted"); 3036 sEventEntitiesProjectionMap.put(Events._ID, Events._ID); 3037 sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID); 3038 sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION); 3039 sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY); 3040 sEventEntitiesProjectionMap.put(Calendars.URL, "url"); 3041 3042 // Instances columns 3043 sInstancesProjectionMap.put(Instances.BEGIN, "begin"); 3044 sInstancesProjectionMap.put(Instances.END, "end"); 3045 sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id"); 3046 sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id"); 3047 sInstancesProjectionMap.put(Instances.START_DAY, "startDay"); 3048 sInstancesProjectionMap.put(Instances.END_DAY, "endDay"); 3049 sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute"); 3050 sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute"); 3051 3052 // Attendees columns 3053 sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id"); 3054 sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id"); 3055 sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName"); 3056 sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail"); 3057 sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus"); 3058 sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship"); 3059 sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType"); 3060 3061 // Reminders columns 3062 sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id"); 3063 sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id"); 3064 sRemindersProjectionMap.put(Reminders.MINUTES, "minutes"); 3065 sRemindersProjectionMap.put(Reminders.METHOD, "method"); 3066 3067 // CalendarAlerts columns 3068 sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id"); 3069 sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id"); 3070 sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin"); 3071 sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end"); 3072 sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime"); 3073 sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state"); 3074 sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes"); 3075 } 3076 3077 /** 3078 * Make sure that there are no entries for accounts that no longer 3079 * exist. We are overriding this since we need to delete from the 3080 * Calendars table, which is not syncable, which has triggers that 3081 * will delete from the Events and tables, which are 3082 * syncable. TODO: update comment, make sure deletes don't get synced. 3083 */ 3084 public void onAccountsUpdated(Account[] accounts) { 3085 mDb = mDbHelper.getWritableDatabase(); 3086 if (mDb == null) return; 3087 3088 HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>(); 3089 HashSet<Account> validAccounts = new HashSet<Account>(); 3090 for (Account account : accounts) { 3091 validAccounts.add(new Account(account.name, account.type)); 3092 accountHasCalendar.put(account, false); 3093 } 3094 ArrayList<Account> accountsToDelete = new ArrayList<Account>(); 3095 3096 mDb.beginTransaction(); 3097 try { 3098 3099 for (String table : new String[]{"Calendars"}) { 3100 // Find all the accounts the contacts DB knows about, mark the ones that aren't 3101 // in the valid set for deletion. 3102 Cursor c = mDb.rawQuery("SELECT DISTINCT " + CalendarDatabaseHelper.ACCOUNT_NAME 3103 + "," 3104 + CalendarDatabaseHelper.ACCOUNT_TYPE + " from " 3105 + table, null); 3106 while (c.moveToNext()) { 3107 if (c.getString(0) != null && c.getString(1) != null) { 3108 Account currAccount = new Account(c.getString(0), c.getString(1)); 3109 if (!validAccounts.contains(currAccount)) { 3110 accountsToDelete.add(currAccount); 3111 } 3112 } 3113 } 3114 c.close(); 3115 } 3116 3117 for (Account account : accountsToDelete) { 3118 Log.d(TAG, "removing data for removed account " + account); 3119 String[] params = new String[]{account.name, account.type}; 3120 mDb.execSQL("DELETE FROM Calendars" 3121 + " WHERE " + CalendarDatabaseHelper.ACCOUNT_NAME + "= ? AND " 3122 + CalendarDatabaseHelper.ACCOUNT_TYPE 3123 + "= ?", params); 3124 } 3125 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 3126 mDb.setTransactionSuccessful(); 3127 } finally { 3128 mDb.endTransaction(); 3129 } 3130 } 3131 3132 /* package */ static boolean readBooleanQueryParameter(Uri uri, String name, 3133 boolean defaultValue) { 3134 final String flag = getQueryParameter(uri, name); 3135 return flag == null 3136 ? defaultValue 3137 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase())); 3138 } 3139 3140 // Duplicated from ContactsProvider2. TODO: a utility class for shared code 3141 /** 3142 * A fast re-implementation of {@link Uri#getQueryParameter} 3143 */ 3144 /* package */ static String getQueryParameter(Uri uri, String parameter) { 3145 String query = uri.getEncodedQuery(); 3146 if (query == null) { 3147 return null; 3148 } 3149 3150 int queryLength = query.length(); 3151 int parameterLength = parameter.length(); 3152 3153 String value; 3154 int index = 0; 3155 while (true) { 3156 index = query.indexOf(parameter, index); 3157 if (index == -1) { 3158 return null; 3159 } 3160 3161 index += parameterLength; 3162 3163 if (queryLength == index) { 3164 return null; 3165 } 3166 3167 if (query.charAt(index) == '=') { 3168 index++; 3169 break; 3170 } 3171 } 3172 3173 int ampIndex = query.indexOf('&', index); 3174 if (ampIndex == -1) { 3175 value = query.substring(index); 3176 } else { 3177 value = query.substring(index, ampIndex); 3178 } 3179 3180 return Uri.decode(value); 3181 } 3182 3183 /** 3184 * Inserts an argument at the beginning of the selection arg list. 3185 * 3186 * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is 3187 * prepended to the user's where clause (combined with 'AND') to generate 3188 * the final where close, so arguments associated with the QueryBuilder are 3189 * prepended before any user selection args to keep them in the right order. 3190 */ 3191 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 3192 if (selectionArgs == null) { 3193 return new String[] {arg}; 3194 } else { 3195 int newLength = selectionArgs.length + 1; 3196 String[] newSelectionArgs = new String[newLength]; 3197 newSelectionArgs[0] = arg; 3198 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 3199 return newSelectionArgs; 3200 } 3201 } 3202} 3203