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 18 package com.android.providers.calendar; 19 20 import android.accounts.Account; 21 import android.accounts.AccountManager; 22 import android.accounts.AccountManagerCallback; 23 import android.accounts.AccountManagerFuture; 24 import android.accounts.OperationCanceledException; 25 import android.accounts.AuthenticatorException; 26 import android.app.AlarmManager; 27 import android.app.PendingIntent; 28 import android.content.AbstractSyncableContentProvider; 29 import android.content.AbstractTableMerger; 30 import android.content.BroadcastReceiver; 31 import android.content.ContentProvider; 32 import android.content.ContentProviderOperation; 33 import android.content.ContentProviderResult; 34 import android.content.ContentResolver; 35 import android.content.ContentUris; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.Entity; 39 import android.content.EntityIterator; 40 import android.content.Intent; 41 import android.content.IntentFilter; 42 import android.content.OperationApplicationException; 43 import android.content.SyncContext; 44 import android.content.UriMatcher; 45 import android.database.Cursor; 46 import android.database.DatabaseUtils; 47 import android.database.SQLException; 48 import android.database.sqlite.SQLiteCursor; 49 import android.database.sqlite.SQLiteDatabase; 50 import android.database.sqlite.SQLiteQueryBuilder; 51 import android.net.Uri; 52 import android.os.Bundle; 53 import android.os.Debug; 54 import android.os.Process; 55 import android.os.RemoteException; 56 import android.pim.DateException; 57 import android.pim.RecurrenceSet; 58 import android.provider.Calendar; 59 import android.provider.Calendar.Attendees; 60 import android.provider.Calendar.BusyBits; 61 import android.provider.Calendar.CalendarAlerts; 62 import android.provider.Calendar.Calendars; 63 import android.provider.Calendar.Events; 64 import android.provider.Calendar.ExtendedProperties; 65 import android.provider.Calendar.Instances; 66 import android.provider.Calendar.Reminders; 67 import android.text.TextUtils; 68 import android.text.format.Time; 69 import android.util.Config; 70 import android.util.Log; 71 import android.util.TimeFormatException; 72 import com.google.android.collect.Maps; 73 import com.google.android.collect.Sets; 74 import com.google.android.gdata.client.AndroidGDataClient; 75 import com.google.android.gdata.client.AndroidXmlParserFactory; 76 import com.google.android.providers.AbstractGDataSyncAdapter; 77 import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData; 78 import com.google.android.googlelogin.GoogleLoginServiceConstants; 79 import com.google.wireless.gdata.calendar.client.CalendarClient; 80 import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory; 81 82 import java.util.ArrayList; 83 import java.util.Collections; 84 import java.util.HashMap; 85 import java.util.Map; 86 import java.util.Set; 87 import java.util.TimeZone; 88 import java.io.IOException; 89 90 public class CalendarProvider extends AbstractSyncableContentProvider { 91 private static final boolean PROFILE = false; 92 private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true; 93 private static final String[] ACCOUNTS_PROJECTION = 94 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE}; 95 96 private static final String[] EVENTS_PROJECTION = new String[] { 97 Events._SYNC_ID, 98 Events._SYNC_VERSION, 99 Events._SYNC_ACCOUNT, 100 Events._SYNC_ACCOUNT_TYPE, 101 Events.CALENDAR_ID, 102 Events.RRULE, 103 Events.RDATE, 104 Events.ORIGINAL_EVENT, 105 }; 106 private static final int EVENTS_SYNC_ID_INDEX = 0; 107 private static final int EVENTS_SYNC_VERSION_INDEX = 1; 108 private static final int EVENTS_SYNC_ACCOUNT_NAME_INDEX = 2; 109 private static final int EVENTS_SYNC_ACCOUNT_TYPE_INDEX = 3; 110 private static final int EVENTS_CALENDAR_ID_INDEX = 4; 111 private static final int EVENTS_RRULE_INDEX = 5; 112 private static final int EVENTS_RDATE_INDEX = 6; 113 private static final int EVENTS_ORIGINAL_EVENT_INDEX = 7; 114 115 private DatabaseUtils.InsertHelper mCalendarsInserter; 116 private DatabaseUtils.InsertHelper mEventsInserter; 117 private DatabaseUtils.InsertHelper mEventsRawTimesInserter; 118 private DatabaseUtils.InsertHelper mDeletedEventsInserter; 119 private DatabaseUtils.InsertHelper mInstancesInserter; 120 private DatabaseUtils.InsertHelper mAttendeesInserter; 121 private DatabaseUtils.InsertHelper mRemindersInserter; 122 private DatabaseUtils.InsertHelper mCalendarAlertsInserter; 123 private DatabaseUtils.InsertHelper mExtendedPropertiesInserter; 124 125 /** 126 * The cached copy of the CalendarMetaData database table. 127 * Make this "package private" instead of "private" so that test code 128 * can access it. 129 */ 130 MetaData mMetaData; 131 132 // The interval in minutes for calculating busy bits 133 private static final int BUSYBIT_INTERVAL = 60; 134 135 // A lookup table for getting a bit mask of length N, for N <= 32 136 // For example, BIT_MASKS[4] gives 0xf (which has 4 bits set to 1). 137 // We use this for computing the busy bits for events. 138 private static final int[] BIT_MASKS = { 139 0, 140 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 141 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 142 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 143 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff, 144 0x0001ffff, 0x0003ffff, 0x0007ffff, 0x000fffff, 145 0x001fffff, 0x003fffff, 0x007fffff, 0x00ffffff, 146 0x01ffffff, 0x03ffffff, 0x07ffffff, 0x0fffffff, 147 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff, 148 }; 149 150 // To determine if a recurrence exception originally overlapped the 151 // window, we need to assume a maximum duration, since we only know 152 // the original start time. 153 private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000; 154 155 public static final class TimeRange { 156 public long begin; 157 public long end; 158 public boolean allDay; 159 } 160 161 public static final class InstancesRange { 162 public long begin; 163 public long end; 164 InstancesRange(long begin, long end)165 public InstancesRange(long begin, long end) { 166 this.begin = begin; 167 this.end = end; 168 } 169 } 170 171 public static final class InstancesList 172 extends ArrayList<ContentValues> { 173 } 174 175 public static final class EventInstancesMap 176 extends HashMap<String, InstancesList> { add(String syncId, ContentValues values)177 public void add(String syncId, ContentValues values) { 178 InstancesList instances = get(syncId); 179 if (instances == null) { 180 instances = new InstancesList(); 181 put(syncId, instances); 182 } 183 instances.add(values); 184 } 185 } 186 187 // A thread that runs in the background and schedules the next 188 // calendar event alarm. 189 private class AlarmScheduler extends Thread { 190 boolean mRemoveAlarms; 191 AlarmScheduler(boolean removeAlarms)192 public AlarmScheduler(boolean removeAlarms) { 193 mRemoveAlarms = removeAlarms; 194 } 195 run()196 public void run() { 197 try { 198 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 199 runScheduleNextAlarm(mRemoveAlarms); 200 } catch (SQLException e) { 201 Log.e(TAG, "runScheduleNextAlarm() failed", e); 202 } 203 } 204 } 205 206 /** 207 * We search backward in time for event reminders that we may have missed 208 * and schedule them if the event has not yet expired. The amount in 209 * the past to search backwards is controlled by this constant. It 210 * should be at least a few minutes to allow for an event that was 211 * recently created on the web to make its way to the phone. Two hours 212 * might seem like overkill, but it is useful in the case where the user 213 * just crossed into a new timezone and might have just missed an alarm. 214 */ 215 private static final long SCHEDULE_ALARM_SLACK = 2 * android.text.format.DateUtils.HOUR_IN_MILLIS; 216 217 /** 218 * Alarms older than this threshold will be deleted from the CalendarAlerts 219 * table. This should be at least a day because if the timezone is 220 * wrong and the user corrects it we might delete good alarms that 221 * appear to be old because the device time was incorrectly in the future. 222 * This threshold must also be larger than SCHEDULE_ALARM_SLACK. We add 223 * the SCHEDULE_ALARM_SLACK to ensure this. 224 * 225 * To make it easier to find and debug problems with missed reminders, 226 * set this to something greater than a day. 227 */ 228 private static final long CLEAR_OLD_ALARM_THRESHOLD = 229 7 * android.text.format.DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK; 230 231 // A lock for synchronizing access to fields that are shared 232 // with the AlarmScheduler thread. 233 private Object mAlarmLock = new Object(); 234 235 private static final String TAG = "CalendarProvider"; 236 private static final String DATABASE_NAME = "calendar.db"; 237 238 // Note: if you update the version number, you must also update the code 239 // in upgradeDatabase() to modify the database (gracefully, if possible). 240 private static final int DATABASE_VERSION = 57; 241 242 // Make sure we load at least two months worth of data. 243 // Client apps can load more data in a background thread. 244 private static final long MINIMUM_EXPANSION_SPAN = 245 2L * 31 * 24 * 60 * 60 * 1000; 246 247 private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID }; 248 private static final int CALENDARS_INDEX_ID = 0; 249 250 // Allocate the string constant once here instead of on the heap 251 private static final String CALENDAR_ID_SELECTION = "calendar_id=?"; 252 253 private static final String[] sInstancesProjection = 254 new String[] { Instances.START_DAY, Instances.END_DAY, 255 Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY }; 256 257 private static final int INSTANCES_INDEX_START_DAY = 0; 258 private static final int INSTANCES_INDEX_END_DAY = 1; 259 private static final int INSTANCES_INDEX_START_MINUTE = 2; 260 private static final int INSTANCES_INDEX_END_MINUTE = 3; 261 private static final int INSTANCES_INDEX_ALL_DAY = 4; 262 263 private static final String[] sBusyBitProjection = new String[] { 264 BusyBits.DAY, BusyBits.BUSYBITS, BusyBits.ALL_DAY_COUNT }; 265 266 private static final int BUSYBIT_INDEX_DAY = 0; 267 private static final int BUSYBIT_INDEX_BUSYBITS= 1; 268 private static final int BUSYBIT_INDEX_ALL_DAY_COUNT = 2; 269 270 private CalendarClient mCalendarClient = null; 271 272 private AlarmManager mAlarmManager; 273 274 private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance(); 275 276 /** 277 * Listens for timezone changes and disk-no-longer-full events 278 */ 279 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 280 @Override 281 public void onReceive(Context context, Intent intent) { 282 String action = intent.getAction(); 283 if (Log.isLoggable(TAG, Log.DEBUG)) { 284 Log.d(TAG, "onReceive() " + action); 285 } 286 if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { 287 updateTimezoneDependentFields(); 288 scheduleNextAlarm(false /* do not remove alarms */); 289 } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { 290 // Try to clean up if things were screwy due to a full disk 291 updateTimezoneDependentFields(); 292 scheduleNextAlarm(false /* do not remove alarms */); 293 } else if (Intent.ACTION_TIME_CHANGED.equals(action)) { 294 scheduleNextAlarm(false /* do not remove alarms */); 295 } 296 } 297 }; 298 CalendarProvider()299 public CalendarProvider() { 300 super(DATABASE_NAME, DATABASE_VERSION, Calendars.CONTENT_URI); 301 } 302 303 @Override onCreate()304 public boolean onCreate() { 305 super.onCreate(); 306 307 setTempProviderSyncAdapter(new CalendarSyncAdapter(getContext(), this)); 308 309 // Register for Intent broadcasts 310 IntentFilter filter = new IntentFilter(); 311 312 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 313 filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); 314 filter.addAction(Intent.ACTION_TIME_CHANGED); 315 final Context c = getContext(); 316 317 // We don't ever unregister this because this thread always wants 318 // to receive notifications, even in the background. And if this 319 // thread is killed then the whole process will be killed and the 320 // memory resources will be reclaimed. 321 c.registerReceiver(mIntentReceiver, filter); 322 323 mMetaData = new MetaData(mOpenHelper); 324 updateTimezoneDependentFields(); 325 326 return true; 327 } 328 329 /** 330 * This creates a background thread to check the timezone and update 331 * the timezone dependent fields in the Instances table if the timezone 332 * has changes. 333 */ updateTimezoneDependentFields()334 private void updateTimezoneDependentFields() { 335 Thread thread = new TimezoneCheckerThread(); 336 thread.start(); 337 } 338 339 private class TimezoneCheckerThread extends Thread { 340 @Override run()341 public void run() { 342 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 343 try { 344 doUpdateTimezoneDependentFields(); 345 } catch (SQLException e) { 346 Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e); 347 try { 348 // Clear at least the in-memory data (and if possible the 349 // database fields) to force a re-computation of Instances. 350 mMetaData.clearInstanceRange(); 351 } catch (SQLException e2) { 352 Log.e(TAG, "clearInstanceRange() also failed: " + e2); 353 } 354 } 355 } 356 } 357 358 /** 359 * This method runs in a background thread. If the timezone has changed 360 * then the Instances table will be regenerated. 361 */ doUpdateTimezoneDependentFields()362 private void doUpdateTimezoneDependentFields() { 363 MetaData.Fields fields = mMetaData.getFields(); 364 String localTimezone = TimeZone.getDefault().getID(); 365 if (TextUtils.equals(fields.timezone, localTimezone)) { 366 // Even if the timezone hasn't changed, check for missed alarms. 367 // This code executes when the CalendarProvider is created and 368 // helps to catch missed alarms when the Calendar process is 369 // killed (because of low-memory conditions) and then restarted. 370 rescheduleMissedAlarms(); 371 return; 372 } 373 374 // The database timezone is different from the current timezone. 375 // Regenerate the Instances table for this month. Include events 376 // starting at the beginning of this month. 377 long now = System.currentTimeMillis(); 378 Time time = new Time(); 379 time.set(now); 380 time.monthDay = 1; 381 time.hour = 0; 382 time.minute = 0; 383 time.second = 0; 384 long begin = time.normalize(true); 385 long end = begin + MINIMUM_EXPANSION_SPAN; 386 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 387 handleInstanceQuery(qb, begin, end, new String[] { Instances._ID }, 388 null /* selection */, null /* sort */, false /* searchByDayInsteadOfMillis */); 389 390 // Also pre-compute the BusyBits table for this month. 391 int startDay = Time.getJulianDay(begin, time.gmtoff); 392 int endDay = startDay + 31; 393 qb = new SQLiteQueryBuilder(); 394 handleBusyBitsQuery(qb, startDay, endDay, sBusyBitProjection, 395 null /* selection */, null /* sort */); 396 rescheduleMissedAlarms(); 397 } 398 rescheduleMissedAlarms()399 private void rescheduleMissedAlarms() { 400 AlarmManager manager = getAlarmManager(); 401 if (manager != null) { 402 Context context = getContext(); 403 ContentResolver cr = context.getContentResolver(); 404 CalendarAlerts.rescheduleMissedAlarms(cr, context, manager); 405 } 406 } 407 408 @Override onDatabaseOpened(SQLiteDatabase db)409 protected void onDatabaseOpened(SQLiteDatabase db) { 410 db.markTableSyncable("Events", "DeletedEvents"); 411 412 if (!isTemporary()) { 413 mCalendarClient = new CalendarClient( 414 new AndroidGDataClient(getContext(), CalendarSyncAdapter.USER_AGENT_APP_VERSION), 415 new XmlCalendarGDataParserFactory( 416 new AndroidXmlParserFactory())); 417 } 418 419 mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars"); 420 mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events"); 421 mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes"); 422 mDeletedEventsInserter = new DatabaseUtils.InsertHelper(db, "DeletedEvents"); 423 mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances"); 424 mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees"); 425 mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders"); 426 mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts"); 427 mExtendedPropertiesInserter = 428 new DatabaseUtils.InsertHelper(db, "ExtendedProperties"); 429 } 430 431 @Override upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion)432 protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) { 433 Log.i(TAG, "Upgrading DB from version " + oldVersion 434 + " to " + newVersion); 435 if (oldVersion < 46) { 436 dropTables(db); 437 bootstrapDatabase(db); 438 return false; // this was lossy 439 } 440 441 if (oldVersion == 46) { 442 Log.w(TAG, "Upgrading CalendarAlerts table"); 443 db.execSQL("UPDATE CalendarAlerts SET reminder_id=NULL;"); 444 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN minutes INTEGER DEFAULT 0;"); 445 oldVersion += 1; 446 } 447 448 if (oldVersion == 47) { 449 // Changing to version 48 was intended to force a data wipe 450 dropTables(db); 451 bootstrapDatabase(db); 452 return false; // this was lossy 453 } 454 455 if (oldVersion == 48) { 456 // Changing to version 49 was intended to force a data wipe 457 dropTables(db); 458 bootstrapDatabase(db); 459 return false; // this was lossy 460 } 461 462 if (oldVersion == 49) { 463 Log.w(TAG, "Upgrading DeletedEvents table"); 464 465 // We don't have enough information to fill in the correct 466 // value of the calendar_id for old rows in the DeletedEvents 467 // table, but rows in that table are transient so it is unlikely 468 // that there are any rows. Plus, the calendar_id is used only 469 // when deleting a calendar, which is a rare event. All new rows 470 // will have the correct calendar_id. 471 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;"); 472 473 // Trigger to remove a calendar's events when we delete the calendar 474 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 475 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 476 "BEGIN " + 477 "DELETE FROM Events WHERE calendar_id = old._id;" + 478 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" + 479 "END"); 480 db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted"); 481 oldVersion += 1; 482 } 483 484 if (oldVersion == 50) { 485 // This should have been deleted in the upgrade from version 49 486 // but we missed it. 487 db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted"); 488 oldVersion += 1; 489 } 490 491 if (oldVersion == 51) { 492 // We added "originalAllDay" to the Events table to keep track of 493 // the allDay status of the original recurring event for entries 494 // that are exceptions to that recurring event. We need this so 495 // that we can format the date correctly for the "originalInstanceTime" 496 // column when we make a change to the recurrence exception and 497 // send it to the server. 498 db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;"); 499 500 // Iterate through the Events table and for each recurrence 501 // exception, fill in the correct value for "originalAllDay", 502 // if possible. The only times where this might not be possible 503 // are (1) the original recurring event no longer exists, or 504 // (2) the original recurring event does not yet have a _sync_id 505 // because it was created on the phone and hasn't been synced to the 506 // server yet. In both cases the originalAllDay field will be set 507 // to null. In the first case we don't care because the recurrence 508 // exception will not be displayed and we won't be able to make 509 // any changes to it (and even if we did, the server should ignore 510 // them, right?). In the second case, the calendar client already 511 // disallows making changes to an instance of a recurring event 512 // until the recurring event has been synced to the server so the 513 // second case should never occur. 514 515 // "cursor" iterates over all the recurrences exceptions. 516 Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events" 517 + " WHERE originalEvent IS NOT NULL", null /* selection args */); 518 if (cursor != null) { 519 try { 520 while (cursor.moveToNext()) { 521 long id = cursor.getLong(0); 522 String originalEvent = cursor.getString(1); 523 524 // Find the original recurring event (if it exists) 525 Cursor recur = db.rawQuery("SELECT allDay FROM Events" 526 + " WHERE _sync_id=?", new String[] {originalEvent}); 527 if (recur == null) { 528 continue; 529 } 530 531 try { 532 // Fill in the "originalAllDay" field of the 533 // recurrence exception with the "allDay" value 534 // from the recurring event. 535 if (recur.moveToNext()) { 536 int allDay = recur.getInt(0); 537 db.execSQL("UPDATE Events SET originalAllDay=" + allDay 538 + " WHERE _id="+id); 539 } 540 } finally { 541 recur.close(); 542 } 543 } 544 } finally { 545 cursor.close(); 546 } 547 } 548 oldVersion += 1; 549 } 550 551 if (oldVersion == 52) { 552 Log.w(TAG, "Upgrading CalendarAlerts table"); 553 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;"); 554 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;"); 555 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;"); 556 oldVersion += 1; 557 } 558 559 if (oldVersion == 53) { 560 Log.w(TAG, "adding eventSyncAccountAndIdIndex"); 561 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 562 + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");"); 563 oldVersion += 1; 564 } 565 566 if (oldVersion == 54) { 567 db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;"); 568 db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;"); 569 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;"); 570 db.execSQL("UPDATE Calendars" 571 + " SET _sync_account_type='com.google'" 572 + " WHERE _sync_account IS NOT NULL"); 573 db.execSQL("UPDATE Events" 574 + " SET _sync_account_type='com.google'" 575 + " WHERE _sync_account IS NOT NULL"); 576 db.execSQL("UPDATE DeletedEvents" 577 + " SET _sync_account_type='com.google'" 578 + " WHERE _sync_account IS NOT NULL"); 579 Log.w(TAG, "re-creating eventSyncAccountAndIdIndex"); 580 db.execSQL("DROP INDEX eventSyncAccountAndIdIndex"); 581 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 582 + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", " 583 + Events._SYNC_ID + ");"); 584 oldVersion += 1; 585 } 586 if (oldVersion == 55 || oldVersion == 56) { // Both require resync 587 // Delete sync state, so all records will be re-synced. 588 db.execSQL("DELETE FROM _sync_state;"); 589 590 // "cursor" iterates over all the calendars 591 Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url " 592 + "FROM Calendars", 593 null /* selection args */); 594 if (cursor != null) { 595 try { 596 while (cursor.moveToNext()) { 597 String accountName = cursor.getString(0); 598 String accountType = cursor.getString(1); 599 final Account account = new Account(accountName, accountType); 600 String calendarUrl = cursor.getString(2); 601 scheduleSync(account, false /* two-way sync */, calendarUrl); 602 } 603 } finally { 604 cursor.close(); 605 } 606 } 607 } 608 if (oldVersion == 55) { 609 db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;"); 610 db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;"); 611 // Clear _sync_dirty to avoid a client-to-server sync that could blow away 612 // server attendees. 613 // Clear _sync_version to pull down the server's event (with attendees) 614 // Change the URLs from full-selfattendance to full 615 db.execSQL("UPDATE Events" 616 + " SET _sync_dirty=0," 617 + " _sync_version=NULL," 618 + " _sync_id=" 619 + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full')," 620 + " commentsUri =" 621 + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');"); 622 db.execSQL("UPDATE Calendars" 623 + " SET url=" 624 + "REPLACE(url, '/private/full-selfattendance', '/private/full');"); 625 626 // "cursor" iterates over all the calendars 627 Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars", 628 null /* selection args */); 629 // Add the owner column. 630 if (cursor != null) { 631 try { 632 while (cursor.moveToNext()) { 633 Long id = cursor.getLong(0); 634 String url = cursor.getString(1); 635 String owner = CalendarSyncAdapter.calendarEmailAddressFromFeedUrl(url); 636 db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?", 637 new Object[] {owner, id}); 638 } 639 } finally { 640 cursor.close(); 641 } 642 } 643 oldVersion += 1; 644 } 645 if (oldVersion == 56) { 646 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify" 647 + " INTEGER NOT NULL DEFAULT 0;"); 648 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers" 649 + " INTEGER NOT NULL DEFAULT 1;"); 650 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests" 651 + " INTEGER NOT NULL DEFAULT 1;"); 652 db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;"); 653 db.execSQL("UPDATE Events SET organizer=" 654 + "(SELECT attendeeEmail FROM Attendees WHERE " 655 + "Attendees.event_id = Events._id AND Attendees.attendeeRelationship=2);"); 656 657 658 oldVersion += 1; 659 } 660 661 return true; // this was lossless 662 } 663 dropTables(SQLiteDatabase db)664 private void dropTables(SQLiteDatabase db) { 665 db.execSQL("DROP TABLE IF EXISTS Calendars;"); 666 db.execSQL("DROP TABLE IF EXISTS Events;"); 667 db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;"); 668 db.execSQL("DROP TABLE IF EXISTS DeletedEvents;"); 669 db.execSQL("DROP TABLE IF EXISTS Instances;"); 670 db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;"); 671 db.execSQL("DROP TABLE IF EXISTS BusyBits;"); 672 db.execSQL("DROP TABLE IF EXISTS Attendees;"); 673 db.execSQL("DROP TABLE IF EXISTS Reminders;"); 674 db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;"); 675 db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;"); 676 } 677 678 @Override bootstrapDatabase(SQLiteDatabase db)679 protected void bootstrapDatabase(SQLiteDatabase db) { 680 super.bootstrapDatabase(db); 681 db.execSQL("CREATE TABLE Calendars (" + 682 "_id INTEGER PRIMARY KEY," + 683 "_sync_account TEXT," + 684 "_sync_account_type TEXT," + 685 "_sync_id TEXT," + 686 "_sync_version TEXT," + 687 "_sync_time TEXT," + // UTC 688 "_sync_local_id INTEGER," + 689 "_sync_dirty INTEGER," + 690 "_sync_mark INTEGER," + // Used to filter out new rows 691 "url TEXT," + 692 "name TEXT," + 693 "displayName TEXT," + 694 "hidden INTEGER NOT NULL DEFAULT 0," + 695 "color INTEGER," + 696 "access_level INTEGER," + 697 "selected INTEGER NOT NULL DEFAULT 1," + 698 "sync_events INTEGER NOT NULL DEFAULT 0," + 699 "location TEXT," + 700 "timezone TEXT," + 701 "ownerAccount TEXT" + 702 ");"); 703 704 // Trigger to remove a calendar's events when we delete the calendar 705 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 706 "BEGIN " + 707 "DELETE FROM Events WHERE calendar_id = old._id;" + 708 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" + 709 "END"); 710 711 // TODO: do we need both dtend and duration? 712 db.execSQL("CREATE TABLE Events (" + 713 "_id INTEGER PRIMARY KEY," + 714 "_sync_account TEXT," + 715 "_sync_account_type TEXT," + 716 "_sync_id TEXT," + 717 "_sync_version TEXT," + 718 "_sync_time TEXT," + // UTC 719 "_sync_local_id INTEGER," + 720 "_sync_dirty INTEGER," + 721 "_sync_mark INTEGER," + // To filter out new rows 722 "calendar_id INTEGER NOT NULL," + 723 "htmlUri TEXT," + 724 "title TEXT," + 725 "eventLocation TEXT," + 726 "description TEXT," + 727 "eventStatus INTEGER," + 728 "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," + 729 "commentsUri TEXT," + 730 "dtstart INTEGER," + // millis since epoch 731 "dtend INTEGER," + // millis since epoch 732 "eventTimezone TEXT," + // timezone for event 733 "duration TEXT," + 734 "allDay INTEGER NOT NULL DEFAULT 0," + 735 "visibility INTEGER NOT NULL DEFAULT 0," + 736 "transparency INTEGER NOT NULL DEFAULT 0," + 737 "hasAlarm INTEGER NOT NULL DEFAULT 0," + 738 "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," + 739 "rrule TEXT," + 740 "rdate TEXT," + 741 "exrule TEXT," + 742 "exdate TEXT," + 743 "originalEvent TEXT," + // _sync_id of recurring event 744 "originalInstanceTime INTEGER," + // millis since epoch 745 "originalAllDay INTEGER," + 746 "lastDate INTEGER," + // millis since epoch 747 "hasAttendeeData INTEGER NOT NULL DEFAULT 0," + 748 "guestsCanModify INTEGER NOT NULL DEFAULT 0," + 749 "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," + 750 "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," + 751 "organizer STRING" + 752 ");"); 753 754 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 755 + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", " 756 + Events._SYNC_ID + ");"); 757 758 db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" + 759 Events.CALENDAR_ID + 760 ");"); 761 762 db.execSQL("CREATE TABLE EventsRawTimes (" + 763 "_id INTEGER PRIMARY KEY," + 764 "event_id INTEGER NOT NULL," + 765 "dtstart2445 TEXT," + 766 "dtend2445 TEXT," + 767 "originalInstanceTime2445 TEXT," + 768 "lastDate2445 TEXT," + 769 "UNIQUE (event_id)" + 770 ");"); 771 772 // NOTE: we do not create a trigger to delete an event's instances upon update, 773 // as all rows currently get updated during a merge. 774 775 db.execSQL("CREATE TABLE DeletedEvents (" + 776 "_sync_id TEXT," + 777 "_sync_version TEXT," + 778 "_sync_account TEXT," + 779 "_sync_account_type TEXT," + 780 (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing, 781 "_sync_mark INTEGER," + // To filter out new rows 782 "calendar_id INTEGER" + 783 ");"); 784 785 db.execSQL("CREATE TABLE Instances (" + 786 "_id INTEGER PRIMARY KEY," + 787 "event_id INTEGER," + 788 "begin INTEGER," + // UTC millis 789 "end INTEGER," + // UTC millis 790 "startDay INTEGER," + // Julian start day 791 "endDay INTEGER," + // Julian end day 792 "startMinute INTEGER," + // minutes from midnight 793 "endMinute INTEGER," + // minutes from midnight 794 "UNIQUE (event_id, begin, end)" + 795 ");"); 796 797 db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" + 798 Instances.START_DAY + 799 ");"); 800 801 db.execSQL("CREATE TABLE CalendarMetaData (" + 802 "_id INTEGER PRIMARY KEY," + 803 "localTimezone TEXT," + 804 "minInstance INTEGER," + // UTC millis 805 "maxInstance INTEGER," + // UTC millis 806 "minBusyBits INTEGER," + // UTC millis 807 "maxBusyBits INTEGER" + // UTC millis 808 ");"); 809 810 db.execSQL("CREATE TABLE BusyBits(" + 811 "day INTEGER PRIMARY KEY," + // the Julian day 812 "busyBits INTEGER," + // 24 bits for 60-minute intervals 813 "allDayCount INTEGER" + // number of all-day events 814 ");"); 815 816 db.execSQL("CREATE TABLE Attendees (" + 817 "_id INTEGER PRIMARY KEY," + 818 "event_id INTEGER," + 819 "attendeeName TEXT," + 820 "attendeeEmail TEXT," + 821 "attendeeStatus INTEGER," + 822 "attendeeRelationship INTEGER," + 823 "attendeeType INTEGER" + 824 ");"); 825 826 db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" + 827 Attendees.EVENT_ID + 828 ");"); 829 830 db.execSQL("CREATE TABLE Reminders (" + 831 "_id INTEGER PRIMARY KEY," + 832 "event_id INTEGER," + 833 "minutes INTEGER," + 834 "method INTEGER NOT NULL" + 835 " DEFAULT " + Reminders.METHOD_DEFAULT + 836 ");"); 837 838 db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" + 839 Reminders.EVENT_ID + 840 ");"); 841 842 // This table stores the Calendar notifications that have gone off. 843 db.execSQL("CREATE TABLE CalendarAlerts (" + 844 "_id INTEGER PRIMARY KEY," + 845 "event_id INTEGER," + 846 "begin INTEGER NOT NULL," + // UTC millis 847 "end INTEGER NOT NULL," + // UTC millis 848 "alarmTime INTEGER NOT NULL," + // UTC millis 849 "creationTime INTEGER NOT NULL," + // UTC millis 850 "receivedTime INTEGER NOT NULL," + // UTC millis 851 "notifyTime INTEGER NOT NULL," + // UTC millis 852 "state INTEGER NOT NULL," + 853 "minutes INTEGER," + 854 "UNIQUE (alarmTime, begin, event_id)" + 855 ");"); 856 857 db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" + 858 CalendarAlerts.EVENT_ID + 859 ");"); 860 861 db.execSQL("CREATE TABLE ExtendedProperties (" + 862 "_id INTEGER PRIMARY KEY," + 863 "event_id INTEGER," + 864 "name TEXT," + 865 "value TEXT" + 866 ");"); 867 868 db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" + 869 ExtendedProperties.EVENT_ID + 870 ");"); 871 872 // Trigger to remove data tied to an event when we delete that event. 873 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " + 874 "BEGIN " + 875 "DELETE FROM Instances WHERE event_id = old._id;" + 876 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" + 877 "DELETE FROM Attendees WHERE event_id = old._id;" + 878 "DELETE FROM Reminders WHERE event_id = old._id;" + 879 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" + 880 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" + 881 "END"); 882 883 // Triggers to set the _sync_dirty flag when an attendee is changed, 884 // inserted or deleted 885 db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " + 886 "BEGIN " + 887 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + 888 "END"); 889 db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " + 890 "BEGIN " + 891 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" + 892 "END"); 893 db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " + 894 "BEGIN " + 895 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + 896 "END"); 897 898 // Triggers to set the _sync_dirty flag when a reminder is changed, 899 // inserted or deleted 900 db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " + 901 "BEGIN " + 902 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + 903 "END"); 904 db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " + 905 "BEGIN " + 906 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" + 907 "END"); 908 db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " + 909 "BEGIN " + 910 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + 911 "END"); 912 // Triggers to set the _sync_dirty flag when an extended property is changed, 913 // inserted or deleted 914 db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " + 915 "BEGIN " + 916 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + 917 "END"); 918 db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " + 919 "BEGIN " + 920 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" + 921 "END"); 922 db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " + 923 "BEGIN " + 924 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + 925 "END"); 926 } 927 928 /** 929 * Make sure that there are no entries for accounts that no longer 930 * exist. We are overriding this since we need to delete from the 931 * Calendars table, which is not syncable, which has triggers that 932 * will delete from the Events and DeletedEvents tables, which are 933 * syncable. 934 */ 935 @Override onAccountsChanged(final Account[] accountsArray)936 protected void onAccountsChanged(final Account[] accountsArray) { 937 super.onAccountsChanged(accountsArray); 938 939 final Map<Account, Boolean> accounts = Maps.newHashMap(); 940 for (Account account : accountsArray) { 941 accounts.put(account, false); 942 } 943 944 mDb.beginTransaction(); 945 try { 946 deleteRowsForRemovedAccounts(accounts, "Calendars"); 947 mDb.setTransactionSuccessful(); 948 } finally { 949 mDb.endTransaction(); 950 } 951 952 if (mCalendarClient == null) { 953 return; 954 } 955 956 // If we have calendars for unknown accounts, delete them. 957 // If there are no calendars at all for a given account, add the 958 // default calendar. 959 960 // TODO: allow caller to specify which account's feeds should be updated 961 String[] features = new String[]{ 962 GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE}; 963 AccountManagerCallback<Account[]> callback = new AccountManagerCallback<Account[]>() { 964 public void run(AccountManagerFuture<Account[]> accountManagerFuture) { 965 Account[] currentAccounts = new Account[0]; 966 try { 967 currentAccounts = accountManagerFuture.getResult(); 968 } catch (OperationCanceledException e) { 969 Log.w(TAG, "onAccountsChanged", e); 970 return; 971 } catch (IOException e) { 972 Log.w(TAG, "onAccountsChanged", e); 973 return; 974 } catch (AuthenticatorException e) { 975 Log.w(TAG, "onAccountsChanged", e); 976 return; 977 } 978 if (currentAccounts.length < 1) { 979 Log.w(TAG, "getPrimaryAccount: no primary account configured."); 980 return; 981 } 982 Account primaryAccount = currentAccounts[0]; 983 984 for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) { 985 // TODO: change this when Calendar supports multiple accounts. Until then 986 // pretend that only the primary exists. 987 boolean ignore = primaryAccount == null || 988 !primaryAccount.equals(entry.getKey()); 989 entry.setValue(ignore); 990 } 991 992 Set<Account> handledAccounts = Sets.newHashSet(); 993 if (Config.LOGV) Log.v(TAG, "querying calendars"); 994 Cursor c = queryInternal(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, 995 null); 996 try { 997 while (c.moveToNext()) { 998 final String accountName = c.getString(0); 999 final String accountType = c.getString(1); 1000 final Account account = new Account(accountName, accountType); 1001 if (handledAccounts.contains(account)) { 1002 continue; 1003 } 1004 handledAccounts.add(account); 1005 if (accounts.containsKey(account)) { 1006 if (Config.LOGV) { 1007 Log.v(TAG, "calendars for account " + account + " exist"); 1008 } 1009 accounts.put(account, true /* hasCalendar */); 1010 } 1011 } 1012 } finally { 1013 c.close(); 1014 c = null; 1015 } 1016 1017 if (Config.LOGV) { 1018 Log.v(TAG, "scanning over " + accounts.size() + " account(s)"); 1019 } 1020 for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) { 1021 final Account account = entry.getKey(); 1022 boolean hasCalendar = entry.getValue(); 1023 if (hasCalendar) { 1024 if (Config.LOGV) { 1025 Log.v(TAG, "ignoring account " + account + 1026 " since it matched an existing calendar"); 1027 } 1028 continue; 1029 } 1030 String feedUrl = mCalendarClient.getDefaultCalendarUrl(account.name, 1031 CalendarClient.PROJECTION_PRIVATE_FULL, null/* query params */); 1032 feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl); 1033 if (Config.LOGV) { 1034 Log.v(TAG, "adding default calendar for account " + account); 1035 } 1036 ContentValues values = new ContentValues(); 1037 values.put(Calendars._SYNC_ACCOUNT, account.name); 1038 values.put(Calendars._SYNC_ACCOUNT_TYPE, account.type); 1039 values.put(Calendars.URL, feedUrl); 1040 values.put(Calendars.OWNER_ACCOUNT, 1041 CalendarSyncAdapter.calendarEmailAddressFromFeedUrl(feedUrl)); 1042 values.put(Calendars.DISPLAY_NAME, 1043 getContext().getString(R.string.calendar_default_name)); 1044 values.put(Calendars.SYNC_EVENTS, 1); 1045 values.put(Calendars.SELECTED, 1); 1046 values.put(Calendars.HIDDEN, 0); 1047 values.put(Calendars.COLOR, -14069085 /* blue */); 1048 // this is just our best guess. the real value will get updated 1049 // when the user does a sync. 1050 values.put(Calendars.TIMEZONE, Time.getCurrentTimezone()); 1051 values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS); 1052 insertInternal(Calendars.CONTENT_URI, values); 1053 1054 scheduleSync(account, false /* do a full sync */, null /* no url */); 1055 1056 } 1057 // Call the CalendarSyncAdapter's onAccountsChanged 1058 getTempProviderSyncAdapter().onAccountsChanged(accountsArray); 1059 } 1060 }; 1061 1062 AccountManager.get(getContext()).getAccountsByTypeAndFeatures( 1063 GoogleLoginServiceConstants.ACCOUNT_TYPE, features, callback, null); 1064 } 1065 1066 @Override queryInternal(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sort)1067 public Cursor queryInternal(Uri url, String[] projectionIn, 1068 String selection, String[] selectionArgs, String sort) { 1069 final SQLiteDatabase db = getDatabase(); 1070 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1071 1072 Cursor ret; 1073 1074 // Generate the body of the query 1075 int match = sURLMatcher.match(url); 1076 switch (match) 1077 { 1078 case EVENTS: 1079 qb.setTables("Events, Calendars"); 1080 qb.setProjectionMap(sEventsProjectionMap); 1081 qb.appendWhere("Events.calendar_id=Calendars._id"); 1082 break; 1083 case EVENTS_ID: 1084 qb.setTables("Events, Calendars"); 1085 qb.setProjectionMap(sEventsProjectionMap); 1086 qb.appendWhere("Events.calendar_id=Calendars._id"); 1087 qb.appendWhere(" AND Events._id="); 1088 qb.appendWhere(url.getPathSegments().get(1)); 1089 break; 1090 case DELETED_EVENTS: 1091 if (isTemporary()) { 1092 qb.setTables("DeletedEvents"); 1093 break; 1094 } else { 1095 throw new IllegalArgumentException("Unknown URL " + url); 1096 } 1097 case CALENDARS: 1098 qb.setTables("Calendars"); 1099 break; 1100 case CALENDARS_ID: 1101 qb.setTables("Calendars"); 1102 qb.appendWhere("_id="); 1103 qb.appendWhere(url.getPathSegments().get(1)); 1104 break; 1105 case INSTANCES: 1106 case INSTANCES_BY_DAY: 1107 long begin; 1108 long end; 1109 try { 1110 begin = Long.valueOf(url.getPathSegments().get(2)); 1111 } catch (NumberFormatException nfe) { 1112 throw new IllegalArgumentException("Cannot parse begin " 1113 + url.getPathSegments().get(2)); 1114 } 1115 try { 1116 end = Long.valueOf(url.getPathSegments().get(3)); 1117 } catch (NumberFormatException nfe) { 1118 throw new IllegalArgumentException("Cannot parse end " 1119 + url.getPathSegments().get(3)); 1120 } 1121 return handleInstanceQuery(qb, begin, end, projectionIn, 1122 selection, sort, match == INSTANCES_BY_DAY); 1123 case BUSYBITS: 1124 int startDay; 1125 int endDay; 1126 try { 1127 startDay = Integer.valueOf(url.getPathSegments().get(2)); 1128 } catch (NumberFormatException nfe) { 1129 throw new IllegalArgumentException("Cannot parse start day " 1130 + url.getPathSegments().get(2)); 1131 } 1132 try { 1133 endDay = Integer.valueOf(url.getPathSegments().get(3)); 1134 } catch (NumberFormatException nfe) { 1135 throw new IllegalArgumentException("Cannot parse end day " 1136 + url.getPathSegments().get(3)); 1137 } 1138 return handleBusyBitsQuery(qb, startDay, endDay, projectionIn, 1139 selection, sort); 1140 case ATTENDEES: 1141 qb.setTables("Attendees, Events, Calendars"); 1142 qb.setProjectionMap(sAttendeesProjectionMap); 1143 qb.appendWhere("Events.calendar_id=Calendars._id"); 1144 qb.appendWhere(" AND Events._id=Attendees.event_id"); 1145 break; 1146 case ATTENDEES_ID: 1147 qb.setTables("Attendees, Events, Calendars"); 1148 qb.setProjectionMap(sAttendeesProjectionMap); 1149 qb.appendWhere("Attendees._id="); 1150 qb.appendWhere(url.getPathSegments().get(1)); 1151 qb.appendWhere(" AND Events.calendar_id=Calendars._id"); 1152 qb.appendWhere(" AND Events._id=Attendees.event_id"); 1153 break; 1154 case REMINDERS: 1155 qb.setTables("Reminders"); 1156 break; 1157 case REMINDERS_ID: 1158 qb.setTables("Reminders, Events, Calendars"); 1159 qb.setProjectionMap(sRemindersProjectionMap); 1160 qb.appendWhere("Reminders._id="); 1161 qb.appendWhere(url.getLastPathSegment()); 1162 qb.appendWhere(" AND Events.calendar_id=Calendars._id"); 1163 qb.appendWhere(" AND Events._id=Reminders.event_id"); 1164 break; 1165 case CALENDAR_ALERTS: 1166 qb.setTables("CalendarAlerts, Events, Calendars"); 1167 qb.setProjectionMap(sCalendarAlertsProjectionMap); 1168 qb.appendWhere("Events.calendar_id=Calendars._id"); 1169 qb.appendWhere(" AND Events._id=CalendarAlerts.event_id"); 1170 break; 1171 case CALENDAR_ALERTS_BY_INSTANCE: 1172 qb.setTables("CalendarAlerts, Events, Calendars"); 1173 qb.setProjectionMap(sCalendarAlertsProjectionMap); 1174 qb.appendWhere("Events.calendar_id=Calendars._id"); 1175 qb.appendWhere(" AND Events._id=CalendarAlerts.event_id"); 1176 String groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN; 1177 return qb.query(db, projectionIn, selection, selectionArgs, 1178 groupBy, null, sort); 1179 case CALENDAR_ALERTS_ID: 1180 qb.setTables("CalendarAlerts, Events, Calendars"); 1181 qb.setProjectionMap(sCalendarAlertsProjectionMap); 1182 qb.appendWhere("CalendarAlerts._id="); 1183 qb.appendWhere(url.getLastPathSegment()); 1184 qb.appendWhere(" AND Events.calendar_id=Calendars._id"); 1185 qb.appendWhere(" AND Events._id=CalendarAlerts.event_id"); 1186 break; 1187 case EXTENDED_PROPERTIES: 1188 qb.setTables("ExtendedProperties"); 1189 break; 1190 case EXTENDED_PROPERTIES_ID: 1191 qb.setTables("ExtendedProperties, Events, Calendars"); 1192 // not sure if we need a projection map or a join. see what callers want. 1193 // qb.setProjectionMap(sExtendedPropertiesProjectionMap); 1194 qb.appendWhere("ExtendedProperties._id="); 1195 qb.appendWhere(url.getPathSegments().get(1)); 1196 // qb.appendWhere(" AND Events.calendar_id = Calendars._id"); 1197 // qb.appendWhere(" AND Events._id=ExtendedProperties.event_id"); 1198 break; 1199 1200 default: 1201 throw new IllegalArgumentException("Unknown URL " + url); 1202 } 1203 1204 // run the query 1205 ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort); 1206 1207 return ret; 1208 } 1209 1210 /* 1211 * Fills the Instances table, if necessary, for the given range and then 1212 * queries the Instances table. 1213 * 1214 * @param qb The query 1215 * @param rangeBegin start of range (Julian days or ms) 1216 * @param rangeEnd end of range (Julian days or ms) 1217 * @param projectionIn The projection 1218 * @param selection The selection 1219 * @param sort How to sort 1220 * @param searchByDay if true, range is in Julian days, if false, range is in ms 1221 * @return 1222 */ handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, long rangeEnd, String[] projectionIn, String selection, String sort, boolean searchByDay)1223 private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, 1224 long rangeEnd, String[] projectionIn, 1225 String selection, String sort, boolean searchByDay) { 1226 final SQLiteDatabase db = getDatabase(); 1227 1228 qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " + 1229 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)"); 1230 qb.setProjectionMap(sInstancesProjectionMap); 1231 if (searchByDay) { 1232 // Convert the first and last Julian day range to a range that uses 1233 // UTC milliseconds. 1234 Time time = new Time(); 1235 long beginMs = time.setJulianDay((int) rangeBegin); 1236 // We add one to lastDay because the time is set to 12am on the given 1237 // Julian day and we want to include all the events on the last day. 1238 long endMs = time.setJulianDay((int) rangeEnd + 1); 1239 // will lock the database. 1240 acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */); 1241 qb.appendWhere("startDay <= "); 1242 qb.appendWhere(String.valueOf(rangeEnd)); 1243 qb.appendWhere(" AND endDay >= "); 1244 } else { 1245 // will lock the database. 1246 acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */); 1247 qb.appendWhere("begin <= "); 1248 qb.appendWhere(String.valueOf(rangeEnd)); 1249 qb.appendWhere(" AND end >= "); 1250 } 1251 qb.appendWhere(String.valueOf(rangeBegin)); 1252 return qb.query(db, projectionIn, selection, null, null, null, sort); 1253 } 1254 handleBusyBitsQuery(SQLiteQueryBuilder qb, int startDay, int endDay, String[] projectionIn, String selection, String sort)1255 private Cursor handleBusyBitsQuery(SQLiteQueryBuilder qb, int startDay, 1256 int endDay, String[] projectionIn, 1257 String selection, String sort) { 1258 final SQLiteDatabase db = getDatabase(); 1259 acquireBusyBitRange(startDay, endDay); 1260 qb.setTables("BusyBits"); 1261 qb.setProjectionMap(sBusyBitsProjectionMap); 1262 qb.appendWhere("day >= "); 1263 qb.appendWhere(String.valueOf(startDay)); 1264 qb.appendWhere(" AND day <= "); 1265 qb.appendWhere(String.valueOf(endDay)); 1266 return qb.query(db, projectionIn, selection, null, null, null, sort); 1267 } 1268 1269 /** 1270 * Ensure that the date range given has all elements in the instance 1271 * table. Acquires the database lock and calls {@link #acquireInstanceRangeLocked}. 1272 * 1273 * @param begin start of range (ms) 1274 * @param end end of range (ms) 1275 * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN 1276 */ acquireInstanceRange(final long begin, final long end, final boolean useMinimumExpansionWindow)1277 private void acquireInstanceRange(final long begin, 1278 final long end, 1279 final boolean useMinimumExpansionWindow) { 1280 mDb.beginTransaction(); 1281 try { 1282 acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow); 1283 mDb.setTransactionSuccessful(); 1284 } finally { 1285 mDb.endTransaction(); 1286 } 1287 } 1288 1289 /** 1290 * Expands the Instances table (if needed) and the BusyBits table. 1291 * Acquires the database lock and calls {@link #acquireBusyBitRangeLocked}. 1292 */ acquireBusyBitRange(final int startDay, final int endDay)1293 private void acquireBusyBitRange(final int startDay, final int endDay) { 1294 mDb.beginTransaction(); 1295 try { 1296 acquireBusyBitRangeLocked(startDay, endDay); 1297 mDb.setTransactionSuccessful(); 1298 } finally { 1299 mDb.endTransaction(); 1300 } 1301 } 1302 1303 /** 1304 * Ensure that the date range given has all elements in the instance 1305 * table. The database lock must be held when calling this method. 1306 * 1307 * @param begin start of range (ms) 1308 * @param end end of range (ms) 1309 * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN 1310 */ acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow)1311 private void acquireInstanceRangeLocked(long begin, long end, 1312 boolean useMinimumExpansionWindow) { 1313 long expandBegin = begin; 1314 long expandEnd = end; 1315 1316 if (useMinimumExpansionWindow) { 1317 // if we end up having to expand events into the instances table, expand 1318 // events for a minimal amount of time, so we do not have to perform 1319 // expansions frequently. 1320 long span = end - begin; 1321 if (span < MINIMUM_EXPANSION_SPAN) { 1322 long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2; 1323 expandBegin -= additionalRange; 1324 expandEnd += additionalRange; 1325 } 1326 } 1327 1328 // Check if the timezone has changed. 1329 // We do this check here because the database is locked and we can 1330 // safely delete all the entries in the Instances table. 1331 MetaData.Fields fields = mMetaData.getFieldsLocked(); 1332 String dbTimezone = fields.timezone; 1333 long maxInstance = fields.maxInstance; 1334 long minInstance = fields.minInstance; 1335 String localTimezone = TimeZone.getDefault().getID(); 1336 boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone); 1337 1338 if (maxInstance == 0 || timezoneChanged) { 1339 // Empty the Instances table and expand from scratch. 1340 mDb.execSQL("DELETE FROM Instances;"); 1341 mDb.execSQL("DELETE FROM BusyBits;"); 1342 if (Config.LOGV) { 1343 Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances and Busybits," 1344 + " timezone changed: " + timezoneChanged); 1345 } 1346 expandInstanceRangeLocked(expandBegin, expandEnd, localTimezone); 1347 1348 mMetaData.writeLocked(localTimezone, expandBegin, expandEnd, 1349 0 /* startDay */, 0 /* endDay */); 1350 return; 1351 } 1352 1353 // If the desired range [begin, end] has already been 1354 // expanded, then simply return. The range is inclusive, that is, 1355 // events that touch either endpoint are included in the expansion. 1356 // This means that a zero-duration event that starts and ends at 1357 // the endpoint will be included. 1358 // We use [begin, end] here and not [expandBegin, expandEnd] for 1359 // checking the range because a common case is for the client to 1360 // request successive days or weeks, for example. If we checked 1361 // that the expanded range [expandBegin, expandEnd] then we would 1362 // always be expanding because there would always be one more day 1363 // or week that hasn't been expanded. 1364 if ((begin >= minInstance) && (end <= maxInstance)) { 1365 if (Config.LOGV) { 1366 Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd 1367 + ") falls within previously expanded range."); 1368 } 1369 return; 1370 } 1371 1372 // If the requested begin point has not been expanded, then include 1373 // more events than requested in the expansion (use "expandBegin"). 1374 if (begin < minInstance) { 1375 expandInstanceRangeLocked(expandBegin, minInstance, localTimezone); 1376 minInstance = expandBegin; 1377 } 1378 1379 // If the requested end point has not been expanded, then include 1380 // more events than requested in the expansion (use "expandEnd"). 1381 if (end > maxInstance) { 1382 expandInstanceRangeLocked(maxInstance, expandEnd, localTimezone); 1383 maxInstance = expandEnd; 1384 } 1385 1386 // Update the bounds on the Instances table. 1387 mMetaData.writeLocked(localTimezone, minInstance, maxInstance, 1388 fields.minBusyBit, fields.maxBusyBit); 1389 } 1390 acquireBusyBitRangeLocked(int firstDay, int lastDay)1391 private void acquireBusyBitRangeLocked(int firstDay, int lastDay) { 1392 if (firstDay > lastDay) { 1393 throw new IllegalArgumentException("firstDay must not be greater than lastDay"); 1394 } 1395 String localTimezone = TimeZone.getDefault().getID(); 1396 MetaData.Fields fields = mMetaData.getFieldsLocked(); 1397 String dbTimezone = fields.timezone; 1398 int minBusyBit = fields.minBusyBit; 1399 int maxBusyBit = fields.maxBusyBit; 1400 boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone); 1401 if (firstDay >= minBusyBit && lastDay <= maxBusyBit && !timezoneChanged) { 1402 if (Config.LOGV) { 1403 Log.v(TAG, "acquireBusyBitRangeLocked() no expansion needed"); 1404 } 1405 return; 1406 } 1407 1408 // Avoid gaps in the BusyBit table and avoid recomputing the busy bits 1409 // that are already in the table. If the busy bit range has been cleared, 1410 // don't bother checking. 1411 if (maxBusyBit != 0) { 1412 if (firstDay > maxBusyBit) { 1413 firstDay = maxBusyBit; 1414 } else if (lastDay < minBusyBit) { 1415 lastDay = minBusyBit; 1416 } else if (firstDay < minBusyBit && lastDay <= maxBusyBit) { 1417 lastDay = minBusyBit; 1418 } else if (lastDay > maxBusyBit && firstDay >= minBusyBit) { 1419 firstDay = maxBusyBit; 1420 } 1421 } 1422 1423 // Allocate space for the busy bits, one 32-bit integer for each day. 1424 int numDays = lastDay - firstDay + 1; 1425 int[] busybits = new int[numDays]; 1426 int[] allDayCounts = new int[numDays]; 1427 1428 // Convert the first and last Julian day range to a range that uses 1429 // UTC milliseconds. 1430 Time time = new Time(); 1431 long begin = time.setJulianDay(firstDay); 1432 1433 // We add one to lastDay because the time is set to 12am on the given 1434 // Julian day and we want to include all the events on the last day. 1435 long end = time.setJulianDay(lastDay + 1); 1436 1437 // Make sure the Instances table includes events in the range 1438 // [begin, end]. 1439 acquireInstanceRange(begin, end, true /* use minimum expansion window */); 1440 1441 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1442 qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " + 1443 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)"); 1444 qb.setProjectionMap(sInstancesProjectionMap); 1445 qb.appendWhere("begin <= "); 1446 qb.appendWhere(String.valueOf(end)); 1447 qb.appendWhere(" AND end >= "); 1448 qb.appendWhere(String.valueOf(begin)); 1449 qb.appendWhere(" AND "); 1450 qb.appendWhere(Instances.SELECTED); 1451 qb.appendWhere("=1"); 1452 1453 final SQLiteDatabase db = getDatabase(); 1454 // Get all the instances that overlap the range [begin,end] 1455 Cursor cursor = qb.query(db, sInstancesProjection, null, null, null, null, null); 1456 int count = 0; 1457 try { 1458 count = cursor.getCount(); 1459 while (cursor.moveToNext()) { 1460 int startDay = cursor.getInt(INSTANCES_INDEX_START_DAY); 1461 int endDay = cursor.getInt(INSTANCES_INDEX_END_DAY); 1462 int startMinute = cursor.getInt(INSTANCES_INDEX_START_MINUTE); 1463 int endMinute = cursor.getInt(INSTANCES_INDEX_END_MINUTE); 1464 boolean allDay = cursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0; 1465 fillBusyBits(firstDay, startDay, endDay, startMinute, endMinute, 1466 allDay, busybits, allDayCounts); 1467 } 1468 } finally { 1469 if (cursor != null) { 1470 cursor.close(); 1471 } 1472 } 1473 1474 if (count == 0) { 1475 return; 1476 } 1477 1478 // Read the busybit range again because that may have changed when we 1479 // called acquireInstanceRange(). 1480 fields = mMetaData.getFieldsLocked(); 1481 minBusyBit = fields.minBusyBit; 1482 maxBusyBit = fields.maxBusyBit; 1483 1484 // If the busybit range was cleared, then delete all the entries. 1485 if (maxBusyBit == 0) { 1486 mDb.execSQL("DELETE FROM BusyBits;"); 1487 } 1488 1489 // Merge the busy bits with the database. 1490 mergeBusyBits(firstDay, lastDay, busybits, allDayCounts); 1491 if (maxBusyBit == 0) { 1492 minBusyBit = firstDay; 1493 maxBusyBit = lastDay; 1494 } else { 1495 if (firstDay < minBusyBit) { 1496 minBusyBit = firstDay; 1497 } 1498 if (lastDay > maxBusyBit) { 1499 maxBusyBit = lastDay; 1500 } 1501 } 1502 // Update the busy bit range 1503 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance, 1504 minBusyBit, maxBusyBit); 1505 } 1506 1507 private static final String[] EXPAND_COLUMNS = new String[] { 1508 Events._ID, 1509 Events._SYNC_ID, 1510 Events.STATUS, 1511 Events.DTSTART, 1512 Events.DTEND, 1513 Events.EVENT_TIMEZONE, 1514 Events.RRULE, 1515 Events.RDATE, 1516 Events.EXRULE, 1517 Events.EXDATE, 1518 Events.DURATION, 1519 Events.ALL_DAY, 1520 Events.ORIGINAL_EVENT, 1521 Events.ORIGINAL_INSTANCE_TIME 1522 }; 1523 1524 /** 1525 * Make instances for the given range. 1526 */ expandInstanceRangeLocked(long begin, long end, String localTimezone)1527 private void expandInstanceRangeLocked(long begin, long end, String localTimezone) { 1528 1529 if (PROFILE) { 1530 Debug.startMethodTracing("expandInstanceRangeLocked"); 1531 } 1532 1533 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1534 Log.v(TAG, "Expanding events between " + begin + " and " + end); 1535 } 1536 1537 Cursor entries = getEntries(begin, end); 1538 try { 1539 performInstanceExpansion(begin, end, localTimezone, entries); 1540 } finally { 1541 if (entries != null) { 1542 entries.close(); 1543 } 1544 } 1545 if (PROFILE) { 1546 Debug.stopMethodTracing(); 1547 } 1548 } 1549 1550 /** 1551 * Get all entries affecting the given window. 1552 * @param begin Window start (ms). 1553 * @param end Window end (ms). 1554 * @return Cursor for the entries; caller must close it. 1555 */ getEntries(long begin, long end)1556 private Cursor getEntries(long begin, long end) { 1557 final SQLiteDatabase db = getDatabase(); 1558 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1559 qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)"); 1560 qb.setProjectionMap(sEventsProjectionMap); 1561 1562 String beginString = String.valueOf(begin); 1563 String endString = String.valueOf(end); 1564 1565 qb.appendWhere("(dtstart <= "); 1566 qb.appendWhere(endString); 1567 qb.appendWhere(" AND "); 1568 qb.appendWhere("(lastDate IS NULL OR lastDate >= "); 1569 qb.appendWhere(beginString); 1570 qb.appendWhere(")) OR ("); 1571 // grab recurrence exceptions that fall outside our expansion window but modify 1572 // recurrences that do fall within our window. we won't insert these into the output 1573 // set of instances, but instead will just add them to our cancellations list, so we 1574 // can cancel the correct recurrence expansion instances. 1575 qb.appendWhere("originalInstanceTime IS NOT NULL "); 1576 qb.appendWhere("AND originalInstanceTime <= "); 1577 qb.appendWhere(endString); 1578 qb.appendWhere(" AND "); 1579 // we don't have originalInstanceDuration or end time. for now, assume the original 1580 // instance lasts no longer than 1 week. 1581 // TODO: compute the originalInstanceEndTime or get this from the server. 1582 qb.appendWhere("originalInstanceTime >= "); 1583 qb.appendWhere(String.valueOf(begin - MAX_ASSUMED_DURATION)); 1584 qb.appendWhere(")"); 1585 1586 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1587 Log.v(TAG, "Retrieving events to expand: " + qb.toString()); 1588 } 1589 1590 return qb.query(db, EXPAND_COLUMNS, null, null, null, null, null); 1591 } 1592 1593 /** 1594 * Perform instance expansion on the given entries. 1595 * @param begin Window start (ms). 1596 * @param end Window end (ms). 1597 * @param localTimezone 1598 * @param entries The entries to process. 1599 */ performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries)1600 private void performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries) { 1601 RecurrenceProcessor rp = new RecurrenceProcessor(); 1602 1603 int statusColumn = entries.getColumnIndex(Events.STATUS); 1604 int dtstartColumn = entries.getColumnIndex(Events.DTSTART); 1605 int dtendColumn = entries.getColumnIndex(Events.DTEND); 1606 int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE); 1607 int durationColumn = entries.getColumnIndex(Events.DURATION); 1608 int rruleColumn = entries.getColumnIndex(Events.RRULE); 1609 int rdateColumn = entries.getColumnIndex(Events.RDATE); 1610 int exruleColumn = entries.getColumnIndex(Events.EXRULE); 1611 int exdateColumn = entries.getColumnIndex(Events.EXDATE); 1612 int allDayColumn = entries.getColumnIndex(Events.ALL_DAY); 1613 int idColumn = entries.getColumnIndex(Events._ID); 1614 int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID); 1615 int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT); 1616 int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME); 1617 1618 ContentValues initialValues; 1619 EventInstancesMap instancesMap = new EventInstancesMap(); 1620 1621 Duration duration = new Duration(); 1622 Time eventTime = new Time(); 1623 1624 // Invariant: entries contains all events that affect the current 1625 // window. It consists of: 1626 // a) Individual events that fall in the window. These will be 1627 // displayed. 1628 // b) Recurrences that included the window. These will be displayed 1629 // if not canceled. 1630 // c) Recurrence exceptions that fall in the window. These will be 1631 // displayed if not cancellations. 1632 // d) Recurrence exceptions that modify an instance inside the 1633 // window (subject to 1 week assumption above), but are outside 1634 // the window. These will not be displayed. Cases c and d are 1635 // distingushed by the start / end time. 1636 1637 while (entries.moveToNext()) { 1638 try { 1639 initialValues = null; 1640 1641 boolean allDay = entries.getInt(allDayColumn) != 0; 1642 1643 String eventTimezone = entries.getString(eventTimezoneColumn); 1644 if (allDay || TextUtils.isEmpty(eventTimezone)) { 1645 // in the events table, allDay events start at midnight. 1646 // this forces them to stay at midnight for all day events 1647 // TODO: check that this actually does the right thing. 1648 eventTimezone = Time.TIMEZONE_UTC; 1649 } 1650 1651 long dtstartMillis = entries.getLong(dtstartColumn); 1652 Long eventId = Long.valueOf(entries.getLong(idColumn)); 1653 1654 String durationStr = entries.getString(durationColumn); 1655 if (durationStr != null) { 1656 try { 1657 duration.parse(durationStr); 1658 } 1659 catch (DateException e) { 1660 Log.w(TAG, "error parsing duration for event " 1661 + eventId + "'" + durationStr + "'", e); 1662 duration.sign = 1; 1663 duration.weeks = 0; 1664 duration.days = 0; 1665 duration.hours = 0; 1666 duration.minutes = 0; 1667 duration.seconds = 0; 1668 durationStr = "+P0S"; 1669 } 1670 } 1671 1672 String syncId = entries.getString(syncIdColumn); 1673 String originalEvent = entries.getString(originalEventColumn); 1674 1675 long originalInstanceTimeMillis = -1; 1676 if (!entries.isNull(originalInstanceTimeColumn)) { 1677 originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn); 1678 } 1679 int status = entries.getInt(statusColumn); 1680 1681 String rruleStr = entries.getString(rruleColumn); 1682 String rdateStr = entries.getString(rdateColumn); 1683 String exruleStr = entries.getString(exruleColumn); 1684 String exdateStr = entries.getString(exdateColumn); 1685 1686 RecurrenceSet recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr); 1687 1688 if (recur.hasRecurrence()) { 1689 // the event is repeating 1690 1691 if (status == Events.STATUS_CANCELED) { 1692 // should not happen! 1693 Log.e(TAG, "Found canceled recurring event in " 1694 + "Events table. Ignoring."); 1695 continue; 1696 } 1697 1698 // need to parse the event into a local calendar. 1699 eventTime.timezone = eventTimezone; 1700 eventTime.set(dtstartMillis); 1701 eventTime.allDay = allDay; 1702 1703 if (durationStr == null) { 1704 // should not happen. 1705 Log.e(TAG, "Repeating event has no duration -- " 1706 + "should not happen."); 1707 if (allDay) { 1708 // set to one day. 1709 duration.sign = 1; 1710 duration.weeks = 0; 1711 duration.days = 1; 1712 duration.hours = 0; 1713 duration.minutes = 0; 1714 duration.seconds = 0; 1715 durationStr = "+P1D"; 1716 } else { 1717 // compute the duration from dtend, if we can. 1718 // otherwise, use 0s. 1719 duration.sign = 1; 1720 duration.weeks = 0; 1721 duration.days = 0; 1722 duration.hours = 0; 1723 duration.minutes = 0; 1724 if (!entries.isNull(dtendColumn)) { 1725 long dtendMillis = entries.getLong(dtendColumn); 1726 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000); 1727 durationStr = "+P" + duration.seconds + "S"; 1728 } else { 1729 duration.seconds = 0; 1730 durationStr = "+P0S"; 1731 } 1732 } 1733 } 1734 1735 long[] dates; 1736 dates = rp.expand(eventTime, recur, begin, end); 1737 1738 // Initialize the "eventTime" timezone outside the loop. 1739 // This is used in computeTimezoneDependentFields(). 1740 if (allDay) { 1741 eventTime.timezone = Time.TIMEZONE_UTC; 1742 } else { 1743 eventTime.timezone = localTimezone; 1744 } 1745 1746 long durationMillis = duration.getMillis(); 1747 for (long date : dates) { 1748 initialValues = new ContentValues(); 1749 initialValues.put(Instances.EVENT_ID, eventId); 1750 1751 initialValues.put(Instances.BEGIN, date); 1752 long dtendMillis = date + durationMillis; 1753 initialValues.put(Instances.END, dtendMillis); 1754 1755 computeTimezoneDependentFields(date, dtendMillis, 1756 eventTime, initialValues); 1757 instancesMap.add(syncId, initialValues); 1758 } 1759 } else { 1760 // the event is not repeating 1761 initialValues = new ContentValues(); 1762 1763 // if this event has an "original" field, then record 1764 // that we need to cancel the original event (we can't 1765 // do that here because the order of this loop isn't 1766 // defined) 1767 if (originalEvent != null && originalInstanceTimeMillis != -1) { 1768 initialValues.put(Events.ORIGINAL_EVENT, originalEvent); 1769 initialValues.put(Events.ORIGINAL_INSTANCE_TIME, 1770 originalInstanceTimeMillis); 1771 initialValues.put(Events.STATUS, status); 1772 } 1773 1774 long dtendMillis = dtstartMillis; 1775 if (durationStr == null) { 1776 if (!entries.isNull(dtendColumn)) { 1777 dtendMillis = entries.getLong(dtendColumn); 1778 } 1779 } else { 1780 dtendMillis = duration.addTo(dtstartMillis); 1781 } 1782 1783 // this non-recurring event might be a recurrence exception that doesn't 1784 // actually fall within our expansion window, but instead was selected 1785 // so we can correctly cancel expanded recurrence instances below. do not 1786 // add events to the instances map if they don't actually fall within our 1787 // expansion window. 1788 if ((dtendMillis < begin) || (dtstartMillis > end)) { 1789 if (originalEvent != null && originalInstanceTimeMillis != -1) { 1790 initialValues.put(Events.STATUS, Events.STATUS_CANCELED); 1791 } else { 1792 Log.w(TAG, "Unexpected event outside window: " + syncId); 1793 continue; 1794 } 1795 } 1796 1797 initialValues.put(Instances.EVENT_ID, eventId); 1798 initialValues.put(Instances.BEGIN, dtstartMillis); 1799 1800 initialValues.put(Instances.END, dtendMillis); 1801 1802 if (allDay) { 1803 eventTime.timezone = Time.TIMEZONE_UTC; 1804 } else { 1805 eventTime.timezone = localTimezone; 1806 } 1807 computeTimezoneDependentFields(dtstartMillis, dtendMillis, 1808 eventTime, initialValues); 1809 1810 instancesMap.add(syncId, initialValues); 1811 } 1812 } catch (DateException e) { 1813 Log.w(TAG, "RecurrenceProcessor error ", e); 1814 } catch (TimeFormatException e) { 1815 Log.w(TAG, "RecurrenceProcessor error ", e); 1816 } 1817 } 1818 1819 // Invariant: instancesMap contains all instances that affect the 1820 // window, indexed by original sync id. It consists of: 1821 // a) Individual events that fall in the window. They have: 1822 // EVENT_ID, BEGIN, END 1823 // b) Instances of recurrences that fall in the window. They may 1824 // be subject to exceptions. They have: 1825 // EVENT_ID, BEGIN, END 1826 // c) Exceptions that fall in the window. They have: 1827 // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS (since they can 1828 // be a modification or cancellation), EVENT_ID, BEGIN, END 1829 // d) Recurrence exceptions that modify an instance inside the 1830 // window but fall outside the window. They have: 1831 // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS = 1832 // STATUS_CANCELED, EVENT_ID, BEGIN, END 1833 1834 // First, delete the original instances corresponding to recurrence 1835 // exceptions. We do this by iterating over the list and for each 1836 // recurrence exception, we search the list for an instance with a 1837 // matching "original instance time". If we find such an instance, 1838 // we remove it from the list. If we don't find such an instance 1839 // then we cancel the recurrence exception. 1840 Set<String> keys = instancesMap.keySet(); 1841 for (String syncId : keys) { 1842 InstancesList list = instancesMap.get(syncId); 1843 for (ContentValues values : list) { 1844 1845 // If this instance is not a recurrence exception, then 1846 // skip it. 1847 if (!values.containsKey(Events.ORIGINAL_EVENT)) { 1848 continue; 1849 } 1850 1851 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 1852 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1853 InstancesList originalList = instancesMap.get(originalEvent); 1854 if (originalList == null) { 1855 // The original recurrence is not present, so don't try canceling it. 1856 continue; 1857 } 1858 1859 // Search the original event for a matching original 1860 // instance time. If there is a matching one, then remove 1861 // the original one. We do this both for exceptions that 1862 // change the original instance as well as for exceptions 1863 // that delete the original instance. 1864 for (int num = originalList.size() - 1; num >= 0; num--) { 1865 ContentValues originalValues = originalList.get(num); 1866 long beginTime = originalValues.getAsLong(Instances.BEGIN); 1867 if (beginTime == originalTime) { 1868 // We found the original instance, so remove it. 1869 originalList.remove(num); 1870 } 1871 } 1872 } 1873 } 1874 1875 // Invariant: instancesMap contains filtered instances. 1876 // It consists of: 1877 // a) Individual events that fall in the window. 1878 // b) Instances of recurrences that fall in the window and have not 1879 // been subject to exceptions. 1880 // c) Exceptions that fall in the window. They will have 1881 // STATUS_CANCELED if they are cancellations. 1882 // d) Recurrence exceptions that modify an instance inside the 1883 // window but fall outside the window. These are STATUS_CANCELED. 1884 1885 // Now do the inserts. Since the db lock is held when this method is executed, 1886 // this will be done in a transaction. 1887 // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db 1888 // while the calendar app is trying to query the db (expanding instances)), we will 1889 // not be "polite" and yield the lock until we're done. This will favor local query 1890 // operations over sync/write operations. 1891 for (String syncId : keys) { 1892 InstancesList list = instancesMap.get(syncId); 1893 for (ContentValues values : list) { 1894 1895 // If this instance was cancelled then don't create a new 1896 // instance. 1897 Integer status = values.getAsInteger(Events.STATUS); 1898 if (status != null && status == Events.STATUS_CANCELED) { 1899 continue; 1900 } 1901 1902 // Remove these fields before inserting a new instance 1903 values.remove(Events.ORIGINAL_EVENT); 1904 values.remove(Events.ORIGINAL_INSTANCE_TIME); 1905 values.remove(Events.STATUS); 1906 1907 mInstancesInserter.replace(values); 1908 } 1909 } 1910 } 1911 1912 /** 1913 * Computes the timezone-dependent fields of an instance of an event and 1914 * updates the "values" map to contain those fields. 1915 * 1916 * @param begin the start time of the instance (in UTC milliseconds) 1917 * @param end the end time of the instance (in UTC milliseconds) 1918 * @param local a Time object with the timezone set to the local timezone 1919 * @param values a map that will contain the timezone-dependent fields 1920 */ computeTimezoneDependentFields(long begin, long end, Time local, ContentValues values)1921 private void computeTimezoneDependentFields(long begin, long end, 1922 Time local, ContentValues values) { 1923 local.set(begin); 1924 int startDay = Time.getJulianDay(begin, local.gmtoff); 1925 int startMinute = local.hour * 60 + local.minute; 1926 1927 local.set(end); 1928 int endDay = Time.getJulianDay(end, local.gmtoff); 1929 int endMinute = local.hour * 60 + local.minute; 1930 1931 // Special case for midnight, which has endMinute == 0. Change 1932 // that to +24 hours on the previous day to make everything simpler. 1933 // Exception: if start and end minute are both 0 on the same day, 1934 // then leave endMinute alone. 1935 if (endMinute == 0 && endDay > startDay) { 1936 endMinute = 24 * 60; 1937 endDay -= 1; 1938 } 1939 1940 values.put(Instances.START_DAY, startDay); 1941 values.put(Instances.END_DAY, endDay); 1942 values.put(Instances.START_MINUTE, startMinute); 1943 values.put(Instances.END_MINUTE, endMinute); 1944 } 1945 fillBusyBits(int minDay, int startDay, int endDay, int startMinute, int endMinute, boolean allDay, int[] busybits, int[] allDayCounts)1946 private void fillBusyBits(int minDay, int startDay, int endDay, int startMinute, 1947 int endMinute, boolean allDay, int[] busybits, int[] allDayCounts) { 1948 1949 // The startDay can be less than the minDay if we have an event 1950 // that starts earlier than the time range we are interested in. 1951 // In that case, we ignore the time range that falls outside the 1952 // the range we are interested in. 1953 if (startDay < minDay) { 1954 startDay = minDay; 1955 startMinute = 0; 1956 } 1957 1958 // Likewise, truncate the event's end day so that it doesn't go past 1959 // the expected range. 1960 int numDays = busybits.length; 1961 int stopDay = endDay; 1962 if (stopDay > minDay + numDays - 1) { 1963 stopDay = minDay + numDays - 1; 1964 } 1965 int dayIndex = startDay - minDay; 1966 1967 if (allDay) { 1968 for (int day = startDay; day <= stopDay; day++, dayIndex++) { 1969 allDayCounts[dayIndex] += 1; 1970 } 1971 return; 1972 } 1973 1974 for (int day = startDay; day <= stopDay; day++, dayIndex++) { 1975 int endTime = endMinute; 1976 // If the event ends on a future day, then show it extending to 1977 // the end of this day. 1978 if (endDay > day) { 1979 endTime = 24 * 60; 1980 } 1981 1982 int startBit = startMinute / BUSYBIT_INTERVAL ; 1983 int endBit = (endTime + BUSYBIT_INTERVAL - 1) / BUSYBIT_INTERVAL; 1984 int len = endBit - startBit; 1985 if (len == 0) { 1986 len = 1; 1987 } 1988 if (len < 0 || len > 24) { 1989 Log.e("Cal", "fillBusyBits() error: len " + len 1990 + " startMinute,endTime " + startMinute + " , " + endTime 1991 + " startDay,endDay " + startDay + " , " + endDay); 1992 } else { 1993 int oneBits = BIT_MASKS[len]; 1994 busybits[dayIndex] |= oneBits << startBit; 1995 } 1996 1997 // Set the start minute to the beginning of the day, in 1998 // case this event spans multiple days. 1999 startMinute = 0; 2000 } 2001 } 2002 mergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts)2003 private void mergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts) { 2004 mDb.beginTransaction(); 2005 try { 2006 mergeBusyBitsLocked(startDay, endDay, busybits, allDayCounts); 2007 mDb.setTransactionSuccessful(); 2008 } finally { 2009 mDb.endTransaction(); 2010 } 2011 } 2012 mergeBusyBitsLocked(int startDay, int endDay, int[] busybits, int[] allDayCounts)2013 private void mergeBusyBitsLocked(int startDay, int endDay, int[] busybits, 2014 int[] allDayCounts) { 2015 final SQLiteDatabase db = getDatabase(); 2016 Cursor cursor = null; 2017 try { 2018 String selection = "day>=" + startDay + " AND day<=" + endDay; 2019 cursor = db.query("BusyBits", sBusyBitProjection, selection, null, null, null, null); 2020 if (cursor == null) { 2021 return; 2022 } 2023 while (cursor.moveToNext()) { 2024 int day = cursor.getInt(BUSYBIT_INDEX_DAY); 2025 int busy = cursor.getInt(BUSYBIT_INDEX_BUSYBITS); 2026 int allDayCount = cursor.getInt(BUSYBIT_INDEX_ALL_DAY_COUNT); 2027 2028 int dayIndex = day - startDay; 2029 busybits[dayIndex] |= busy; 2030 allDayCounts[dayIndex] += allDayCount; 2031 } 2032 } finally { 2033 if (cursor != null) { 2034 cursor.close(); 2035 } 2036 } 2037 2038 // Allocate a map that we can reuse 2039 ContentValues values = new ContentValues(); 2040 2041 // Write the busy bits to the database 2042 int len = busybits.length; 2043 for (int dayIndex = 0; dayIndex < len; dayIndex++) { 2044 int busy = busybits[dayIndex]; 2045 int allDayCount = allDayCounts[dayIndex]; 2046 if (busy == 0 && allDayCount == 0) { 2047 continue; 2048 } 2049 int day = startDay + dayIndex; 2050 2051 values.clear(); 2052 values.put(BusyBits.DAY, day); 2053 values.put(BusyBits.BUSYBITS, busy); 2054 values.put(BusyBits.ALL_DAY_COUNT, allDayCount); 2055 db.replace("BusyBits", null, values); 2056 } 2057 } 2058 2059 /** 2060 * Updates the BusyBit table when a new event is inserted into the Events 2061 * table. This is called after the event has been entered into the Events 2062 * table. If the event time is not within the date range of the current 2063 * BusyBits table, then the busy bits are not updated. The BusyBits 2064 * table is not automatically expanded to include this event. 2065 * 2066 * @param eventId the id of the newly created event 2067 * @param values the ContentValues for the new event 2068 */ insertBusyBitsLocked(long eventId, ContentValues values)2069 private void insertBusyBitsLocked(long eventId, ContentValues values) { 2070 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2071 if (fields.maxBusyBit == 0) { 2072 return; 2073 } 2074 2075 // If this is a recurrence event, then the expanded Instances range 2076 // should be 0 because this is called after updateInstancesLocked(). 2077 // But for now check this condition and report an error if it occurs. 2078 // In the future, we could even support recurring events by 2079 // expanding them here and updating the busy bits for each instance. 2080 if (isRecurrenceEvent(values)) { 2081 Log.e(TAG, "insertBusyBitsLocked(): unexpected recurrence event\n"); 2082 return; 2083 } 2084 2085 long dtstartMillis = values.getAsLong(Events.DTSTART); 2086 Long dtendMillis = values.getAsLong(Events.DTEND); 2087 if (dtendMillis == null) { 2088 dtendMillis = dtstartMillis; 2089 } 2090 2091 boolean allDay = false; 2092 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2093 if (allDayInteger != null) { 2094 allDay = allDayInteger != 0; 2095 } 2096 2097 Time time = new Time(); 2098 if (allDay) { 2099 time.timezone = Time.TIMEZONE_UTC; 2100 } 2101 2102 ContentValues busyValues = new ContentValues(); 2103 computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues); 2104 2105 int startDay = busyValues.getAsInteger(Instances.START_DAY); 2106 int endDay = busyValues.getAsInteger(Instances.END_DAY); 2107 2108 // If the event time is not in the expanded BusyBits range, 2109 // then return. 2110 if (startDay > fields.maxBusyBit || endDay < fields.minBusyBit) { 2111 return; 2112 } 2113 2114 // Allocate space for the busy bits, one 32-bit integer for each day, 2115 // plus 24 bytes for the count of events that occur in each time slot. 2116 int numDays = endDay - startDay + 1; 2117 int[] busybits = new int[numDays]; 2118 int[] allDayCounts = new int[numDays]; 2119 2120 int startMinute = busyValues.getAsInteger(Instances.START_MINUTE); 2121 int endMinute = busyValues.getAsInteger(Instances.END_MINUTE); 2122 fillBusyBits(startDay, startDay, endDay, startMinute, endMinute, 2123 allDay, busybits, allDayCounts); 2124 mergeBusyBits(startDay, endDay, busybits, allDayCounts); 2125 } 2126 2127 /** 2128 * Updates the busy bits for an event that is being updated. This is 2129 * called before the event is updated in the Events table because we need 2130 * to know the time of the event before it was changed. 2131 * 2132 * @param eventId the id of the event being updated 2133 * @param values the ContentValues for the updated event 2134 */ updateBusyBitsLocked(long eventId, ContentValues values)2135 private void updateBusyBitsLocked(long eventId, ContentValues values) { 2136 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2137 if (fields.maxBusyBit == 0) { 2138 return; 2139 } 2140 2141 // If this is a recurring event, then clear the BusyBits table. 2142 if (isRecurrenceEvent(values)) { 2143 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance, 2144 0 /* startDay */, 0 /* endDay */); 2145 return; 2146 } 2147 2148 // If the event fields being updated don't contain the start or end 2149 // time, then we don't need to bother updating the BusyBits table. 2150 Long dtstartLong = values.getAsLong(Events.DTSTART); 2151 Long dtendLong = values.getAsLong(Events.DTEND); 2152 if (dtstartLong == null && dtendLong == null) { 2153 return; 2154 } 2155 2156 // If the timezone has changed, then clear the busy bits table 2157 // and return. 2158 String dbTimezone = fields.timezone; 2159 String localTimezone = TimeZone.getDefault().getID(); 2160 boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone); 2161 if (timezoneChanged) { 2162 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance, 2163 0 /* startDay */, 0 /* endDay */); 2164 return; 2165 } 2166 2167 // Read the existing event start and end times from the Events table. 2168 TimeRange eventRange = readEventStartEnd(eventId); 2169 2170 // Fill in the new start time (if missing) or the new end time (if 2171 // missing) from the existing event start and end times. 2172 long dtstartMillis; 2173 if (dtstartLong != null) { 2174 dtstartMillis = dtstartLong; 2175 } else { 2176 dtstartMillis = eventRange.begin; 2177 } 2178 2179 long dtendMillis; 2180 if (dtendLong != null) { 2181 dtendMillis = dtendLong; 2182 } else { 2183 dtendMillis = eventRange.end; 2184 } 2185 2186 // Compute the start and end Julian days for the event. 2187 Time time = new Time(); 2188 if (eventRange.allDay) { 2189 time.timezone = Time.TIMEZONE_UTC; 2190 } 2191 ContentValues busyValues = new ContentValues(); 2192 computeTimezoneDependentFields(eventRange.begin, eventRange.end, time, busyValues); 2193 int oldStartDay = busyValues.getAsInteger(Instances.START_DAY); 2194 int oldEndDay = busyValues.getAsInteger(Instances.END_DAY); 2195 2196 boolean allDay = false; 2197 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2198 if (allDayInteger != null) { 2199 allDay = allDayInteger != 0; 2200 } 2201 2202 if (allDay) { 2203 time.timezone = Time.TIMEZONE_UTC; 2204 } else { 2205 time.timezone = TimeZone.getDefault().getID(); 2206 } 2207 2208 computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues); 2209 int newStartDay = busyValues.getAsInteger(Instances.START_DAY); 2210 int newEndDay = busyValues.getAsInteger(Instances.END_DAY); 2211 2212 // If both the old and new event times are outside the expanded 2213 // BusyBits table, then return. 2214 if ((oldStartDay > fields.maxBusyBit || oldEndDay < fields.minBusyBit) 2215 && (newStartDay > fields.maxBusyBit || newEndDay < fields.minBusyBit)) { 2216 return; 2217 } 2218 2219 // If the old event time is within the expanded Instances range, 2220 // then clear the BusyBits table and return. 2221 if (oldStartDay <= fields.maxBusyBit && oldEndDay >= fields.minBusyBit) { 2222 // We could recompute the busy bits for the days containing the 2223 // old event time. For now, just clear the BusyBits table. 2224 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance, 2225 0 /* startDay */, 0 /* endDay */); 2226 return; 2227 } 2228 2229 // The new event time is within the expanded Instances range. 2230 // So insert the busy bits for that day (or days). 2231 2232 // Allocate space for the busy bits, one 32-bit integer for each day, 2233 // plus 24 bytes for the count of events that occur in each time slot. 2234 int numDays = newEndDay - newStartDay + 1; 2235 int[] busybits = new int[numDays]; 2236 int[] allDayCounts = new int[numDays]; 2237 2238 int startMinute = busyValues.getAsInteger(Instances.START_MINUTE); 2239 int endMinute = busyValues.getAsInteger(Instances.END_MINUTE); 2240 fillBusyBits(newStartDay, newStartDay, newEndDay, startMinute, endMinute, 2241 allDay, busybits, allDayCounts); 2242 mergeBusyBits(newStartDay, newEndDay, busybits, allDayCounts); 2243 } 2244 2245 /** 2246 * This method is called just before an event is deleted. 2247 * 2248 * @param eventId 2249 */ deleteBusyBitsLocked(long eventId)2250 private void deleteBusyBitsLocked(long eventId) { 2251 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2252 if (fields.maxBusyBit == 0) { 2253 return; 2254 } 2255 2256 // TODO: if the event being deleted is not a recurring event and the 2257 // start and end time are outside the BusyBit range, then we could 2258 // avoid clearing the BusyBits table. For now, always clear the 2259 // BusyBits table because deleting events is relatively rare. 2260 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance, 2261 0 /* startDay */, 0 /* endDay */); 2262 } 2263 2264 // Read the start and end time for an event from the Events table. 2265 // Also read the "all-day" indicator. readEventStartEnd(long eventId)2266 private TimeRange readEventStartEnd(long eventId) { 2267 Cursor cursor = null; 2268 TimeRange range = new TimeRange(); 2269 try { 2270 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 2271 new String[] { Events.DTSTART, Events.DTEND, Events.ALL_DAY }, 2272 null /* selection */, 2273 null /* selectionArgs */, 2274 null /* sort */); 2275 if (cursor == null || !cursor.moveToFirst()) { 2276 Log.d(TAG, "Couldn't find " + eventId + " in Events table"); 2277 return null; 2278 } 2279 range.begin = cursor.getLong(0); 2280 range.end = cursor.getLong(1); 2281 range.allDay = cursor.getInt(2) != 0; 2282 } finally { 2283 if (cursor != null) { 2284 cursor.close(); 2285 } 2286 } 2287 return range; 2288 } 2289 2290 @Override getType(Uri url)2291 public String getType(Uri url) { 2292 int match = sURLMatcher.match(url); 2293 switch (match) { 2294 case EVENTS: 2295 return "vnd.android.cursor.dir/event"; 2296 case EVENTS_ID: 2297 return "vnd.android.cursor.item/event"; 2298 case REMINDERS: 2299 return "vnd.android.cursor.dir/reminder"; 2300 case REMINDERS_ID: 2301 return "vnd.android.cursor.item/reminder"; 2302 case CALENDAR_ALERTS: 2303 return "vnd.android.cursor.dir/calendar-alert"; 2304 case CALENDAR_ALERTS_BY_INSTANCE: 2305 return "vnd.android.cursor.dir/calendar-alert-by-instance"; 2306 case CALENDAR_ALERTS_ID: 2307 return "vnd.android.cursor.item/calendar-alert"; 2308 case INSTANCES: 2309 case INSTANCES_BY_DAY: 2310 return "vnd.android.cursor.dir/event-instance"; 2311 case BUSYBITS: 2312 return "vnd.android.cursor.dir/busybits"; 2313 default: 2314 throw new IllegalArgumentException("Unknown URL " + url); 2315 } 2316 } 2317 isRecurrenceEvent(ContentValues values)2318 public static boolean isRecurrenceEvent(ContentValues values) { 2319 return (!TextUtils.isEmpty(values.getAsString(Events.RRULE))|| 2320 !TextUtils.isEmpty(values.getAsString(Events.RDATE))|| 2321 !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT))); 2322 } 2323 2324 @Override insertInternal(Uri url, ContentValues initialValues)2325 public Uri insertInternal(Uri url, ContentValues initialValues) { 2326 final SQLiteDatabase db = getDatabase(); 2327 long rowID; 2328 2329 int match = sURLMatcher.match(url); 2330 switch (match) { 2331 case EVENTS: 2332 if (!isTemporary()) { 2333 initialValues.put(Events._SYNC_DIRTY, 1); 2334 if (!initialValues.containsKey(Events.DTSTART)) { 2335 throw new RuntimeException("DTSTART field missing from event"); 2336 } 2337 } 2338 // TODO: avoid the call to updateBundleFromEvent if this is just finding local 2339 // changes. or avoid for temp providers altogether, if we can compute this 2340 // during a merge. 2341 // TODO: do we really need to make a copy? 2342 ContentValues updatedValues = updateContentValuesFromEvent(initialValues); 2343 if (updatedValues == null) { 2344 throw new RuntimeException("Could not insert event."); 2345 // return null; 2346 } 2347 String owner = null; 2348 if (updatedValues.containsKey(Events.CALENDAR_ID) && 2349 !updatedValues.containsKey(Events.ORGANIZER)) { 2350 owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID)); 2351 // TODO: This isn't entirely correct. If a guest is adding a recurrence 2352 // exception to an event, the organizer should stay the original organizer. 2353 // This value doesn't go to the server and it will get fixed on sync, 2354 // so it shouldn't really matter. 2355 if (owner != null) { 2356 updatedValues.put(Events.ORGANIZER, owner); 2357 } 2358 } 2359 2360 long rowId = mEventsInserter.insert(updatedValues); 2361 Uri uri = Uri.parse("content://" + url.getAuthority() + "/events/" + rowId); 2362 if (!isTemporary() && rowId != -1) { 2363 updateEventRawTimesLocked(rowId, updatedValues); 2364 updateInstancesLocked(updatedValues, rowId, true /* new event */, db); 2365 insertBusyBitsLocked(rowId, updatedValues); 2366 2367 // If we inserted a new event that specified the self-attendee 2368 // status, then we need to add an entry to the attendees table. 2369 if (initialValues.containsKey(Events.SELF_ATTENDEE_STATUS)) { 2370 int status = initialValues.getAsInteger(Events.SELF_ATTENDEE_STATUS); 2371 if (owner == null) { 2372 owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID)); 2373 } 2374 createAttendeeEntry(rowId, status, owner); 2375 } 2376 triggerAppWidgetUpdate(rowId); 2377 } 2378 2379 return uri; 2380 case CALENDARS: 2381 if (!isTemporary()) { 2382 Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS); 2383 if (syncEvents != null && syncEvents == 1) { 2384 String accountName = initialValues.getAsString(Calendars._SYNC_ACCOUNT); 2385 String accountType = initialValues.getAsString( 2386 Calendars._SYNC_ACCOUNT_TYPE); 2387 final Account account = new Account(accountName, accountType); 2388 String calendarUrl = initialValues.getAsString(Calendars.URL); 2389 scheduleSync(account, false /* two-way sync */, calendarUrl); 2390 } 2391 } 2392 rowID = mCalendarsInserter.insert(initialValues); 2393 return ContentUris.withAppendedId(Calendars.CONTENT_URI, rowID); 2394 case ATTENDEES: 2395 if (!initialValues.containsKey(Attendees.EVENT_ID)) { 2396 throw new IllegalArgumentException("Attendees values must " 2397 + "contain an event_id"); 2398 } 2399 rowID = mAttendeesInserter.insert(initialValues); 2400 2401 // Copy the attendee status value to the Events table. 2402 updateEventAttendeeStatus(db, initialValues); 2403 2404 return ContentUris.withAppendedId(Calendar.Attendees.CONTENT_URI, rowID); 2405 case REMINDERS: 2406 if (!initialValues.containsKey(Reminders.EVENT_ID)) { 2407 throw new IllegalArgumentException("Reminders values must " 2408 + "contain an event_id"); 2409 } 2410 rowID = mRemindersInserter.insert(initialValues); 2411 2412 if (!isTemporary()) { 2413 // Schedule another event alarm, if necessary 2414 if (Log.isLoggable(TAG, Log.DEBUG)) { 2415 Log.d(TAG, "insertInternal() changing reminder"); 2416 } 2417 scheduleNextAlarm(false /* do not remove alarms */); 2418 } 2419 return ContentUris.withAppendedId(Calendar.Reminders.CONTENT_URI, rowID); 2420 case CALENDAR_ALERTS: 2421 if (!initialValues.containsKey(CalendarAlerts.EVENT_ID)) { 2422 throw new IllegalArgumentException("CalendarAlerts values must " 2423 + "contain an event_id"); 2424 } 2425 rowID = mCalendarAlertsInserter.insert(initialValues); 2426 2427 return Uri.parse(CalendarAlerts.CONTENT_URI + "/" + rowID); 2428 case EXTENDED_PROPERTIES: 2429 if (!initialValues.containsKey(Calendar.ExtendedProperties.EVENT_ID)) { 2430 throw new IllegalArgumentException("ExtendedProperties values must " 2431 + "contain an event_id"); 2432 } 2433 rowID = mExtendedPropertiesInserter.insert(initialValues); 2434 2435 return ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, rowID); 2436 case DELETED_EVENTS: 2437 if (isTemporary()) { 2438 rowID = mDeletedEventsInserter.insert(initialValues); 2439 return ContentUris.withAppendedId(Calendar.Events.DELETED_CONTENT_URI, rowID); 2440 } 2441 // fallthrough 2442 case EVENTS_ID: 2443 case REMINDERS_ID: 2444 case CALENDAR_ALERTS_ID: 2445 case EXTENDED_PROPERTIES_ID: 2446 case INSTANCES: 2447 case INSTANCES_BY_DAY: 2448 throw new UnsupportedOperationException("Cannot insert into that URL"); 2449 default: 2450 throw new IllegalArgumentException("Unknown URL " + url); 2451 } 2452 } 2453 2454 /** 2455 * Gets the calendar's owner for an event. 2456 * @param calId 2457 * @return email of owner or null 2458 */ getOwner(long calId)2459 private String getOwner(long calId) { 2460 // Get the email address of this user from this Calendar 2461 String emailAddress = null; 2462 Cursor cursor = null; 2463 try { 2464 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 2465 new String[] { Calendars.OWNER_ACCOUNT }, 2466 null /* selection */, 2467 null /* selectionArgs */, 2468 null /* sort */); 2469 if (cursor == null || !cursor.moveToFirst()) { 2470 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 2471 return null; 2472 } 2473 emailAddress = cursor.getString(0); 2474 } finally { 2475 if (cursor != null) { 2476 cursor.close(); 2477 } 2478 } 2479 return emailAddress; 2480 } 2481 2482 /** 2483 * Creates an entry in the Attendees table that refers to the given event 2484 * and that has the given response status. 2485 * 2486 * @param eventId the event id that the new entry in the Attendees table 2487 * should refer to 2488 * @param status the response status 2489 * @param emailAddress the email of the attendee 2490 */ createAttendeeEntry(long eventId, int status, String emailAddress)2491 private void createAttendeeEntry(long eventId, int status, String emailAddress) { 2492 ContentValues values = new ContentValues(); 2493 values.put(Attendees.EVENT_ID, eventId); 2494 values.put(Attendees.ATTENDEE_STATUS, status); 2495 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 2496 // TODO: The relationship could actually be ORGANIZER, but it will get straightened out 2497 // on sync. 2498 values.put(Attendees.ATTENDEE_RELATIONSHIP, 2499 Attendees.RELATIONSHIP_ATTENDEE); 2500 values.put(Attendees.ATTENDEE_EMAIL, emailAddress); 2501 2502 // We don't know the ATTENDEE_NAME but that will be filled in by the 2503 // server and sent back to us. 2504 mAttendeesInserter.insert(values); 2505 } 2506 2507 /** 2508 * Updates the attendee status in the Events table to be consistent with 2509 * the value in the Attendees table. 2510 * 2511 * @param db the database 2512 * @param attendeeValues the column values for one row in the Attendees 2513 * table. 2514 */ updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues)2515 private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) { 2516 // Get the event id for this attendee 2517 long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID); 2518 2519 if (MULTIPLE_ATTENDEES_PER_EVENT) { 2520 // Get the calendar id for this event 2521 Cursor cursor = null; 2522 long calId; 2523 try { 2524 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 2525 new String[] { Events.CALENDAR_ID }, 2526 null /* selection */, 2527 null /* selectionArgs */, 2528 null /* sort */); 2529 if (cursor == null || !cursor.moveToFirst()) { 2530 Log.d(TAG, "Couldn't find " + eventId + " in Events table"); 2531 return; 2532 } 2533 calId = cursor.getLong(0); 2534 } finally { 2535 if (cursor != null) { 2536 cursor.close(); 2537 } 2538 } 2539 2540 // Get the owner email for this Calendar 2541 String calendarEmail = null; 2542 cursor = null; 2543 try { 2544 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 2545 new String[] { Calendars.OWNER_ACCOUNT }, 2546 null /* selection */, 2547 null /* selectionArgs */, 2548 null /* sort */); 2549 if (cursor == null || !cursor.moveToFirst()) { 2550 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 2551 return; 2552 } 2553 calendarEmail = cursor.getString(0); 2554 } finally { 2555 if (cursor != null) { 2556 cursor.close(); 2557 } 2558 } 2559 2560 if (calendarEmail == null) { 2561 return; 2562 } 2563 2564 // Get the email address for this attendee 2565 String attendeeEmail = null; 2566 if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 2567 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL); 2568 } 2569 2570 // If the attendee email does not match the calendar email, then this 2571 // attendee is not the owner of this calendar so we don't update the 2572 // selfAttendeeStatus in the event. 2573 if (!calendarEmail.equals(attendeeEmail)) { 2574 return; 2575 } 2576 } 2577 2578 int status = Attendees.ATTENDEE_STATUS_NONE; 2579 if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) { 2580 int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 2581 if (rel == Attendees.RELATIONSHIP_ORGANIZER) { 2582 status = Attendees.ATTENDEE_STATUS_ACCEPTED; 2583 } 2584 } 2585 2586 if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) { 2587 status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS); 2588 } 2589 2590 ContentValues values = new ContentValues(); 2591 values.put(Events.SELF_ATTENDEE_STATUS, status); 2592 db.update("Events", values, "_id="+eventId, null); 2593 } 2594 2595 /** 2596 * Updates the instances table when an event is added or updated. 2597 * @param values The new values of the event. 2598 * @param rowId The database row id of the event. 2599 * @param newEvent true if the event is new. 2600 * @param db The database 2601 */ updateInstancesLocked(ContentValues values, long rowId, boolean newEvent, SQLiteDatabase db)2602 private void updateInstancesLocked(ContentValues values, 2603 long rowId, 2604 boolean newEvent, 2605 SQLiteDatabase db) { 2606 2607 // If there are no expanded Instances, then return. 2608 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2609 if (fields.maxInstance == 0) { 2610 return; 2611 } 2612 2613 Long dtstartMillis = values.getAsLong(Events.DTSTART); 2614 if (dtstartMillis == null) { 2615 if (newEvent) { 2616 // must be present for a new event. 2617 throw new RuntimeException("DTSTART missing."); 2618 } 2619 if (Config.LOGV) Log.v(TAG, "Missing DTSTART. " 2620 + "No need to update instance."); 2621 return; 2622 } 2623 2624 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 2625 Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 2626 2627 if (!newEvent) { 2628 // Want to do this for regular event, recurrence, or exception. 2629 // For recurrence or exception, more deletion may happen below if we 2630 // do an instance expansion. This deletion will suffice if the exception 2631 // is moved outside the window, for instance. 2632 db.delete("Instances", "event_id=" + rowId, null /* selectionArgs */); 2633 } 2634 2635 if (isRecurrenceEvent(values)) { 2636 // The recurrence or exception needs to be (re-)expanded if: 2637 // a) Exception or recurrence that falls inside window 2638 boolean insideWindow = dtstartMillis <= fields.maxInstance && 2639 (lastDateMillis == null || lastDateMillis >= fields.minInstance); 2640 // b) Exception that affects instance inside window 2641 // These conditions match the query in getEntries 2642 // See getEntries comment for explanation of subtracting 1 week. 2643 boolean affectsWindow = originalInstanceTime != null && 2644 originalInstanceTime <= fields.maxInstance && 2645 originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION; 2646 if (insideWindow || affectsWindow) { 2647 updateRecurrenceInstancesLocked(values, rowId, db); 2648 } 2649 // TODO: an exception creation or update could be optimized by 2650 // updating just the affected instances, instead of regenerating 2651 // the recurrence. 2652 return; 2653 } 2654 2655 Long dtendMillis = values.getAsLong(Events.DTEND); 2656 if (dtendMillis == null) { 2657 dtendMillis = dtstartMillis; 2658 } 2659 2660 // if the event is in the expanded range, insert 2661 // into the instances table. 2662 // TODO: deal with durations. currently, durations are only used in 2663 // recurrences. 2664 2665 if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) { 2666 ContentValues instanceValues = new ContentValues(); 2667 instanceValues.put(Instances.EVENT_ID, rowId); 2668 instanceValues.put(Instances.BEGIN, dtstartMillis); 2669 instanceValues.put(Instances.END, dtendMillis); 2670 2671 boolean allDay = false; 2672 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2673 if (allDayInteger != null) { 2674 allDay = allDayInteger != 0; 2675 } 2676 2677 // Update the timezone-dependent fields. 2678 Time local = new Time(); 2679 if (allDay) { 2680 local.timezone = Time.TIMEZONE_UTC; 2681 } else { 2682 local.timezone = fields.timezone; 2683 } 2684 2685 computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues); 2686 mInstancesInserter.insert(instanceValues); 2687 } 2688 } 2689 2690 /** 2691 * Determines the recurrence entries associated with a particular recurrence. 2692 * This set is the base recurrence and any exception. 2693 * 2694 * Normally the entries are indicated by the sync id of the base recurrence 2695 * (which is the originalEvent in the exceptions). 2696 * However, a complication is that a recurrence may not yet have a sync id. 2697 * In that case, the recurrence is specified by the rowId. 2698 * 2699 * @param recurrenceSyncId The sync id of the base recurrence, or null. 2700 * @param rowId The row id of the base recurrence. 2701 * @return the relevant entries. 2702 */ getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId)2703 private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) { 2704 final SQLiteDatabase db = getDatabase(); 2705 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2706 2707 qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)"); 2708 qb.setProjectionMap(sEventsProjectionMap); 2709 if (recurrenceSyncId == null) { 2710 String where = "Events._id = " + rowId; 2711 qb.appendWhere(where); 2712 } else { 2713 String where = "Events._sync_id = \"" + recurrenceSyncId + "\"" 2714 + " OR Events.originalEvent = \"" + recurrenceSyncId + "\""; 2715 qb.appendWhere(where); 2716 } 2717 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2718 Log.v(TAG, "Retrieving events to expand: " + qb.toString()); 2719 } 2720 2721 return qb.query(db, EXPAND_COLUMNS, null /* selection */, null /* selectionArgs */, null /* groupBy */, null /* having */, null /* sortOrder */); 2722 } 2723 2724 /** 2725 * Do incremental Instances update of a recurrence or recurrence exception. 2726 * 2727 * This method does performInstanceExpansion on just the modified recurrence, 2728 * to avoid the overhead of recomputing the entire instance table. 2729 * 2730 * @param values The new values of the event. 2731 * @param rowId The database row id of the event. 2732 * @param db The database 2733 */ updateRecurrenceInstancesLocked(ContentValues values, long rowId, SQLiteDatabase db)2734 private void updateRecurrenceInstancesLocked(ContentValues values, 2735 long rowId, 2736 SQLiteDatabase db) { 2737 MetaData.Fields fields = mMetaData.getFieldsLocked(); 2738 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT); 2739 String recurrenceSyncId = null; 2740 if (originalEvent != null) { 2741 recurrenceSyncId = originalEvent; 2742 } else { 2743 // Get the recurrence's sync id from the database 2744 recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events" 2745 + " WHERE _id = " + rowId, null /* selection args */); 2746 } 2747 // recurrenceSyncId is the _sync_id of the underlying recurrence 2748 // If the recurrence hasn't gone to the server, it will be null. 2749 2750 // Need to clear out old instances 2751 if (recurrenceSyncId == null) { 2752 // Creating updating a recurrence that hasn't gone to the server. 2753 // Need to delete based on row id 2754 String where = "_id IN (SELECT Instances._id as _id" 2755 + " FROM Instances INNER JOIN Events" 2756 + " ON (Events._id = Instances.event_id)" 2757 + " WHERE Events._id =?)"; 2758 db.delete("Instances", where, new String[]{"" + rowId}); 2759 } else { 2760 // Creating or modifying a recurrence or exception. 2761 // Delete instances for recurrence (_sync_id = recurrenceSyncId) 2762 // and all exceptions (originalEvent = recurrenceSyncId) 2763 String where = "_id IN (SELECT Instances._id as _id" 2764 + " FROM Instances INNER JOIN Events" 2765 + " ON (Events._id = Instances.event_id)" 2766 + " WHERE Events._sync_id =?" 2767 + " OR Events.originalEvent =?)"; 2768 db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId}); 2769 } 2770 2771 // Now do instance expansion 2772 Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId); 2773 try { 2774 performInstanceExpansion(fields.minInstance, fields.maxInstance, fields.timezone, entries); 2775 } finally { 2776 if (entries != null) { 2777 entries.close(); 2778 } 2779 } 2780 2781 // Clear busy bits 2782 mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance, 2783 0 /* startDay */, 0 /* endDay */); 2784 } 2785 calculateLastDate(ContentValues values)2786 long calculateLastDate(ContentValues values) 2787 throws DateException { 2788 // Allow updates to some event fields like the title or hasAlarm 2789 // without requiring DTSTART. 2790 if (!values.containsKey(Events.DTSTART)) { 2791 if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE) 2792 || values.containsKey(Events.DURATION) 2793 || values.containsKey(Events.EVENT_TIMEZONE) 2794 || values.containsKey(Events.RDATE) 2795 || values.containsKey(Events.EXRULE) 2796 || values.containsKey(Events.EXDATE)) { 2797 throw new RuntimeException("DTSTART field missing from event"); 2798 } 2799 return -1; 2800 } 2801 long dtstartMillis = values.getAsLong(Events.DTSTART); 2802 long lastMillis = -1; 2803 2804 // Can we use dtend with a repeating event? What does that even 2805 // mean? 2806 // NOTE: if the repeating event has a dtend, we convert it to a 2807 // duration during event processing, so this situation should not 2808 // occur. 2809 Long dtEnd = values.getAsLong(Events.DTEND); 2810 if (dtEnd != null) { 2811 lastMillis = dtEnd; 2812 } else { 2813 // find out how long it is 2814 Duration duration = new Duration(); 2815 String durationStr = values.getAsString(Events.DURATION); 2816 if (durationStr != null) { 2817 duration.parse(durationStr); 2818 } 2819 2820 RecurrenceSet recur = new RecurrenceSet(values); 2821 2822 if (recur.hasRecurrence()) { 2823 // the event is repeating, so find the last date it 2824 // could appear on 2825 2826 String tz = values.getAsString(Events.EVENT_TIMEZONE); 2827 2828 if (TextUtils.isEmpty(tz)) { 2829 // floating timezone 2830 tz = Time.TIMEZONE_UTC; 2831 } 2832 Time dtstartLocal = new Time(tz); 2833 2834 dtstartLocal.set(dtstartMillis); 2835 2836 RecurrenceProcessor rp = new RecurrenceProcessor(); 2837 lastMillis = rp.getLastOccurence(dtstartLocal, recur); 2838 if (lastMillis == -1) { 2839 return lastMillis; // -1 2840 } 2841 } else { 2842 // the event is not repeating, just use dtstartMillis 2843 lastMillis = dtstartMillis; 2844 } 2845 2846 // that was the beginning of the event. this is the end. 2847 lastMillis = duration.addTo(lastMillis); 2848 } 2849 return lastMillis; 2850 } 2851 updateContentValuesFromEvent(ContentValues initialValues)2852 private ContentValues updateContentValuesFromEvent(ContentValues initialValues) { 2853 try { 2854 ContentValues values = new ContentValues(initialValues); 2855 2856 long last = calculateLastDate(values); 2857 if (last != -1) { 2858 values.put(Events.LAST_DATE, last); 2859 } 2860 2861 return values; 2862 } catch (DateException e) { 2863 // don't add it if there was an error 2864 Log.w(TAG, "Could not calculate last date.", e); 2865 return null; 2866 } 2867 } 2868 updateEventRawTimesLocked(long eventId, ContentValues values)2869 private void updateEventRawTimesLocked(long eventId, ContentValues values) { 2870 ContentValues rawValues = new ContentValues(); 2871 2872 rawValues.put("event_id", eventId); 2873 2874 String timezone = values.getAsString(Events.EVENT_TIMEZONE); 2875 2876 boolean allDay = false; 2877 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2878 if (allDayInteger != null) { 2879 allDay = allDayInteger != 0; 2880 } 2881 2882 if (allDay || TextUtils.isEmpty(timezone)) { 2883 // floating timezone 2884 timezone = Time.TIMEZONE_UTC; 2885 } 2886 2887 Time time = new Time(timezone); 2888 time.allDay = allDay; 2889 Long dtstartMillis = values.getAsLong(Events.DTSTART); 2890 if (dtstartMillis != null) { 2891 time.set(dtstartMillis); 2892 rawValues.put("dtstart2445", time.format2445()); 2893 } 2894 2895 Long dtendMillis = values.getAsLong(Events.DTEND); 2896 if (dtendMillis != null) { 2897 time.set(dtendMillis); 2898 rawValues.put("dtend2445", time.format2445()); 2899 } 2900 2901 Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 2902 if (originalInstanceMillis != null) { 2903 // This is a recurrence exception so we need to get the all-day 2904 // status of the original recurring event in order to format the 2905 // date correctly. 2906 allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY); 2907 if (allDayInteger != null) { 2908 time.allDay = allDayInteger != 0; 2909 } 2910 time.set(originalInstanceMillis); 2911 rawValues.put("originalInstanceTime2445", time.format2445()); 2912 } 2913 2914 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 2915 if (lastDateMillis != null) { 2916 time.allDay = allDay; 2917 time.set(lastDateMillis); 2918 rawValues.put("lastDate2445", time.format2445()); 2919 } 2920 2921 mEventsRawTimesInserter.replace(rawValues); 2922 } 2923 2924 @Override deleteInternal(Uri url, String where, String[] whereArgs)2925 public int deleteInternal(Uri url, String where, String[] whereArgs) { 2926 final SQLiteDatabase db = getDatabase(); 2927 int match = sURLMatcher.match(url); 2928 switch (match) 2929 { 2930 case EVENTS_ID: 2931 { 2932 String id = url.getLastPathSegment(); 2933 if (where != null) { 2934 throw new UnsupportedOperationException("CalendarProvider " 2935 + "doesn't support where based deletion for type " 2936 + match); 2937 } 2938 if (!isTemporary()) { 2939 deleteBusyBitsLocked(Integer.parseInt(id)); 2940 2941 // Query this event to get the fields needed for inserting 2942 // a new row in the DeletedEvents table. 2943 Cursor cursor = db.query("Events", EVENTS_PROJECTION, 2944 "_id=" + id, null, null, null, null); 2945 try { 2946 if (cursor.moveToNext()) { 2947 String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX); 2948 if (!TextUtils.isEmpty(syncId)) { 2949 String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX); 2950 String syncAccountName = 2951 cursor.getString(EVENTS_SYNC_ACCOUNT_NAME_INDEX); 2952 String syncAccountType = 2953 cursor.getString(EVENTS_SYNC_ACCOUNT_TYPE_INDEX); 2954 Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX); 2955 2956 ContentValues values = new ContentValues(); 2957 values.put(Events._SYNC_ID, syncId); 2958 values.put(Events._SYNC_VERSION, syncVersion); 2959 values.put(Events._SYNC_ACCOUNT, syncAccountName); 2960 values.put(Events._SYNC_ACCOUNT_TYPE, syncAccountType); 2961 values.put(Events.CALENDAR_ID, calId); 2962 mDeletedEventsInserter.insert(values); 2963 2964 // TODO: we may also want to delete exception 2965 // events for this event (in case this was a 2966 // recurring event). We can do that with the 2967 // following code: 2968 // db.delete("Events", "originalEvent=?", new String[] {syncId}); 2969 } 2970 2971 // If this was a recurring event or a recurrence 2972 // exception, then force a recalculation of the 2973 // instances. 2974 String rrule = cursor.getString(EVENTS_RRULE_INDEX); 2975 String rdate = cursor.getString(EVENTS_RDATE_INDEX); 2976 String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX); 2977 if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate) 2978 || !TextUtils.isEmpty(origEvent)) { 2979 mMetaData.clearInstanceRange(); 2980 } 2981 } 2982 } finally { 2983 cursor.close(); 2984 cursor = null; 2985 } 2986 triggerAppWidgetUpdate(-1); 2987 } 2988 2989 // There is a delete trigger that will cause all instances 2990 // matching this event id to get deleted as well. In fact, all 2991 // of the following tables will remove entries matching this 2992 // event id: Instances, EventsRawTimes, Attendees, Reminders, 2993 // CalendarAlerts, and ExtendedProperties. 2994 int result = db.delete("Events", "_id=" + id, null); 2995 return result; 2996 } 2997 case ATTENDEES: 2998 { 2999 int result = db.delete("Attendees", where, whereArgs); 3000 return result; 3001 } 3002 case ATTENDEES_ID: 3003 { 3004 // we currently don't support deletions to the attendees list. 3005 // TODO: remove this restriction when we handle the full attendees 3006 // feed. we'll need to put in some logic to check that the 3007 // modification will be allowed by the server. 3008 throw new IllegalArgumentException("Cannot delete attendees."); 3009 // String id = url.getPathSegments().get(1); 3010 // int result = db.delete("Attendees", "_id="+id, null); 3011 // return result; 3012 } 3013 case REMINDERS: 3014 { 3015 int result = db.delete("Reminders", where, whereArgs); 3016 return result; 3017 } 3018 case REMINDERS_ID: 3019 { 3020 String id = url.getLastPathSegment(); 3021 int result = db.delete("Reminders", "_id="+id, null); 3022 return result; 3023 } 3024 case CALENDAR_ALERTS: 3025 { 3026 int result = db.delete("CalendarAlerts", where, whereArgs); 3027 return result; 3028 } 3029 case CALENDAR_ALERTS_ID: 3030 { 3031 String id = url.getLastPathSegment(); 3032 int result = db.delete("CalendarAlerts", "_id="+id, null); 3033 return result; 3034 } 3035 case DELETED_EVENTS: 3036 case EVENTS: 3037 throw new UnsupportedOperationException("Cannot delete that URL"); 3038 case CALENDARS_ID: 3039 StringBuilder whereSb = new StringBuilder("_id="); 3040 whereSb.append(url.getPathSegments().get(1)); 3041 if (!TextUtils.isEmpty(where)) { 3042 whereSb.append(" AND ("); 3043 whereSb.append(where); 3044 whereSb.append(')'); 3045 } 3046 where = whereSb.toString(); 3047 // fall through to CALENDARS for the actual delete 3048 case CALENDARS: 3049 return deleteMatchingCalendars(where); 3050 case INSTANCES: 3051 case INSTANCES_BY_DAY: 3052 throw new UnsupportedOperationException("Cannot delete that URL"); 3053 default: 3054 throw new IllegalArgumentException("Unknown URL " + url); 3055 } 3056 } 3057 deleteMatchingCalendars(String where)3058 private int deleteMatchingCalendars(String where) { 3059 // query to find all the calendars that match, for each 3060 // - delete calendar subscription 3061 // - delete calendar 3062 3063 int numDeleted = 0; 3064 final SQLiteDatabase db = getDatabase(); 3065 Cursor c = db.query("Calendars", sCalendarsIdProjection, where, null, 3066 null, null, null); 3067 if (c == null) { 3068 return 0; 3069 } 3070 try { 3071 while (c.moveToNext()) { 3072 long id = c.getLong(CALENDARS_INDEX_ID); 3073 if (!isTemporary()) { 3074 modifyCalendarSubscription(id, false /* not selected */); 3075 } 3076 c.deleteRow(); 3077 numDeleted++; 3078 } 3079 } finally { 3080 c.close(); 3081 } 3082 return numDeleted; 3083 } 3084 3085 // TODO: call calculateLastDate()! 3086 @Override updateInternal(Uri url, ContentValues values, String where, String[] selectionArgs)3087 public int updateInternal(Uri url, ContentValues values, 3088 String where, String[] selectionArgs) { 3089 int match = sURLMatcher.match(url); 3090 3091 // TODO: remove this restriction 3092 if (!TextUtils.isEmpty(where) && match != CALENDAR_ALERTS) { 3093 throw new IllegalArgumentException( 3094 "WHERE based updates not supported"); 3095 } 3096 final SQLiteDatabase db = getDatabase(); 3097 3098 switch (match) { 3099 case CALENDARS_ID: 3100 { 3101 long id = ContentUris.parseId(url); 3102 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS); 3103 if (syncEvents != null && !isTemporary()) { 3104 modifyCalendarSubscription(id, syncEvents == 1); 3105 } 3106 3107 int result = db.update("Calendars", values, "_id="+ id, null); 3108 if (!isTemporary()) { 3109 // When we change the display status of a Calendar 3110 // we need to update the busy bits. 3111 if (values.containsKey(Calendars.SELECTED) || (syncEvents != null)) { 3112 // Clear the BusyBits table. 3113 mMetaData.clearBusyBitRange(); 3114 } 3115 } 3116 3117 return result; 3118 } 3119 case EVENTS_ID: 3120 { 3121 long id = ContentUris.parseId(url); 3122 if (!isTemporary()) { 3123 values.put(Events._SYNC_DIRTY, 1); 3124 3125 // Disallow updating the attendee status in the Events 3126 // table. In the future, we could support this but we 3127 // would have to query and update the attendees table 3128 // to keep the values consistent. 3129 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) { 3130 throw new IllegalArgumentException("Updating " 3131 + Events.SELF_ATTENDEE_STATUS 3132 + " in Events table is not allowed."); 3133 } 3134 3135 if (values.containsKey(Events.HTML_URI)) { 3136 throw new IllegalArgumentException("Updating " 3137 + Events.HTML_URI 3138 + " in Events table is not allowed."); 3139 } 3140 3141 updateBusyBitsLocked(id, values); 3142 } 3143 3144 ContentValues updatedValues = updateContentValuesFromEvent(values); 3145 if (updatedValues == null) { 3146 Log.w(TAG, "Could not update event."); 3147 return 0; 3148 } 3149 3150 int result = db.update("Events", updatedValues, "_id="+id, null); 3151 if (!isTemporary()) { 3152 if (result > 0) { 3153 updateEventRawTimesLocked(id, updatedValues); 3154 updateInstancesLocked(updatedValues, id, false /* not a new event */, db); 3155 3156 if (values.containsKey(Events.DTSTART)) { 3157 // The start time of the event changed, so run the 3158 // event alarm scheduler. 3159 if (Log.isLoggable(TAG, Log.DEBUG)) { 3160 Log.d(TAG, "updateInternal() changing event"); 3161 } 3162 scheduleNextAlarm(false /* do not remove alarms */); 3163 triggerAppWidgetUpdate(id); 3164 } 3165 } 3166 } 3167 return result; 3168 } 3169 case ATTENDEES_ID: 3170 { 3171 // Copy the attendee status value to the Events table. 3172 updateEventAttendeeStatus(db, values); 3173 3174 long id = ContentUris.parseId(url); 3175 return db.update("Attendees", values, "_id="+id, null); 3176 } 3177 case CALENDAR_ALERTS_ID: 3178 { 3179 long id = ContentUris.parseId(url); 3180 return db.update("CalendarAlerts", values, "_id="+id, null); 3181 } 3182 case CALENDAR_ALERTS: 3183 { 3184 return db.update("CalendarAlerts", values, where, null); 3185 } 3186 case REMINDERS_ID: 3187 { 3188 long id = ContentUris.parseId(url); 3189 int result = db.update("Reminders", values, "_id="+id, null); 3190 if (!isTemporary()) { 3191 // Reschedule the event alarms because the 3192 // "minutes" field may have changed. 3193 if (Log.isLoggable(TAG, Log.DEBUG)) { 3194 Log.d(TAG, "updateInternal() changing reminder"); 3195 } 3196 scheduleNextAlarm(false /* do not remove alarms */); 3197 } 3198 return result; 3199 } 3200 case EXTENDED_PROPERTIES_ID: 3201 { 3202 long id = ContentUris.parseId(url); 3203 return db.update("ExtendedProperties", values, "_id="+id, null); 3204 } 3205 default: 3206 throw new IllegalArgumentException("Unknown URL " + url); 3207 } 3208 } 3209 3210 /** 3211 * Schedule a calendar sync for the account. 3212 * @param account the account for which to schedule a sync 3213 * @param uploadChangesOnly if set, specify that the sync should only send 3214 * up local changes 3215 * @param url the url feed for the calendar to sync (may be null) 3216 */ scheduleSync(Account account, boolean uploadChangesOnly, String url)3217 private void scheduleSync(Account account, boolean uploadChangesOnly, String url) { 3218 Bundle extras = new Bundle(); 3219 extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly); 3220 if (url != null) { 3221 extras.putString("feed", url); 3222 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 3223 } 3224 ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras); 3225 } 3226 modifyCalendarSubscription(long id, boolean syncEvents)3227 private void modifyCalendarSubscription(long id, boolean syncEvents) { 3228 // get the account, url, and current selected state 3229 // for this calendar. 3230 Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id), 3231 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE, 3232 Calendars.URL, Calendars.SYNC_EVENTS}, 3233 null /* selection */, 3234 null /* selectionArgs */, 3235 null /* sort */); 3236 3237 Account account = null; 3238 String calendarUrl = null; 3239 boolean oldSyncEvents = false; 3240 if (cursor != null && cursor.moveToFirst()) { 3241 try { 3242 final String accountName = cursor.getString(0); 3243 final String accountType = cursor.getString(1); 3244 account = new Account(accountName, accountType); 3245 calendarUrl = cursor.getString(2); 3246 oldSyncEvents = (cursor.getInt(3) != 0); 3247 } finally { 3248 cursor.close(); 3249 } 3250 } 3251 3252 if (account == null || TextUtils.isEmpty(calendarUrl)) { 3253 // should not happen? 3254 Log.w(TAG, "Cannot update subscription because account " 3255 + "or calendar url empty -- should not happen."); 3256 return; 3257 } 3258 3259 if (oldSyncEvents == syncEvents) { 3260 // nothing to do 3261 return; 3262 } 3263 3264 // If we are no longer syncing a calendar then make sure that the 3265 // old calendar sync data is cleared. Then if we later add this 3266 // calendar back, we will sync all the events. 3267 if (!syncEvents) { 3268 byte[] data = readSyncDataBytes(account); 3269 GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data); 3270 if (syncData != null) { 3271 syncData.feedData.remove(calendarUrl); 3272 data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData); 3273 writeSyncDataBytes(account, data); 3274 } 3275 3276 // Delete all of the events in this calendar to save space. 3277 // This is the closest we can come to deleting a calendar. 3278 // Clients should never actually delete a calendar. That won't 3279 // work. We need to keep the calendar entry in the Calendars table 3280 // in order to know not to sync the events for that calendar from 3281 // the server. 3282 final SQLiteDatabase db = getDatabase(); 3283 String[] args = new String[] {Long.toString(id)}; 3284 db.delete("Events", CALENDAR_ID_SELECTION, args); 3285 // Note that we do not delete the matching entries 3286 // in the DeletedEvents table. We will let those 3287 // deleted events propagate to the server. 3288 3289 // TODO: cancel any pending/ongoing syncs for this calendar. 3290 3291 // TODO: there is a corner case to deal with here: namely, if 3292 // we edit or delete an event on the phone and then remove 3293 // (that is, stop syncing) a calendar, and if we also make a 3294 // change on the server to that event at about the same time, 3295 // then we will never propagate the changes from the phone to 3296 // the server. 3297 } 3298 3299 // If the calendar is not selected for syncing, then don't download 3300 // events. 3301 scheduleSync(account, !syncEvents, calendarUrl); 3302 } 3303 3304 @Override onSyncStop(SyncContext context, boolean success)3305 public void onSyncStop(SyncContext context, boolean success) { 3306 super.onSyncStop(context, success); 3307 if (Log.isLoggable(TAG, Log.DEBUG)) { 3308 Log.d(TAG, "onSyncStop() success: " + success); 3309 } 3310 scheduleNextAlarm(false /* do not remove alarms */); 3311 triggerAppWidgetUpdate(-1); 3312 } 3313 3314 @Override getMergers()3315 protected Iterable<EventMerger> getMergers() { 3316 return Collections.singletonList(new EventMerger()); 3317 } 3318 3319 /** 3320 * Update any existing widgets with the changed events. 3321 * 3322 * @param changedEventId Specific event known to be changed, otherwise -1. 3323 * If present, we use it to decide if an update is necessary. 3324 */ triggerAppWidgetUpdate(long changedEventId)3325 private synchronized void triggerAppWidgetUpdate(long changedEventId) { 3326 Context context = getContext(); 3327 if (context != null) { 3328 mAppWidgetProvider.providerUpdated(context, changedEventId); 3329 } 3330 } 3331 bootCompleted()3332 void bootCompleted() { 3333 // Remove alarms from the CalendarAlerts table that have been marked 3334 // as "scheduled" but not fired yet. We do this because the 3335 // AlarmManagerService loses all information about alarms when the 3336 // power turns off but we store the information in a database table 3337 // that persists across reboots. See the documentation for 3338 // scheduleNextAlarmLocked() for more information. 3339 scheduleNextAlarm(true /* remove alarms */); 3340 } 3341 3342 /* Retrieve and cache the alarm manager */ getAlarmManager()3343 private AlarmManager getAlarmManager() { 3344 synchronized(mAlarmLock) { 3345 if (mAlarmManager == null) { 3346 Context context = getContext(); 3347 if (context == null) { 3348 Log.e(TAG, "getAlarmManager() cannot get Context"); 3349 return null; 3350 } 3351 Object service = context.getSystemService(Context.ALARM_SERVICE); 3352 mAlarmManager = (AlarmManager) service; 3353 } 3354 return mAlarmManager; 3355 } 3356 } 3357 scheduleNextAlarmCheck(long triggerTime)3358 void scheduleNextAlarmCheck(long triggerTime) { 3359 AlarmManager manager = getAlarmManager(); 3360 if (manager == null) { 3361 Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager"); 3362 return; 3363 } 3364 Context context = getContext(); 3365 Intent intent = new Intent(CalendarReceiver.SCHEDULE); 3366 intent.setClass(context, CalendarReceiver.class); 3367 PendingIntent pending = PendingIntent.getBroadcast(context, 3368 0, intent, PendingIntent.FLAG_NO_CREATE); 3369 if (pending != null) { 3370 // Cancel any previous alarms that do the same thing. 3371 manager.cancel(pending); 3372 } 3373 pending = PendingIntent.getBroadcast(context, 3374 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 3375 3376 if (Log.isLoggable(TAG, Log.DEBUG)) { 3377 Time time = new Time(); 3378 time.set(triggerTime); 3379 String timeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3380 Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr); 3381 } 3382 3383 manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending); 3384 } 3385 3386 /* 3387 * This method runs the alarm scheduler in a background thread. 3388 */ scheduleNextAlarm(boolean removeAlarms)3389 void scheduleNextAlarm(boolean removeAlarms) { 3390 Thread thread = new AlarmScheduler(removeAlarms); 3391 thread.start(); 3392 } 3393 3394 /** 3395 * This method runs in a background thread and schedules an alarm for 3396 * the next calendar event, if necessary. 3397 */ runScheduleNextAlarm(boolean removeAlarms)3398 private void runScheduleNextAlarm(boolean removeAlarms) { 3399 // Do not schedule any alarms if this is a temporary database. 3400 if (isTemporary()) { 3401 return; 3402 } 3403 3404 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 3405 db.beginTransaction(); 3406 try { 3407 if (removeAlarms) { 3408 removeScheduledAlarmsLocked(db); 3409 } 3410 scheduleNextAlarmLocked(db); 3411 db.setTransactionSuccessful(); 3412 } finally { 3413 db.endTransaction(); 3414 } 3415 } 3416 3417 /** 3418 * This method looks at the 24-hour window from now for any events that it 3419 * needs to schedule. This method runs within a database transaction. It 3420 * also runs in a background thread. 3421 * 3422 * The CalendarProvider keeps track of which alarms it has already scheduled 3423 * to avoid scheduling them more than once and for debugging problems with 3424 * alarms. It stores this knowledge in a database table called CalendarAlerts 3425 * which persists across reboots. But the actual alarm list is in memory 3426 * and disappears if the phone loses power. To avoid missing an alarm, we 3427 * clear the entries in the CalendarAlerts table when we start up the 3428 * CalendarProvider. 3429 * 3430 * Scheduling an alarm multiple times is not tragic -- we filter out the 3431 * extra ones when we receive them. But we still need to keep track of the 3432 * scheduled alarms. The main reason is that we need to prevent multiple 3433 * notifications for the same alarm (on the receive side) in case we 3434 * accidentally schedule the same alarm multiple times. We don't have 3435 * visibility into the system's alarm list so we can never know for sure if 3436 * we have already scheduled an alarm and it's better to err on scheduling 3437 * an alarm twice rather than missing an alarm. Another reason we keep 3438 * track of scheduled alarms in a database table is that it makes it easy to 3439 * run an SQL query to find the next reminder that we haven't scheduled. 3440 * 3441 * @param db the database 3442 */ scheduleNextAlarmLocked(SQLiteDatabase db)3443 private void scheduleNextAlarmLocked(SQLiteDatabase db) { 3444 AlarmManager alarmManager = getAlarmManager(); 3445 if (alarmManager == null) { 3446 Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!"); 3447 return; 3448 } 3449 3450 final long currentMillis = System.currentTimeMillis(); 3451 final long start = currentMillis - SCHEDULE_ALARM_SLACK; 3452 final long end = start + (24 * 60 * 60 * 1000); 3453 ContentResolver cr = getContext().getContentResolver(); 3454 if (Log.isLoggable(TAG, Log.DEBUG)) { 3455 Time time = new Time(); 3456 time.set(start); 3457 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3458 Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr); 3459 } 3460 3461 // Clear old alarms but keep alarms around for a while to prevent 3462 // multiple alerts for the same reminder. The "clearUpToTime' 3463 // should be further in the past than the point in time where 3464 // we start searching for events (the "start" variable defined above). 3465 long clearUpToTime = currentMillis - CLEAR_OLD_ALARM_THRESHOLD; 3466 db.delete("CalendarAlerts", CalendarAlerts.ALARM_TIME + "<" + clearUpToTime, null); 3467 3468 long nextAlarmTime = end; 3469 long alarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis); 3470 if (alarmTime != -1 && alarmTime < nextAlarmTime) { 3471 nextAlarmTime = alarmTime; 3472 } 3473 3474 // Extract events from the database sorted by alarm time. The 3475 // alarm times are computed from Instances.begin (whose units 3476 // are milliseconds) and Reminders.minutes (whose units are 3477 // minutes). 3478 // 3479 // Also, ignore events whose end time is already in the past. 3480 // Also, ignore events alarms that we have already scheduled. 3481 // 3482 // Note 1: we can add support for the case where Reminders.minutes 3483 // equals -1 to mean use Calendars.minutes by adding a UNION for 3484 // that case where the two halves restrict the WHERE clause on 3485 // Reminders.minutes != -1 and Reminders.minutes = 1, respectively. 3486 // 3487 // Note 2: we have to name "myAlarmTime" different from the 3488 // "alarmTime" column in CalendarAlerts because otherwise the 3489 // query won't find multiple alarms for the same event. 3490 String query = "SELECT begin-(minutes*60000) AS myAlarmTime," 3491 + " Instances.event_id AS eventId, begin, end," 3492 + " title, allDay, method, minutes" 3493 + " FROM Instances INNER JOIN Events" 3494 + " ON (Events._id = Instances.event_id)" 3495 + " INNER JOIN Reminders" 3496 + " ON (Instances.event_id = Reminders.event_id)" 3497 + " WHERE method=" + Reminders.METHOD_ALERT 3498 + " AND myAlarmTime>=" + start 3499 + " AND myAlarmTime<=" + nextAlarmTime 3500 + " AND end>=" + currentMillis 3501 + " AND 0=(SELECT count(*) from CalendarAlerts CA" 3502 + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin" 3503 + " AND CA.alarmTime=myAlarmTime)" 3504 + " ORDER BY myAlarmTime,begin,title"; 3505 3506 acquireInstanceRangeLocked(start, end, false /* don't use minimum expansion windows */); 3507 Cursor cursor = null; 3508 try { 3509 cursor = db.rawQuery(query, null); 3510 3511 int beginIndex = cursor.getColumnIndex(Instances.BEGIN); 3512 int endIndex = cursor.getColumnIndex(Instances.END); 3513 int eventIdIndex = cursor.getColumnIndex("eventId"); 3514 int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime"); 3515 int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES); 3516 3517 if (Log.isLoggable(TAG, Log.DEBUG)) { 3518 Time time = new Time(); 3519 time.set(nextAlarmTime); 3520 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3521 Log.d(TAG, "nextAlarmTime: " + alarmTimeStr 3522 + " cursor results: " + cursor.getCount() 3523 + " query: " + query); 3524 } 3525 3526 while (cursor.moveToNext()) { 3527 // Schedule all alarms whose alarm time is as early as any 3528 // scheduled alarm. For example, if the earliest alarm is at 3529 // 1pm, then we will schedule all alarms that occur at 1pm 3530 // but no alarms that occur later than 1pm. 3531 // Actually, we allow alarms up to a minute later to also 3532 // be scheduled so that we don't have to check immediately 3533 // again after an event alarm goes off. 3534 alarmTime = cursor.getLong(alarmTimeIndex); 3535 long eventId = cursor.getLong(eventIdIndex); 3536 int minutes = cursor.getInt(minutesIndex); 3537 long startTime = cursor.getLong(beginIndex); 3538 3539 if (Log.isLoggable(TAG, Log.DEBUG)) { 3540 int titleIndex = cursor.getColumnIndex(Events.TITLE); 3541 String title = cursor.getString(titleIndex); 3542 Time time = new Time(); 3543 time.set(alarmTime); 3544 String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); 3545 time.set(startTime); 3546 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3547 long endTime = cursor.getLong(endIndex); 3548 time.set(endTime); 3549 String endTimeStr = time.format(" - %a, %b %d, %Y %I:%M%P"); 3550 time.set(currentMillis); 3551 String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3552 Log.d(TAG, " looking at id: " + eventId + " " + title 3553 + " " + startTime 3554 + startTimeStr + endTimeStr + " alarm: " 3555 + alarmTime + schedTime 3556 + " currentTime: " + currentTimeStr); 3557 } 3558 3559 if (alarmTime < nextAlarmTime) { 3560 nextAlarmTime = alarmTime; 3561 } else if (alarmTime > nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS) { 3562 // This event alarm (and all later ones) will be scheduled 3563 // later. 3564 break; 3565 } 3566 3567 // Avoid an SQLiteContraintException by checking if this alarm 3568 // already exists in the table. 3569 if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) { 3570 if (Log.isLoggable(TAG, Log.DEBUG)) { 3571 int titleIndex = cursor.getColumnIndex(Events.TITLE); 3572 String title = cursor.getString(titleIndex); 3573 Log.d(TAG, " alarm exists for id: " + eventId + " " + title); 3574 } 3575 continue; 3576 } 3577 3578 // Insert this alarm into the CalendarAlerts table 3579 long endTime = cursor.getLong(endIndex); 3580 Uri uri = CalendarAlerts.insert(cr, eventId, startTime, 3581 endTime, alarmTime, minutes); 3582 if (uri == null) { 3583 Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed"); 3584 continue; 3585 } 3586 3587 Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION); 3588 intent.setData(uri); 3589 3590 // Also include the begin and end time of this event, because 3591 // we cannot determine that from the Events database table. 3592 intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, startTime); 3593 intent.putExtra(android.provider.Calendar.EVENT_END_TIME, endTime); 3594 if (Log.isLoggable(TAG, Log.DEBUG)) { 3595 int titleIndex = cursor.getColumnIndex(Events.TITLE); 3596 String title = cursor.getString(titleIndex); 3597 Time time = new Time(); 3598 time.set(alarmTime); 3599 String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); 3600 time.set(startTime); 3601 String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3602 time.set(endTime); 3603 String endTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3604 time.set(currentMillis); 3605 String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P"); 3606 Log.d(TAG, " scheduling " + title 3607 + startTimeStr + " - " + endTimeStr + " alarm: " + schedTime 3608 + " currentTime: " + currentTimeStr 3609 + " uri: " + uri); 3610 } 3611 PendingIntent sender = PendingIntent.getBroadcast(getContext(), 3612 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 3613 alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender); 3614 } 3615 } finally { 3616 if (cursor != null) { 3617 cursor.close(); 3618 } 3619 } 3620 3621 // If we scheduled an event alarm, then schedule the next alarm check 3622 // for one minute past that alarm. Otherwise, if there were no 3623 // event alarms scheduled, then check again in 24 hours. If a new 3624 // event is inserted before the next alarm check, then this method 3625 // will be run again when the new event is inserted. 3626 if (nextAlarmTime != Long.MAX_VALUE) { 3627 scheduleNextAlarmCheck(nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS); 3628 } else { 3629 scheduleNextAlarmCheck(currentMillis + android.text.format.DateUtils.DAY_IN_MILLIS); 3630 } 3631 } 3632 3633 /** 3634 * Removes the entries in the CalendarAlerts table for alarms that we have 3635 * scheduled but that have not fired yet. We do this to ensure that we 3636 * don't miss an alarm. The CalendarAlerts table keeps track of the 3637 * alarms that we have scheduled but the actual alarm list is in memory 3638 * and will be cleared if the phone reboots. 3639 * 3640 * We don't need to remove entries that have already fired, and in fact 3641 * we should not remove them because we need to display the notifications 3642 * until the user dismisses them. 3643 * 3644 * We could remove entries that have fired and been dismissed, but we leave 3645 * them around for a while because it makes it easier to debug problems. 3646 * Entries that are old enough will be cleaned up later when we schedule 3647 * new alarms. 3648 */ removeScheduledAlarmsLocked(SQLiteDatabase db)3649 private void removeScheduledAlarmsLocked(SQLiteDatabase db) { 3650 if (Log.isLoggable(TAG, Log.DEBUG)) { 3651 Log.d(TAG, "removing scheduled alarms"); 3652 } 3653 db.delete(CalendarAlerts.TABLE_NAME, 3654 CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */); 3655 } 3656 3657 private static String sEventsTable = "Events"; 3658 private static String sDeletedEventsTable = "DeletedEvents"; 3659 private static String sAttendeesTable = "Attendees"; 3660 private static String sRemindersTable = "Reminders"; 3661 private static String sCalendarAlertsTable = "CalendarAlerts"; 3662 private static String sExtendedPropertiesTable = "ExtendedProperties"; 3663 3664 private class EventMerger extends AbstractTableMerger { 3665 3666 private ContentValues mValues = new ContentValues(); EventMerger()3667 EventMerger() { 3668 super(getDatabase(), sEventsTable, Calendar.Events.CONTENT_URI, 3669 sDeletedEventsTable, Calendar.Events.DELETED_CONTENT_URI); 3670 } 3671 3672 @Override notifyChanges()3673 protected void notifyChanges() { 3674 getContext().getContentResolver().notifyChange(Events.CONTENT_URI, 3675 null /* observer */, false /* do not sync to network */); 3676 } 3677 3678 @Override cursorRowToContentValues(Cursor cursor, ContentValues map)3679 protected void cursorRowToContentValues(Cursor cursor, ContentValues map) { 3680 rowToContentValues(cursor, map); 3681 } 3682 3683 @Override insertRow(ContentProvider diffs, Cursor diffsCursor)3684 public void insertRow(ContentProvider diffs, Cursor diffsCursor) { 3685 rowToContentValues(diffsCursor, mValues); 3686 final SQLiteDatabase db = getDatabase(); 3687 long rowId = mEventsInserter.insert(mValues); 3688 if (rowId <= 0) { 3689 Log.e(TAG, "Unable to insert values into calendar db: " + mValues); 3690 return; 3691 } 3692 3693 long diffsRowId = diffsCursor.getLong( 3694 diffsCursor.getColumnIndex(Events._ID)); 3695 3696 insertAttendees(diffs, diffsRowId, rowId, db); 3697 insertRemindersIfNecessary(diffs, diffsRowId, rowId, db); 3698 insertExtendedPropertiesIfNecessary(diffs, diffsRowId, rowId, db); 3699 updateEventRawTimesLocked(rowId, mValues); 3700 updateInstancesLocked(mValues, rowId, true /* new event */, db); 3701 insertBusyBitsLocked(rowId, mValues); 3702 3703 // Update the _SYNC_DIRTY flag of the event. We have to do this 3704 // after inserting since the update of the reminders and extended properties 3705 // methods will fire a sql trigger that will cause this flag to 3706 // be set. 3707 clearSyncDirtyFlag(db, rowId); 3708 } 3709 clearSyncDirtyFlag(SQLiteDatabase db, long rowId)3710 private void clearSyncDirtyFlag(SQLiteDatabase db, long rowId) { 3711 mValues.clear(); 3712 mValues.put(Events._SYNC_DIRTY, 0); 3713 db.update(mTable, mValues, Events._ID + '=' + rowId, null); 3714 } 3715 insertAttendees(ContentProvider diffs, long diffsRowId, long rowId, SQLiteDatabase db)3716 private void insertAttendees(ContentProvider diffs, 3717 long diffsRowId, 3718 long rowId, 3719 SQLiteDatabase db) { 3720 // query attendees in diffs 3721 Cursor attendeesCursor = 3722 diffs.query(Attendees.CONTENT_URI, null, 3723 "event_id=" + diffsRowId, null, null); 3724 ContentValues attendeesValues = new ContentValues(); 3725 try { 3726 while (attendeesCursor.moveToNext()) { 3727 attendeesValues.clear(); 3728 DatabaseUtils.cursorStringToContentValues(attendeesCursor, 3729 Attendees.ATTENDEE_NAME, 3730 attendeesValues); 3731 DatabaseUtils.cursorStringToContentValues(attendeesCursor, 3732 Attendees.ATTENDEE_EMAIL, 3733 attendeesValues); 3734 DatabaseUtils.cursorIntToContentValues(attendeesCursor, 3735 Attendees.ATTENDEE_STATUS, 3736 attendeesValues); 3737 DatabaseUtils.cursorIntToContentValues(attendeesCursor, 3738 Attendees.ATTENDEE_TYPE, 3739 attendeesValues); 3740 DatabaseUtils.cursorIntToContentValues(attendeesCursor, 3741 Attendees.ATTENDEE_RELATIONSHIP, 3742 attendeesValues); 3743 attendeesValues.put(Attendees.EVENT_ID, rowId); 3744 mAttendeesInserter.insert(attendeesValues); 3745 } 3746 } finally { 3747 if (attendeesCursor != null) { 3748 attendeesCursor.close(); 3749 } 3750 } 3751 } 3752 insertRemindersIfNecessary(ContentProvider diffs, long diffsRowId, long rowId, SQLiteDatabase db)3753 private void insertRemindersIfNecessary(ContentProvider diffs, 3754 long diffsRowId, 3755 long rowId, 3756 SQLiteDatabase db) { 3757 // insert reminders, if necessary. 3758 Integer hasAlarm = mValues.getAsInteger(Events.HAS_ALARM); 3759 if (hasAlarm != null && hasAlarm.intValue() == 1) { 3760 // query reminders in diffs 3761 Cursor reminderCursor = 3762 diffs.query(Reminders.CONTENT_URI, null, 3763 "event_id=" + diffsRowId, null, null); 3764 ContentValues reminderValues = new ContentValues(); 3765 try { 3766 while (reminderCursor.moveToNext()) { 3767 reminderValues.clear(); 3768 DatabaseUtils.cursorIntToContentValues(reminderCursor, 3769 Reminders.METHOD, 3770 reminderValues); 3771 DatabaseUtils.cursorIntToContentValues(reminderCursor, 3772 Reminders.MINUTES, 3773 reminderValues); 3774 reminderValues.put(Reminders.EVENT_ID, rowId); 3775 mRemindersInserter.insert(reminderValues); 3776 } 3777 } finally { 3778 if (reminderCursor != null) { 3779 reminderCursor.close(); 3780 } 3781 } 3782 } 3783 } 3784 insertExtendedPropertiesIfNecessary(ContentProvider diffs, long diffsRowId, long rowId, SQLiteDatabase db)3785 private void insertExtendedPropertiesIfNecessary(ContentProvider diffs, 3786 long diffsRowId, 3787 long rowId, 3788 SQLiteDatabase db) { 3789 // insert extended properties, if necessary. 3790 Integer hasExtendedProperties = mValues.getAsInteger(Events.HAS_EXTENDED_PROPERTIES); 3791 if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) { 3792 // query reminders in diffs 3793 Cursor extendedPropertiesCursor = 3794 diffs.query(Calendar.ExtendedProperties.CONTENT_URI, null, 3795 "event_id=" + diffsRowId, null, null); 3796 ContentValues extendedPropertiesValues = new ContentValues(); 3797 try { 3798 while (extendedPropertiesCursor.moveToNext()) { 3799 extendedPropertiesValues.clear(); 3800 DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor, 3801 Calendar.ExtendedProperties.NAME, extendedPropertiesValues); 3802 DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor, 3803 Calendar.ExtendedProperties.VALUE, extendedPropertiesValues); 3804 extendedPropertiesValues.put(ExtendedProperties.EVENT_ID, rowId); 3805 mExtendedPropertiesInserter.insert(extendedPropertiesValues); 3806 } 3807 } finally { 3808 if (extendedPropertiesCursor != null) { 3809 extendedPropertiesCursor.close(); 3810 } 3811 } 3812 } 3813 } 3814 3815 @Override updateRow(long localId, ContentProvider diffs, Cursor diffsCursor)3816 public void updateRow(long localId, ContentProvider diffs, 3817 Cursor diffsCursor) { 3818 rowToContentValues(diffsCursor, mValues); 3819 final SQLiteDatabase db = getDatabase(); 3820 updateBusyBitsLocked(localId, mValues); 3821 int numRows = db.update(mTable, mValues, "_id=" + localId, null /* selectionArgs */); 3822 3823 if (numRows <= 0) { 3824 Log.e(TAG, "Unable to update calendar db: " + mValues); 3825 return; 3826 } 3827 3828 long diffsRowId = diffsCursor.getLong( 3829 diffsCursor.getColumnIndex(Events._ID)); 3830 // TODO: only update the attendees, reminders, and extended properties if they have 3831 // changed? 3832 // delete the existing attendees, reminders, and extended properties 3833 db.delete(sAttendeesTable, "event_id=" + localId, null /* selectionArgs */); 3834 db.delete(sRemindersTable, "event_id=" + localId, null /* selectionArgs */); 3835 db.delete(sExtendedPropertiesTable, "event_id=" + localId, 3836 null /* selectionArgs */); 3837 3838 // process attendees sent by the server. 3839 insertAttendees(diffs, diffsRowId, localId, db); 3840 // process reminders sent by the server. 3841 insertRemindersIfNecessary(diffs, diffsRowId, localId, db); 3842 3843 // process extended properties sent by the server. 3844 insertExtendedPropertiesIfNecessary(diffs, diffsRowId, localId, db); 3845 3846 updateEventRawTimesLocked(localId, mValues); 3847 updateInstancesLocked(mValues, localId, false /* not a new event */, db); 3848 3849 // Update the _SYNC_DIRTY flag of the event. We have to do this 3850 // after updating since the update of the reminders and extended properties 3851 // methods will fire a sql trigger that will cause this flag to 3852 // be set. 3853 clearSyncDirtyFlag(db, localId); 3854 } 3855 3856 @Override resolveRow(long localId, String syncId, ContentProvider diffs, Cursor diffsCursor)3857 public void resolveRow(long localId, String syncId, 3858 ContentProvider diffs, Cursor diffsCursor) { 3859 // server wins 3860 updateRow(localId, diffs, diffsCursor); 3861 } 3862 3863 @Override deleteRow(Cursor localCursor)3864 public void deleteRow(Cursor localCursor) { 3865 long localId = localCursor.getLong(localCursor.getColumnIndexOrThrow(Events._ID)); 3866 deleteBusyBitsLocked(localId); 3867 3868 // we have to read this row from the DB since the projection that is used 3869 // by cursor doesn't necessarily contain the columns we need 3870 Cursor c = getDatabase().query(sEventsTable, 3871 new String[]{Events.RRULE, Events.RDATE, Events.ORIGINAL_EVENT}, 3872 "_id=" + localId, null, null, null, null); 3873 try { 3874 c.moveToNext(); 3875 // If this was a recurring event or a recurrence exception, then 3876 // force a recalculation of the instances. 3877 // We can get a tombstoned recurrence exception 3878 // that doesn't have a rrule, rdate, or originalEvent, and the 3879 // check below wouldn't catch that. However, in practice we also 3880 // get a different event with a rrule in that case, so the 3881 // instances get cleared by that rule. 3882 // This should be re-evaluated when calendar supports gd:deleted. 3883 String rrule = c.getString(c.getColumnIndexOrThrow(Events.RRULE)); 3884 String rdate = c.getString(c.getColumnIndexOrThrow(Events.RDATE)); 3885 String origEvent = c.getString(c.getColumnIndexOrThrow(Events.ORIGINAL_EVENT)); 3886 if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate) 3887 || !TextUtils.isEmpty(origEvent)) { 3888 mMetaData.clearInstanceRange(); 3889 } 3890 } finally { 3891 c.close(); 3892 } 3893 super.deleteRow(localCursor); 3894 } 3895 rowToContentValues(Cursor diffsCursor, ContentValues values)3896 private void rowToContentValues(Cursor diffsCursor, ContentValues values) { 3897 values.clear(); 3898 3899 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ID, values); 3900 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_TIME, values); 3901 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_VERSION, values); 3902 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_DIRTY, values); 3903 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ACCOUNT, values); 3904 DatabaseUtils.cursorStringToContentValues(diffsCursor, 3905 Events._SYNC_ACCOUNT_TYPE, values); 3906 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.HTML_URI, values); 3907 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.TITLE, values); 3908 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_LOCATION, values); 3909 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DESCRIPTION, values); 3910 DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.STATUS, values); 3911 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.SELF_ATTENDEE_STATUS, 3912 values); 3913 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.COMMENTS_URI, values); 3914 DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTSTART, values); 3915 DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTEND, values); 3916 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_TIMEZONE, values); 3917 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DURATION, values); 3918 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.ALL_DAY, values); 3919 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.VISIBILITY, values); 3920 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.TRANSPARENCY, values); 3921 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ALARM, values); 3922 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_EXTENDED_PROPERTIES, 3923 values); 3924 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.RRULE, values); 3925 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_EVENT, values); 3926 DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.ORIGINAL_INSTANCE_TIME, 3927 values); 3928 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_ALL_DAY, 3929 values); 3930 DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.LAST_DATE, values); 3931 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ATTENDEE_DATA, values); 3932 DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.CALENDAR_ID, values); 3933 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_INVITE_OTHERS, 3934 values); 3935 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_MODIFY, values); 3936 DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_SEE_GUESTS, 3937 values); 3938 DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORGANIZER, values); 3939 } 3940 } 3941 3942 private static final int EVENTS = 1; 3943 private static final int EVENTS_ID = 2; 3944 private static final int INSTANCES = 3; 3945 private static final int DELETED_EVENTS = 4; 3946 private static final int CALENDARS = 5; 3947 private static final int CALENDARS_ID = 6; 3948 private static final int ATTENDEES = 7; 3949 private static final int ATTENDEES_ID = 8; 3950 private static final int REMINDERS = 9; 3951 private static final int REMINDERS_ID = 10; 3952 private static final int EXTENDED_PROPERTIES = 11; 3953 private static final int EXTENDED_PROPERTIES_ID = 12; 3954 private static final int CALENDAR_ALERTS = 13; 3955 private static final int CALENDAR_ALERTS_ID = 14; 3956 private static final int CALENDAR_ALERTS_BY_INSTANCE = 15; 3957 private static final int BUSYBITS = 16; 3958 private static final int INSTANCES_BY_DAY = 17; 3959 3960 private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH); 3961 private static final HashMap<String, String> sInstancesProjectionMap; 3962 private static final HashMap<String, String> sEventsProjectionMap; 3963 private static final HashMap<String, String> sAttendeesProjectionMap; 3964 private static final HashMap<String, String> sRemindersProjectionMap; 3965 private static final HashMap<String, String> sCalendarAlertsProjectionMap; 3966 private static final HashMap<String, String> sBusyBitsProjectionMap; 3967 3968 static { 3969 sURLMatcher.addURI("calendar", "instances/when/*/*", INSTANCES); 3970 sURLMatcher.addURI("calendar", "instances/whenbyday/*/*", INSTANCES_BY_DAY); 3971 sURLMatcher.addURI("calendar", "events", EVENTS); 3972 sURLMatcher.addURI("calendar", "events/#", EVENTS_ID); 3973 sURLMatcher.addURI("calendar", "calendars", CALENDARS); 3974 sURLMatcher.addURI("calendar", "calendars/#", CALENDARS_ID); 3975 sURLMatcher.addURI("calendar", "deleted_events", DELETED_EVENTS); 3976 sURLMatcher.addURI("calendar", "attendees", ATTENDEES); 3977 sURLMatcher.addURI("calendar", "attendees/#", ATTENDEES_ID); 3978 sURLMatcher.addURI("calendar", "reminders", REMINDERS); 3979 sURLMatcher.addURI("calendar", "reminders/#", REMINDERS_ID); 3980 sURLMatcher.addURI("calendar", "extendedproperties", EXTENDED_PROPERTIES); 3981 sURLMatcher.addURI("calendar", "extendedproperties/#", EXTENDED_PROPERTIES_ID); 3982 sURLMatcher.addURI("calendar", "calendar_alerts", CALENDAR_ALERTS); 3983 sURLMatcher.addURI("calendar", "calendar_alerts/#", CALENDAR_ALERTS_ID); 3984 sURLMatcher.addURI("calendar", "calendar_alerts/by_instance", CALENDAR_ALERTS_BY_INSTANCE); 3985 sURLMatcher.addURI("calendar", "busybits/when/*/*", BUSYBITS); 3986 3987 3988 sEventsProjectionMap = new HashMap<String, String>(); 3989 // Events columns sEventsProjectionMap.put(Events.HTML_URI, "htmlUri")3990 sEventsProjectionMap.put(Events.HTML_URI, "htmlUri"); sEventsProjectionMap.put(Events.TITLE, "title")3991 sEventsProjectionMap.put(Events.TITLE, "title"); sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation")3992 sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation"); sEventsProjectionMap.put(Events.DESCRIPTION, "description")3993 sEventsProjectionMap.put(Events.DESCRIPTION, "description"); sEventsProjectionMap.put(Events.STATUS, "eventStatus")3994 sEventsProjectionMap.put(Events.STATUS, "eventStatus"); sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus")3995 sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus"); sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri")3996 sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri"); sEventsProjectionMap.put(Events.DTSTART, "dtstart")3997 sEventsProjectionMap.put(Events.DTSTART, "dtstart"); sEventsProjectionMap.put(Events.DTEND, "dtend")3998 sEventsProjectionMap.put(Events.DTEND, "dtend"); sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone")3999 sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone"); sEventsProjectionMap.put(Events.DURATION, "duration")4000 sEventsProjectionMap.put(Events.DURATION, "duration"); sEventsProjectionMap.put(Events.ALL_DAY, "allDay")4001 sEventsProjectionMap.put(Events.ALL_DAY, "allDay"); sEventsProjectionMap.put(Events.VISIBILITY, "visibility")4002 sEventsProjectionMap.put(Events.VISIBILITY, "visibility"); sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency")4003 sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency"); sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm")4004 sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm"); sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties")4005 sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties"); sEventsProjectionMap.put(Events.RRULE, "rrule")4006 sEventsProjectionMap.put(Events.RRULE, "rrule"); sEventsProjectionMap.put(Events.RDATE, "rdate")4007 sEventsProjectionMap.put(Events.RDATE, "rdate"); sEventsProjectionMap.put(Events.EXRULE, "exrule")4008 sEventsProjectionMap.put(Events.EXRULE, "exrule"); sEventsProjectionMap.put(Events.EXDATE, "exdate")4009 sEventsProjectionMap.put(Events.EXDATE, "exdate"); sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent")4010 sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent"); sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime")4011 sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime"); sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay")4012 sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay"); sEventsProjectionMap.put(Events.LAST_DATE, "lastDate")4013 sEventsProjectionMap.put(Events.LAST_DATE, "lastDate"); sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData")4014 sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData"); sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id")4015 sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id"); sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers")4016 sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers"); sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify")4017 sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify"); sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests")4018 sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests"); sEventsProjectionMap.put(Events.ORGANIZER, "organizer")4019 sEventsProjectionMap.put(Events.ORGANIZER, "organizer"); 4020 4021 // Calendar columns sEventsProjectionMap.put(Events.COLOR, "color")4022 sEventsProjectionMap.put(Events.COLOR, "color"); sEventsProjectionMap.put(Events.ACCESS_LEVEL, "access_level")4023 sEventsProjectionMap.put(Events.ACCESS_LEVEL, "access_level"); sEventsProjectionMap.put(Events.SELECTED, "selected")4024 sEventsProjectionMap.put(Events.SELECTED, "selected"); sEventsProjectionMap.put(Calendars.URL, "url")4025 sEventsProjectionMap.put(Calendars.URL, "url"); sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone")4026 sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone"); sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount")4027 sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount"); 4028 4029 // Put the shared items into the Instances projection map 4030 sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 4031 sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 4032 sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 4033 sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 4034 sEventsProjectionMap.put(Events._ID, "Events._id AS _id")4035 sEventsProjectionMap.put(Events._ID, "Events._id AS _id"); sEventsProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id")4036 sEventsProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id"); sEventsProjectionMap.put(Events._SYNC_VERSION, "Events._sync_version AS _sync_version")4037 sEventsProjectionMap.put(Events._SYNC_VERSION, "Events._sync_version AS _sync_version"); sEventsProjectionMap.put(Events._SYNC_TIME, "Events._sync_time AS _sync_time")4038 sEventsProjectionMap.put(Events._SYNC_TIME, "Events._sync_time AS _sync_time"); sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id")4039 sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id"); sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty")4040 sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty"); sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account")4041 sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account"); sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, "Events._sync_account_type AS _sync_account_type")4042 sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, 4043 "Events._sync_account_type AS _sync_account_type"); 4044 4045 // Instances columns sInstancesProjectionMap.put(Instances.BEGIN, "begin")4046 sInstancesProjectionMap.put(Instances.BEGIN, "begin"); sInstancesProjectionMap.put(Instances.END, "end")4047 sInstancesProjectionMap.put(Instances.END, "end"); sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id")4048 sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id"); sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id")4049 sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id"); sInstancesProjectionMap.put(Instances.START_DAY, "startDay")4050 sInstancesProjectionMap.put(Instances.START_DAY, "startDay"); sInstancesProjectionMap.put(Instances.END_DAY, "endDay")4051 sInstancesProjectionMap.put(Instances.END_DAY, "endDay"); sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute")4052 sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute"); sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute")4053 sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute"); 4054 4055 // BusyBits columns 4056 sBusyBitsProjectionMap = new HashMap<String, String>(); sBusyBitsProjectionMap.put(BusyBits.DAY, "day")4057 sBusyBitsProjectionMap.put(BusyBits.DAY, "day"); sBusyBitsProjectionMap.put(BusyBits.BUSYBITS, "busyBits")4058 sBusyBitsProjectionMap.put(BusyBits.BUSYBITS, "busyBits"); sBusyBitsProjectionMap.put(BusyBits.ALL_DAY_COUNT, "allDayCount")4059 sBusyBitsProjectionMap.put(BusyBits.ALL_DAY_COUNT, "allDayCount"); 4060 4061 // Attendees columns sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id")4062 sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id"); sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id")4063 sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName")4064 sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail")4065 sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus")4066 sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship")4067 sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship"); sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType")4068 sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType"); 4069 4070 // Reminders columns sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id")4071 sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id"); sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id")4072 sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id"); sRemindersProjectionMap.put(Reminders.MINUTES, "minutes")4073 sRemindersProjectionMap.put(Reminders.MINUTES, "minutes"); sRemindersProjectionMap.put(Reminders.METHOD, "method")4074 sRemindersProjectionMap.put(Reminders.METHOD, "method"); 4075 4076 // CalendarAlerts columns sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id")4077 sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id"); sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id")4078 sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id"); sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin")4079 sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin"); sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end")4080 sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end"); sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime")4081 sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime"); sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state")4082 sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state"); sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes")4083 sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes"); 4084 } 4085 4086 /** 4087 * An implementation of EntityIterator that builds the Entity for a calendar event. 4088 */ 4089 private static class CalendarEntityIterator implements EntityIterator { 4090 private final Cursor mEntityCursor; 4091 private volatile boolean mIsClosed; 4092 private final SQLiteDatabase mDb; 4093 4094 private static final String[] EVENTS_PROJECTION = new String[]{ 4095 Calendar.Events._ID, 4096 Calendar.Events.HTML_URI, 4097 Calendar.Events.TITLE, 4098 Calendar.Events.DESCRIPTION, 4099 Calendar.Events.EVENT_LOCATION, 4100 Calendar.Events.STATUS, 4101 Calendar.Events.SELF_ATTENDEE_STATUS, 4102 Calendar.Events.COMMENTS_URI, 4103 Calendar.Events.DTSTART, 4104 Calendar.Events.DTEND, 4105 Calendar.Events.DURATION, 4106 Calendar.Events.EVENT_TIMEZONE, 4107 Calendar.Events.ALL_DAY, 4108 Calendar.Events.VISIBILITY, 4109 Calendar.Events.TRANSPARENCY, 4110 Calendar.Events.HAS_ALARM, 4111 Calendar.Events.HAS_EXTENDED_PROPERTIES, 4112 Calendar.Events.RRULE, 4113 Calendar.Events.RDATE, 4114 Calendar.Events.EXRULE, 4115 Calendar.Events.EXDATE, 4116 Calendar.Events.ORIGINAL_EVENT, 4117 Calendar.Events.ORIGINAL_INSTANCE_TIME, 4118 Calendar.Events.ORIGINAL_ALL_DAY, 4119 Calendar.Events.LAST_DATE, 4120 Calendar.Events.HAS_ATTENDEE_DATA, 4121 Calendar.Events.CALENDAR_ID, 4122 Calendar.Events.GUESTS_CAN_INVITE_OTHERS, 4123 Calendar.Events.GUESTS_CAN_MODIFY, 4124 Calendar.Events.GUESTS_CAN_SEE_GUESTS, 4125 Calendar.Events.ORGANIZER, 4126 }; 4127 private static final int COLUMN_ID = 0; 4128 private static final int COLUMN_HTML_URI = 1; 4129 private static final int COLUMN_TITLE = 2; 4130 private static final int COLUMN_DESCRIPTION = 3; 4131 private static final int COLUMN_EVENT_LOCATION = 4; 4132 private static final int COLUMN_STATUS = 5; 4133 private static final int COLUMN_SELF_ATTENDEE_STATUS = 6; 4134 private static final int COLUMN_COMMENTS_URI = 7; 4135 private static final int COLUMN_DTSTART = 8; 4136 private static final int COLUMN_DTEND = 9; 4137 private static final int COLUMN_DURATION = 10; 4138 private static final int COLUMN_EVENT_TIMEZONE = 11; 4139 private static final int COLUMN_ALL_DAY = 12; 4140 private static final int COLUMN_VISIBILITY = 13; 4141 private static final int COLUMN_TRANSPARENCY = 14; 4142 private static final int COLUMN_HAS_ALARM = 15; 4143 private static final int COLUMN_HAS_EXTENDED_PROPERTIES = 16; 4144 private static final int COLUMN_RRULE = 17; 4145 private static final int COLUMN_RDATE = 18; 4146 private static final int COLUMN_EXRULE = 19; 4147 private static final int COLUMN_EXDATE = 20; 4148 private static final int COLUMN_ORIGINAL_EVENT = 21; 4149 private static final int COLUMN_ORIGINAL_INSTANCE_TIME = 22; 4150 private static final int COLUMN_ORIGINAL_ALL_DAY = 23; 4151 private static final int COLUMN_LAST_DATE = 24; 4152 private static final int COLUMN_HAS_ATTENDEE_DATA = 25; 4153 private static final int COLUMN_CALENDAR_ID = 26; 4154 private static final int COLUMN_GUESTS_CAN_INVITE_OTHERS = 27; 4155 private static final int COLUMN_GUESTS_CAN_MODIFY = 28; 4156 private static final int COLUMN_GUESTS_CAN_SEE_GUESTS = 29; 4157 private static final int COLUMN_ORGANIZER = 30; 4158 4159 private static final String[] REMINDERS_PROJECTION = new String[] { 4160 Calendar.Reminders.MINUTES, 4161 Calendar.Reminders.METHOD, 4162 }; 4163 private static final int COLUMN_MINUTES = 0; 4164 private static final int COLUMN_METHOD = 1; 4165 4166 private static final String[] ATTENDEES_PROJECTION = new String[] { 4167 Calendar.Attendees.ATTENDEE_NAME, 4168 Calendar.Attendees.ATTENDEE_EMAIL, 4169 Calendar.Attendees.ATTENDEE_RELATIONSHIP, 4170 Calendar.Attendees.ATTENDEE_TYPE, 4171 Calendar.Attendees.ATTENDEE_STATUS, 4172 }; 4173 private static final int COLUMN_ATTENDEE_NAME = 0; 4174 private static final int COLUMN_ATTENDEE_EMAIL = 1; 4175 private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2; 4176 private static final int COLUMN_ATTENDEE_TYPE = 3; 4177 private static final int COLUMN_ATTENDEE_STATUS = 4; 4178 private static final String[] EXTENDED_PROJECTION = new String[] { 4179 Calendar.ExtendedProperties.NAME, 4180 Calendar.ExtendedProperties.VALUE, 4181 }; 4182 private static final int COLUMN_NAME = 0; 4183 private static final int COLUMN_VALUE = 1; 4184 CalendarEntityIterator(CalendarProvider provider, String eventIdString, Uri uri, String selection, String[] selectionArgs, String sortOrder)4185 public CalendarEntityIterator(CalendarProvider provider, String eventIdString, Uri uri, 4186 String selection, String[] selectionArgs, String sortOrder) { 4187 mIsClosed = false; 4188 mDb = provider.mOpenHelper.getReadableDatabase(); 4189 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4190 qb.setTables(sEventsTable); 4191 if (eventIdString != null) { 4192 qb.appendWhere(Calendar.Events._ID + "=" + eventIdString); 4193 } 4194 mEntityCursor = qb.query(mDb, EVENTS_PROJECTION, selection, selectionArgs, 4195 null, null, sortOrder); 4196 mEntityCursor.moveToFirst(); 4197 } 4198 close()4199 public void close() { 4200 if (mIsClosed) { 4201 throw new IllegalStateException("closing when already closed"); 4202 } 4203 mIsClosed = true; 4204 mEntityCursor.close(); 4205 } 4206 hasNext()4207 public boolean hasNext() throws RemoteException { 4208 4209 if (mIsClosed) { 4210 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 4211 } 4212 4213 return !mEntityCursor.isAfterLast(); 4214 } 4215 reset()4216 public void reset() throws RemoteException { 4217 if (mIsClosed) { 4218 throw new IllegalStateException("calling next() when the iterator is closed"); 4219 } 4220 mEntityCursor.moveToFirst(); 4221 } 4222 next()4223 public Entity next() throws RemoteException { 4224 if (mIsClosed) { 4225 throw new IllegalStateException("calling next() when the iterator is closed"); 4226 } 4227 if (!hasNext()) { 4228 throw new IllegalStateException("you may only call next() if hasNext() is true"); 4229 } 4230 4231 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 4232 final long eventId = c.getLong(COLUMN_ID); 4233 4234 // we expect the cursor is already at the row we need to read from 4235 ContentValues entityValues = new ContentValues(); 4236 entityValues.put(Calendar.Events._ID, eventId); 4237 entityValues.put(Calendar.Events.CALENDAR_ID, c.getInt(COLUMN_CALENDAR_ID)); 4238 entityValues.put(Calendar.Events.HTML_URI, c.getString(COLUMN_HTML_URI)); 4239 entityValues.put(Calendar.Events.TITLE, c.getString(COLUMN_TITLE)); 4240 entityValues.put(Calendar.Events.DESCRIPTION, c.getString(COLUMN_DESCRIPTION)); 4241 entityValues.put(Calendar.Events.EVENT_LOCATION, c.getString(COLUMN_EVENT_LOCATION)); 4242 entityValues.put(Calendar.Events.STATUS, c.getInt(COLUMN_STATUS)); 4243 entityValues.put(Calendar.Events.SELF_ATTENDEE_STATUS, 4244 c.getInt(COLUMN_SELF_ATTENDEE_STATUS)); 4245 entityValues.put(Calendar.Events.COMMENTS_URI, c.getString(COLUMN_COMMENTS_URI)); 4246 entityValues.put(Calendar.Events.DTSTART, c.getLong(COLUMN_DTSTART)); 4247 entityValues.put(Calendar.Events.DTEND, c.getLong(COLUMN_DTEND)); 4248 entityValues.put(Calendar.Events.DURATION, c.getString(COLUMN_DURATION)); 4249 entityValues.put(Calendar.Events.EVENT_TIMEZONE, c.getString(COLUMN_EVENT_TIMEZONE)); 4250 entityValues.put(Calendar.Events.ALL_DAY, c.getString(COLUMN_ALL_DAY)); 4251 entityValues.put(Calendar.Events.VISIBILITY, c.getInt(COLUMN_VISIBILITY)); 4252 entityValues.put(Calendar.Events.TRANSPARENCY, c.getInt(COLUMN_TRANSPARENCY)); 4253 entityValues.put(Calendar.Events.HAS_ALARM, c.getString(COLUMN_HAS_ALARM)); 4254 entityValues.put(Calendar.Events.HAS_EXTENDED_PROPERTIES, 4255 c.getString(COLUMN_HAS_EXTENDED_PROPERTIES)); 4256 entityValues.put(Calendar.Events.RRULE, c.getString(COLUMN_RRULE)); 4257 entityValues.put(Calendar.Events.RDATE, c.getString(COLUMN_RDATE)); 4258 entityValues.put(Calendar.Events.EXRULE, c.getString(COLUMN_EXRULE)); 4259 entityValues.put(Calendar.Events.EXDATE, c.getString(COLUMN_EXDATE)); 4260 entityValues.put(Calendar.Events.ORIGINAL_EVENT, c.getString(COLUMN_ORIGINAL_EVENT)); 4261 entityValues.put(Calendar.Events.ORIGINAL_INSTANCE_TIME, 4262 c.getLong(COLUMN_ORIGINAL_INSTANCE_TIME)); 4263 entityValues.put(Calendar.Events.ORIGINAL_ALL_DAY, c.getInt(COLUMN_ORIGINAL_ALL_DAY)); 4264 entityValues.put(Calendar.Events.LAST_DATE, c.getLong(COLUMN_LAST_DATE)); 4265 entityValues.put(Calendar.Events.HAS_ATTENDEE_DATA, 4266 c.getInt(COLUMN_HAS_ATTENDEE_DATA)); 4267 entityValues.put(Calendar.Events.GUESTS_CAN_INVITE_OTHERS, 4268 c.getInt(COLUMN_GUESTS_CAN_INVITE_OTHERS)); 4269 entityValues.put(Calendar.Events.GUESTS_CAN_MODIFY, 4270 c.getInt(COLUMN_GUESTS_CAN_MODIFY)); 4271 entityValues.put(Calendar.Events.GUESTS_CAN_SEE_GUESTS, 4272 c.getInt(COLUMN_GUESTS_CAN_SEE_GUESTS)); 4273 entityValues.put(Calendar.Events.ORGANIZER, c.getString(COLUMN_ORGANIZER)); 4274 4275 Entity entity = new Entity(entityValues); 4276 Cursor cursor = null; 4277 try { 4278 cursor = mDb.query(sRemindersTable, REMINDERS_PROJECTION, "event_id=" + eventId, 4279 null, null, null, null); 4280 while (cursor.moveToNext()) { 4281 ContentValues reminderValues = new ContentValues(); 4282 reminderValues.put(Calendar.Reminders.MINUTES, cursor.getInt(COLUMN_MINUTES)); 4283 reminderValues.put(Calendar.Reminders.METHOD, cursor.getInt(COLUMN_METHOD)); 4284 entity.addSubValue(Calendar.Reminders.CONTENT_URI, reminderValues); 4285 } 4286 } finally { 4287 if (cursor != null) { 4288 cursor.close(); 4289 } 4290 } 4291 4292 cursor = null; 4293 try { 4294 cursor = mDb.query(sAttendeesTable, ATTENDEES_PROJECTION, "event_id=" + eventId, 4295 null, null, null, null); 4296 while (cursor.moveToNext()) { 4297 ContentValues attendeeValues = new ContentValues(); 4298 attendeeValues.put(Calendar.Attendees.ATTENDEE_NAME, 4299 cursor.getString(COLUMN_ATTENDEE_NAME)); 4300 attendeeValues.put(Calendar.Attendees.ATTENDEE_EMAIL, 4301 cursor.getString(COLUMN_ATTENDEE_EMAIL)); 4302 attendeeValues.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 4303 cursor.getInt(COLUMN_ATTENDEE_RELATIONSHIP)); 4304 attendeeValues.put(Calendar.Attendees.ATTENDEE_TYPE, 4305 cursor.getInt(COLUMN_ATTENDEE_TYPE)); 4306 attendeeValues.put(Calendar.Attendees.ATTENDEE_STATUS, 4307 cursor.getInt(COLUMN_ATTENDEE_STATUS)); 4308 entity.addSubValue(Calendar.Attendees.CONTENT_URI, attendeeValues); 4309 } 4310 } finally { 4311 if (cursor != null) { 4312 cursor.close(); 4313 } 4314 } 4315 4316 cursor = null; 4317 try { 4318 cursor = mDb.query(sExtendedPropertiesTable, EXTENDED_PROJECTION, 4319 "event_id=" + eventId, null, null, null, null); 4320 while (cursor.moveToNext()) { 4321 ContentValues extendedValues = new ContentValues(); 4322 extendedValues.put(Calendar.ExtendedProperties.NAME, c.getString(COLUMN_NAME)); 4323 extendedValues.put(Calendar.ExtendedProperties.VALUE, 4324 c.getString(COLUMN_VALUE)); 4325 entity.addSubValue(Calendar.ExtendedProperties.CONTENT_URI, extendedValues); 4326 } 4327 } finally { 4328 if (cursor != null) { 4329 cursor.close(); 4330 } 4331 } 4332 4333 mEntityCursor.moveToNext(); 4334 // add the data to the contact 4335 return entity; 4336 } 4337 } 4338 4339 @Override queryEntities(Uri uri, String selection, String[] selectionArgs, String sortOrder)4340 public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, 4341 String sortOrder) { 4342 final int match = sURLMatcher.match(uri); 4343 switch (match) { 4344 case EVENTS: 4345 case EVENTS_ID: 4346 String calendarId = null; 4347 if (match == EVENTS_ID) { 4348 calendarId = uri.getPathSegments().get(1); 4349 } 4350 4351 return new CalendarEntityIterator(this, calendarId, 4352 uri, selection, selectionArgs, sortOrder); 4353 default: 4354 throw new UnsupportedOperationException("Unknown uri: " + uri); 4355 } 4356 } 4357 4358 @Override applyBatch(ArrayList<ContentProviderOperation> operations)4359 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 4360 throws OperationApplicationException { 4361 4362 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 4363 db.beginTransaction(); 4364 try { 4365 ContentProviderResult[] results = super.applyBatch(operations); 4366 db.setTransactionSuccessful(); 4367 return results; 4368 } finally { 4369 db.endTransaction(); 4370 } 4371 } 4372 } 4373