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