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