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