CalendarProvider2.java revision bf61571797b7b6a390d35f16aad7765ea348e5ae
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 com.android.calendarcommon.DateException; 21import com.android.calendarcommon.EventRecurrence; 22import com.android.calendarcommon.RecurrenceProcessor; 23import com.android.calendarcommon.RecurrenceSet; 24import com.android.providers.calendar.CalendarDatabaseHelper.Tables; 25import com.android.providers.calendar.CalendarDatabaseHelper.Views; 26import com.google.common.annotations.VisibleForTesting; 27 28import android.accounts.Account; 29import android.accounts.AccountManager; 30import android.accounts.OnAccountsUpdateListener; 31import android.content.BroadcastReceiver; 32import android.content.ContentResolver; 33import android.content.ContentUris; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Intent; 37import android.content.IntentFilter; 38import android.content.UriMatcher; 39import android.database.Cursor; 40import android.database.DatabaseUtils; 41import android.database.SQLException; 42import android.database.sqlite.SQLiteDatabase; 43import android.database.sqlite.SQLiteQueryBuilder; 44import android.net.Uri; 45import android.os.Handler; 46import android.os.Message; 47import android.os.Process; 48import android.provider.BaseColumns; 49import android.provider.CalendarContract; 50import android.provider.CalendarContract.Attendees; 51import android.provider.CalendarContract.CalendarAlerts; 52import android.provider.CalendarContract.Calendars; 53import android.provider.CalendarContract.Events; 54import android.provider.CalendarContract.Instances; 55import android.provider.CalendarContract.Reminders; 56import android.provider.CalendarContract.SyncState; 57import android.text.TextUtils; 58import android.text.format.DateUtils; 59import android.text.format.Time; 60import android.util.Log; 61import android.util.TimeFormatException; 62import android.util.TimeUtils; 63 64import java.lang.reflect.Array; 65import java.util.ArrayList; 66import java.util.Arrays; 67import java.util.HashMap; 68import java.util.HashSet; 69import java.util.List; 70import java.util.Set; 71import java.util.TimeZone; 72import java.util.regex.Matcher; 73import java.util.regex.Pattern; 74 75/** 76 * Calendar content provider. The contract between this provider and applications 77 * is defined in {@link android.provider.CalendarContract}. 78 */ 79public class CalendarProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener { 80 81 82 protected static final String TAG = "CalendarProvider2"; 83 84 private static final String TIMEZONE_GMT = "GMT"; 85 private static final String ACCOUNT_SELECTION_PREFIX = Calendars.ACCOUNT_NAME + "=? AND " 86 + Calendars.ACCOUNT_TYPE + "=?"; 87 88 protected static final boolean PROFILE = false; 89 private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true; 90 91 private static final String[] ID_ONLY_PROJECTION = 92 new String[] {Events._ID}; 93 94 private static final String[] EVENTS_PROJECTION = new String[] { 95 Events._SYNC_ID, 96 Events.RRULE, 97 Events.RDATE, 98 Events.ORIGINAL_ID, 99 Events.ORIGINAL_SYNC_ID, 100 }; 101 102 private static final int EVENTS_SYNC_ID_INDEX = 0; 103 private static final int EVENTS_RRULE_INDEX = 1; 104 private static final int EVENTS_RDATE_INDEX = 2; 105 private static final int EVENTS_ORIGINAL_ID_INDEX = 3; 106 private static final int EVENTS_ORIGINAL_SYNC_ID_INDEX = 4; 107 108 private static final String[] ID_PROJECTION = new String[] { 109 Attendees._ID, 110 Attendees.EVENT_ID, // Assume these are the same for each table 111 }; 112 private static final int ID_INDEX = 0; 113 private static final int EVENT_ID_INDEX = 1; 114 115 /** 116 * Projection to query for correcting times in allDay events. 117 */ 118 private static final String[] ALLDAY_TIME_PROJECTION = new String[] { 119 Events._ID, 120 Events.DTSTART, 121 Events.DTEND, 122 Events.DURATION 123 }; 124 private static final int ALLDAY_ID_INDEX = 0; 125 private static final int ALLDAY_DTSTART_INDEX = 1; 126 private static final int ALLDAY_DTEND_INDEX = 2; 127 private static final int ALLDAY_DURATION_INDEX = 3; 128 129 private static final int DAY_IN_SECONDS = 24 * 60 * 60; 130 131 /** 132 * The cached copy of the CalendarMetaData database table. 133 * Make this "package private" instead of "private" so that test code 134 * can access it. 135 */ 136 MetaData mMetaData; 137 CalendarCache mCalendarCache; 138 139 private CalendarDatabaseHelper mDbHelper; 140 private CalendarInstancesHelper mInstancesHelper; 141 142 // The extended property name for storing an Event original Timezone. 143 // Due to an issue in Calendar Server restricting the length of the name we 144 // had to strip it down 145 // TODO - Better name would be: 146 // "com.android.providers.calendar.CalendarSyncAdapter#originalTimezone" 147 protected static final String EXT_PROP_ORIGINAL_TIMEZONE = 148 "CalendarSyncAdapter#originalTimezone"; 149 150 private static final String SQL_SELECT_EVENTSRAWTIMES = "SELECT " + 151 CalendarContract.EventsRawTimes.EVENT_ID + ", " + 152 CalendarContract.EventsRawTimes.DTSTART_2445 + ", " + 153 CalendarContract.EventsRawTimes.DTEND_2445 + ", " + 154 Events.EVENT_TIMEZONE + 155 " FROM " + 156 Tables.EVENTS_RAW_TIMES + ", " + 157 Tables.EVENTS + 158 " WHERE " + 159 CalendarContract.EventsRawTimes.EVENT_ID + " = " + Tables.EVENTS + "." + Events._ID; 160 161 private static final String SQL_UPDATE_EVENT_SET_DIRTY = "UPDATE " + 162 Tables.EVENTS + 163 " SET " + Events.DIRTY + "=1" + 164 " WHERE " + Events._ID + "=?"; 165 166 protected static final String SQL_WHERE_ID = Events._ID + "=?"; 167 private static final String SQL_WHERE_EVENT_ID = "event_id=?"; 168 private static final String SQL_WHERE_ORIGINAL_EVENT = Events.ORIGINAL_SYNC_ID + "=?"; 169 private static final String SQL_WHERE_ATTENDEES_ID = 170 Tables.ATTENDEES + "." + Attendees._ID + "=? AND " + 171 Tables.EVENTS + "." + Events._ID + "=" + Tables.ATTENDEES + "." + Attendees.EVENT_ID; 172 173 private static final String SQL_WHERE_REMINDERS_ID = 174 Tables.REMINDERS + "." + Reminders._ID + "=? AND " + 175 Tables.EVENTS + "." + Events._ID + "=" + Tables.REMINDERS + "." + Reminders.EVENT_ID; 176 177 private static final String SQL_WHERE_CALENDAR_ALERT = 178 Views.EVENTS + "." + Events._ID + "=" + 179 Tables.CALENDAR_ALERTS + "." + CalendarAlerts.EVENT_ID; 180 181 private static final String SQL_WHERE_CALENDAR_ALERT_ID = 182 Views.EVENTS + "." + Events._ID + "=" + 183 Tables.CALENDAR_ALERTS + "." + CalendarAlerts.EVENT_ID + 184 " AND " + 185 Tables.CALENDAR_ALERTS + "." + CalendarAlerts._ID + "=?"; 186 187 private static final String SQL_WHERE_EXTENDED_PROPERTIES_ID = 188 Tables.EXTENDED_PROPERTIES + "." + CalendarContract.ExtendedProperties._ID + "=?"; 189 190 private static final String SQL_DELETE_FROM_CALENDARS = "DELETE FROM " + Tables.CALENDARS + 191 " WHERE " + Calendars.ACCOUNT_NAME + "=? AND " + 192 Calendars.ACCOUNT_TYPE + "=?"; 193 194 private static final String SQL_SELECT_COUNT_FOR_SYNC_ID = 195 "SELECT COUNT(*) FROM " + Tables.EVENTS + " WHERE " + Events._SYNC_ID + "=?"; 196 197 // Make sure we load at least two months worth of data. 198 // Client apps can load more data in a background thread. 199 private static final long MINIMUM_EXPANSION_SPAN = 200 2L * 31 * 24 * 60 * 60 * 1000; 201 202 private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID }; 203 private static final int CALENDARS_INDEX_ID = 0; 204 205 private static final String INSTANCE_QUERY_TABLES = 206 CalendarDatabaseHelper.Tables.INSTANCES + " INNER JOIN " + 207 CalendarDatabaseHelper.Views.EVENTS + " AS " + 208 CalendarDatabaseHelper.Tables.EVENTS + 209 " ON (" + CalendarDatabaseHelper.Tables.INSTANCES + "." 210 + CalendarContract.Instances.EVENT_ID + "=" + 211 CalendarDatabaseHelper.Tables.EVENTS + "." 212 + CalendarContract.Events._ID + ")"; 213 214 private static final String INSTANCE_SEARCH_QUERY_TABLES = "(" + 215 CalendarDatabaseHelper.Tables.INSTANCES + " INNER JOIN " + 216 CalendarDatabaseHelper.Views.EVENTS + " AS " + 217 CalendarDatabaseHelper.Tables.EVENTS + 218 " ON (" + CalendarDatabaseHelper.Tables.INSTANCES + "." 219 + CalendarContract.Instances.EVENT_ID + "=" + 220 CalendarDatabaseHelper.Tables.EVENTS + "." 221 + CalendarContract.Events._ID + ")" + ") LEFT OUTER JOIN " + 222 CalendarDatabaseHelper.Tables.ATTENDEES + 223 " ON (" + CalendarDatabaseHelper.Tables.ATTENDEES + "." 224 + CalendarContract.Attendees.EVENT_ID + "=" + 225 CalendarDatabaseHelper.Tables.EVENTS + "." 226 + CalendarContract.Events._ID + ")"; 227 228 private static final String SQL_WHERE_INSTANCES_BETWEEN_DAY = 229 CalendarContract.Instances.START_DAY + "<=? AND " + 230 CalendarContract.Instances.END_DAY + ">=?"; 231 232 private static final String SQL_WHERE_INSTANCES_BETWEEN = 233 CalendarContract.Instances.BEGIN + "<=? AND " + 234 CalendarContract.Instances.END + ">=?"; 235 236 private static final int INSTANCES_INDEX_START_DAY = 0; 237 private static final int INSTANCES_INDEX_END_DAY = 1; 238 private static final int INSTANCES_INDEX_START_MINUTE = 2; 239 private static final int INSTANCES_INDEX_END_MINUTE = 3; 240 private static final int INSTANCES_INDEX_ALL_DAY = 4; 241 242 /** 243 * The sort order is: events with an earlier start time occur first and if 244 * the start times are the same, then events with a later end time occur 245 * first. The later end time is ordered first so that long-running events in 246 * the calendar views appear first. If the start and end times of two events 247 * are the same then we sort alphabetically on the title. This isn't 248 * required for correctness, it just adds a nice touch. 249 */ 250 public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC"; 251 252 /** 253 * A regex for describing how we split search queries into tokens. Keeps 254 * quoted phrases as one token. "one \"two three\"" ==> ["one" "two three"] 255 */ 256 private static final Pattern SEARCH_TOKEN_PATTERN = 257 Pattern.compile("[^\\s\"'.?!,]+|" // first part matches unquoted words 258 + "\"([^\"]*)\""); // second part matches quoted phrases 259 /** 260 * A special character that was use to escape potentially problematic 261 * characters in search queries. 262 * 263 * Note: do not use backslash for this, as it interferes with the regex 264 * escaping mechanism. 265 */ 266 private static final String SEARCH_ESCAPE_CHAR = "#"; 267 268 /** 269 * A regex for matching any characters in an incoming search query that we 270 * need to escape with {@link #SEARCH_ESCAPE_CHAR}, including the escape 271 * character itself. 272 */ 273 private static final Pattern SEARCH_ESCAPE_PATTERN = 274 Pattern.compile("([%_" + SEARCH_ESCAPE_CHAR + "])"); 275 276 /** 277 * Alias used for aggregate concatenation of attendee e-mails when grouping 278 * attendees by instance. 279 */ 280 private static final String ATTENDEES_EMAIL_CONCAT = 281 "group_concat(" + CalendarContract.Attendees.ATTENDEE_EMAIL + ")"; 282 283 /** 284 * Alias used for aggregate concatenation of attendee names when grouping 285 * attendees by instance. 286 */ 287 private static final String ATTENDEES_NAME_CONCAT = 288 "group_concat(" + CalendarContract.Attendees.ATTENDEE_NAME + ")"; 289 290 private static final String[] SEARCH_COLUMNS = new String[] { 291 CalendarContract.Events.TITLE, 292 CalendarContract.Events.DESCRIPTION, 293 CalendarContract.Events.EVENT_LOCATION, 294 ATTENDEES_EMAIL_CONCAT, 295 ATTENDEES_NAME_CONCAT 296 }; 297 298 /** 299 * Arbitrary integer that we assign to the messages that we send to this 300 * thread's handler, indicating that these are requests to send an update 301 * notification intent. 302 */ 303 private static final int UPDATE_BROADCAST_MSG = 1; 304 305 /** 306 * Any requests to send a PROVIDER_CHANGED intent will be collapsed over 307 * this window, to prevent spamming too many intents at once. 308 */ 309 private static final long UPDATE_BROADCAST_TIMEOUT_MILLIS = 310 DateUtils.SECOND_IN_MILLIS; 311 312 private static final long SYNC_UPDATE_BROADCAST_TIMEOUT_MILLIS = 313 30 * DateUtils.SECOND_IN_MILLIS; 314 315 /** Set of columns allowed to be altered when creating an exception to a recurring event. */ 316 private static final HashSet<String> ALLOWED_IN_EXCEPTION = new HashSet<String>(); 317 static { 318 // _id, _sync_account, _sync_account_type, dirty, _sync_mark, calendar_id 319 ALLOWED_IN_EXCEPTION.add(Events._SYNC_ID); 320 ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA1); 321 ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA7); 322 ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA3); 323 ALLOWED_IN_EXCEPTION.add(Events.TITLE); 324 ALLOWED_IN_EXCEPTION.add(Events.EVENT_LOCATION); 325 ALLOWED_IN_EXCEPTION.add(Events.DESCRIPTION); 326 ALLOWED_IN_EXCEPTION.add(Events.STATUS); 327 ALLOWED_IN_EXCEPTION.add(Events.SELF_ATTENDEE_STATUS); 328 ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA6); 329 ALLOWED_IN_EXCEPTION.add(Events.DTSTART); 330 // dtend -- set from duration as part of creating the exception 331 ALLOWED_IN_EXCEPTION.add(Events.EVENT_TIMEZONE); 332 ALLOWED_IN_EXCEPTION.add(Events.EVENT_END_TIMEZONE); 333 ALLOWED_IN_EXCEPTION.add(Events.DURATION); 334 ALLOWED_IN_EXCEPTION.add(Events.ALL_DAY); 335 ALLOWED_IN_EXCEPTION.add(Events.ACCESS_LEVEL); 336 ALLOWED_IN_EXCEPTION.add(Events.AVAILABILITY); 337 ALLOWED_IN_EXCEPTION.add(Events.HAS_ALARM); 338 ALLOWED_IN_EXCEPTION.add(Events.HAS_EXTENDED_PROPERTIES); 339 ALLOWED_IN_EXCEPTION.add(Events.RRULE); 340 ALLOWED_IN_EXCEPTION.add(Events.RDATE); 341 ALLOWED_IN_EXCEPTION.add(Events.EXRULE); 342 ALLOWED_IN_EXCEPTION.add(Events.EXDATE); 343 ALLOWED_IN_EXCEPTION.add(Events.ORIGINAL_SYNC_ID); 344 ALLOWED_IN_EXCEPTION.add(Events.ORIGINAL_INSTANCE_TIME); 345 // originalAllDay, lastDate 346 ALLOWED_IN_EXCEPTION.add(Events.HAS_ATTENDEE_DATA); 347 ALLOWED_IN_EXCEPTION.add(Events.GUESTS_CAN_MODIFY); 348 ALLOWED_IN_EXCEPTION.add(Events.GUESTS_CAN_INVITE_OTHERS); 349 ALLOWED_IN_EXCEPTION.add(Events.GUESTS_CAN_SEE_GUESTS); 350 ALLOWED_IN_EXCEPTION.add(Events.ORGANIZER); 351 // deleted, original_id, alerts 352 } 353 354 /** Don't clone these from the base event into the exception event. */ 355 private static final String[] DONT_CLONE_INTO_EXCEPTION = { 356 Events._SYNC_ID, 357 Events.SYNC_DATA1, 358 Events.SYNC_DATA2, 359 Events.SYNC_DATA3, 360 Events.SYNC_DATA4, 361 Events.SYNC_DATA5, 362 Events.SYNC_DATA6, 363 Events.SYNC_DATA7, 364 Events.SYNC_DATA8, 365 Events.SYNC_DATA9, 366 Events.SYNC_DATA10, 367 }; 368 369 /** set to 'true' to enable debug logging for recurrence exception code */ 370 private static final boolean DEBUG_EXCEPTION = false; 371 372 private Context mContext; 373 private ContentResolver mContentResolver; 374 375 private static CalendarProvider2 mInstance; 376 377 @VisibleForTesting 378 protected CalendarAlarmManager mCalendarAlarm; 379 380 private final Handler mBroadcastHandler = new Handler() { 381 @Override 382 public void handleMessage(Message msg) { 383 Context context = CalendarProvider2.this.mContext; 384 if (msg.what == UPDATE_BROADCAST_MSG) { 385 // Broadcast a provider changed intent 386 doSendUpdateNotification(); 387 // Because the handler does not guarantee message delivery in 388 // the case that the provider is killed, we need to make sure 389 // that the provider stays alive long enough to deliver the 390 // notification. This empty service is sufficient to "wedge" the 391 // process until we stop it here. 392 context.stopService(new Intent(context, EmptyService.class)); 393 } 394 } 395 }; 396 397 /** 398 * Listens for timezone changes and disk-no-longer-full events 399 */ 400 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 401 @Override 402 public void onReceive(Context context, Intent intent) { 403 String action = intent.getAction(); 404 if (Log.isLoggable(TAG, Log.DEBUG)) { 405 Log.d(TAG, "onReceive() " + action); 406 } 407 if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { 408 updateTimezoneDependentFields(); 409 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 410 } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { 411 // Try to clean up if things were screwy due to a full disk 412 updateTimezoneDependentFields(); 413 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 414 } else if (Intent.ACTION_TIME_CHANGED.equals(action)) { 415 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 416 } 417 } 418 }; 419 420 protected void verifyAccounts() { 421 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 422 onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); 423 } 424 425 /* Visible for testing */ 426 @Override 427 protected CalendarDatabaseHelper getDatabaseHelper(final Context context) { 428 return CalendarDatabaseHelper.getInstance(context); 429 } 430 431 protected static CalendarProvider2 getInstance() { 432 return mInstance; 433 } 434 435 @Override 436 public void shutdown() { 437 if (mDbHelper != null) { 438 mDbHelper.close(); 439 mDbHelper = null; 440 mDb = null; 441 } 442 } 443 444 @Override 445 public boolean onCreate() { 446 super.onCreate(); 447 try { 448 return initialize(); 449 } catch (RuntimeException e) { 450 if (Log.isLoggable(TAG, Log.ERROR)) { 451 Log.e(TAG, "Cannot start provider", e); 452 } 453 return false; 454 } 455 } 456 457 private boolean initialize() { 458 mInstance = this; 459 460 mContext = getContext(); 461 mContentResolver = mContext.getContentResolver(); 462 463 mDbHelper = (CalendarDatabaseHelper)getDatabaseHelper(); 464 mDb = mDbHelper.getWritableDatabase(); 465 466 mMetaData = new MetaData(mDbHelper); 467 mInstancesHelper = new CalendarInstancesHelper(mDbHelper, mMetaData); 468 469 // Register for Intent broadcasts 470 IntentFilter filter = new IntentFilter(); 471 472 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 473 filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); 474 filter.addAction(Intent.ACTION_TIME_CHANGED); 475 476 // We don't ever unregister this because this thread always wants 477 // to receive notifications, even in the background. And if this 478 // thread is killed then the whole process will be killed and the 479 // memory resources will be reclaimed. 480 mContext.registerReceiver(mIntentReceiver, filter); 481 482 mCalendarCache = new CalendarCache(mDbHelper); 483 484 // This is pulled out for testing 485 initCalendarAlarm(); 486 487 postInitialize(); 488 489 return true; 490 } 491 492 protected void initCalendarAlarm() { 493 mCalendarAlarm = getOrCreateCalendarAlarmManager(); 494 mCalendarAlarm.getScheduleNextAlarmWakeLock(); 495 } 496 497 synchronized CalendarAlarmManager getOrCreateCalendarAlarmManager() { 498 if (mCalendarAlarm == null) { 499 mCalendarAlarm = new CalendarAlarmManager(mContext); 500 } 501 return mCalendarAlarm; 502 } 503 504 protected void postInitialize() { 505 Thread thread = new PostInitializeThread(); 506 thread.start(); 507 } 508 509 private class PostInitializeThread extends Thread { 510 @Override 511 public void run() { 512 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 513 514 verifyAccounts(); 515 516 doUpdateTimezoneDependentFields(); 517 } 518 } 519 520 /** 521 * This creates a background thread to check the timezone and update 522 * the timezone dependent fields in the Instances table if the timezone 523 * has changed. 524 */ 525 protected void updateTimezoneDependentFields() { 526 Thread thread = new TimezoneCheckerThread(); 527 thread.start(); 528 } 529 530 private class TimezoneCheckerThread extends Thread { 531 @Override 532 public void run() { 533 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 534 doUpdateTimezoneDependentFields(); 535 } 536 } 537 538 /** 539 * Check if we are in the same time zone 540 */ 541 private boolean isLocalSameAsInstancesTimezone() { 542 String localTimezone = TimeZone.getDefault().getID(); 543 return TextUtils.equals(mCalendarCache.readTimezoneInstances(), localTimezone); 544 } 545 546 /** 547 * This method runs in a background thread. If the timezone has changed 548 * then the Instances table will be regenerated. 549 */ 550 protected void doUpdateTimezoneDependentFields() { 551 try { 552 String timezoneType = mCalendarCache.readTimezoneType(); 553 // Nothing to do if we have the "home" timezone type (timezone is sticky) 554 if (timezoneType != null && timezoneType.equals(CalendarCache.TIMEZONE_TYPE_HOME)) { 555 return; 556 } 557 // We are here in "auto" mode, the timezone is coming from the device 558 if (! isSameTimezoneDatabaseVersion()) { 559 String localTimezone = TimeZone.getDefault().getID(); 560 doProcessEventRawTimes(localTimezone, TimeUtils.getTimeZoneDatabaseVersion()); 561 } 562 if (isLocalSameAsInstancesTimezone()) { 563 // Even if the timezone hasn't changed, check for missed alarms. 564 // This code executes when the CalendarProvider2 is created and 565 // helps to catch missed alarms when the Calendar process is 566 // killed (because of low-memory conditions) and then restarted. 567 mCalendarAlarm.rescheduleMissedAlarms(); 568 } 569 } catch (SQLException e) { 570 if (Log.isLoggable(TAG, Log.ERROR)) { 571 Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e); 572 } 573 try { 574 // Clear at least the in-memory data (and if possible the 575 // database fields) to force a re-computation of Instances. 576 mMetaData.clearInstanceRange(); 577 } catch (SQLException e2) { 578 if (Log.isLoggable(TAG, Log.ERROR)) { 579 Log.e(TAG, "clearInstanceRange() also failed: " + e2); 580 } 581 } 582 } 583 } 584 585 protected void doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion) { 586 mDb.beginTransaction(); 587 try { 588 updateEventsStartEndFromEventRawTimesLocked(); 589 updateTimezoneDatabaseVersion(timeZoneDatabaseVersion); 590 mCalendarCache.writeTimezoneInstances(localTimezone); 591 regenerateInstancesTable(); 592 mDb.setTransactionSuccessful(); 593 } finally { 594 mDb.endTransaction(); 595 } 596 } 597 598 private void updateEventsStartEndFromEventRawTimesLocked() { 599 Cursor cursor = mDb.rawQuery(SQL_SELECT_EVENTSRAWTIMES, null /* selection args */); 600 try { 601 while (cursor.moveToNext()) { 602 long eventId = cursor.getLong(0); 603 String dtStart2445 = cursor.getString(1); 604 String dtEnd2445 = cursor.getString(2); 605 String eventTimezone = cursor.getString(3); 606 if (dtStart2445 == null && dtEnd2445 == null) { 607 if (Log.isLoggable(TAG, Log.ERROR)) { 608 Log.e(TAG, "Event " + eventId + " has dtStart2445 and dtEnd2445 null " 609 + "at the same time in EventsRawTimes!"); 610 } 611 continue; 612 } 613 updateEventsStartEndLocked(eventId, 614 eventTimezone, 615 dtStart2445, 616 dtEnd2445); 617 } 618 } finally { 619 cursor.close(); 620 cursor = null; 621 } 622 } 623 624 private long get2445ToMillis(String timezone, String dt2445) { 625 if (null == dt2445) { 626 if (Log.isLoggable(TAG, Log.VERBOSE)) { 627 Log.v(TAG, "Cannot parse null RFC2445 date"); 628 } 629 return 0; 630 } 631 Time time = (timezone != null) ? new Time(timezone) : new Time(); 632 try { 633 time.parse(dt2445); 634 } catch (TimeFormatException e) { 635 if (Log.isLoggable(TAG, Log.ERROR)) { 636 Log.e(TAG, "Cannot parse RFC2445 date " + dt2445); 637 } 638 return 0; 639 } 640 return time.toMillis(true /* ignore DST */); 641 } 642 643 private void updateEventsStartEndLocked(long eventId, 644 String timezone, String dtStart2445, String dtEnd2445) { 645 646 ContentValues values = new ContentValues(); 647 values.put(Events.DTSTART, get2445ToMillis(timezone, dtStart2445)); 648 values.put(Events.DTEND, get2445ToMillis(timezone, dtEnd2445)); 649 650 int result = mDb.update(Tables.EVENTS, values, SQL_WHERE_ID, 651 new String[] {String.valueOf(eventId)}); 652 if (0 == result) { 653 if (Log.isLoggable(TAG, Log.VERBOSE)) { 654 Log.v(TAG, "Could not update Events table with values " + values); 655 } 656 } 657 } 658 659 private void updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion) { 660 try { 661 mCalendarCache.writeTimezoneDatabaseVersion(timeZoneDatabaseVersion); 662 } catch (CalendarCache.CacheException e) { 663 if (Log.isLoggable(TAG, Log.ERROR)) { 664 Log.e(TAG, "Could not write timezone database version in the cache"); 665 } 666 } 667 } 668 669 /** 670 * Check if the time zone database version is the same as the cached one 671 */ 672 protected boolean isSameTimezoneDatabaseVersion() { 673 String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion(); 674 if (timezoneDatabaseVersion == null) { 675 return false; 676 } 677 return TextUtils.equals(timezoneDatabaseVersion, TimeUtils.getTimeZoneDatabaseVersion()); 678 } 679 680 @VisibleForTesting 681 protected String getTimezoneDatabaseVersion() { 682 String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion(); 683 if (timezoneDatabaseVersion == null) { 684 return ""; 685 } 686 if (Log.isLoggable(TAG, Log.INFO)) { 687 Log.i(TAG, "timezoneDatabaseVersion = " + timezoneDatabaseVersion); 688 } 689 return timezoneDatabaseVersion; 690 } 691 692 private boolean isHomeTimezone() { 693 String type = mCalendarCache.readTimezoneType(); 694 return type.equals(CalendarCache.TIMEZONE_TYPE_HOME); 695 } 696 697 private void regenerateInstancesTable() { 698 // The database timezone is different from the current timezone. 699 // Regenerate the Instances table for this month. Include events 700 // starting at the beginning of this month. 701 long now = System.currentTimeMillis(); 702 String instancesTimezone = mCalendarCache.readTimezoneInstances(); 703 Time time = new Time(instancesTimezone); 704 time.set(now); 705 time.monthDay = 1; 706 time.hour = 0; 707 time.minute = 0; 708 time.second = 0; 709 710 long begin = time.normalize(true); 711 long end = begin + MINIMUM_EXPANSION_SPAN; 712 713 Cursor cursor = null; 714 try { 715 cursor = handleInstanceQuery(new SQLiteQueryBuilder(), 716 begin, end, 717 new String[] { Instances._ID }, 718 null /* selection */, null, 719 null /* sort */, 720 false /* searchByDayInsteadOfMillis */, 721 true /* force Instances deletion and expansion */, 722 instancesTimezone, isHomeTimezone()); 723 } finally { 724 if (cursor != null) { 725 cursor.close(); 726 } 727 } 728 729 mCalendarAlarm.rescheduleMissedAlarms(); 730 } 731 732 733 @Override 734 protected void notifyChange(boolean syncToNetwork) { 735 // Note that semantics are changed: notification is for CONTENT_URI, not the specific 736 // Uri that was modified. 737 mContentResolver.notifyChange(CalendarContract.CONTENT_URI, null, syncToNetwork); 738 } 739 740 @Override 741 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 742 String sortOrder) { 743 if (Log.isLoggable(TAG, Log.VERBOSE)) { 744 Log.v(TAG, "query uri - " + uri); 745 } 746 747 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 748 749 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 750 String groupBy = null; 751 String limit = null; // Not currently implemented 752 String instancesTimezone; 753 754 final int match = sUriMatcher.match(uri); 755 switch (match) { 756 case SYNCSTATE: 757 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 758 sortOrder); 759 760 case EVENTS: 761 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 762 qb.setProjectionMap(sEventsProjectionMap); 763 selection = appendAccountFromParameterToSelection(selection, uri); 764 selection = appendLastSyncedColumnToSelection(selection, uri); 765 break; 766 case EVENTS_ID: 767 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 768 qb.setProjectionMap(sEventsProjectionMap); 769 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 770 qb.appendWhere(SQL_WHERE_ID); 771 break; 772 773 case EVENT_ENTITIES: 774 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 775 qb.setProjectionMap(sEventEntitiesProjectionMap); 776 selection = appendAccountFromParameterToSelection(selection, uri); 777 selection = appendLastSyncedColumnToSelection(selection, uri); 778 break; 779 case EVENT_ENTITIES_ID: 780 qb.setTables(CalendarDatabaseHelper.Views.EVENTS); 781 qb.setProjectionMap(sEventEntitiesProjectionMap); 782 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 783 qb.appendWhere(SQL_WHERE_ID); 784 break; 785 786 case CALENDARS: 787 case CALENDAR_ENTITIES: 788 qb.setTables(Tables.CALENDARS); 789 selection = appendAccountFromParameterToSelection(selection, uri); 790 break; 791 case CALENDARS_ID: 792 case CALENDAR_ENTITIES_ID: 793 qb.setTables(Tables.CALENDARS); 794 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 795 qb.appendWhere(SQL_WHERE_ID); 796 break; 797 case INSTANCES: 798 case INSTANCES_BY_DAY: 799 long begin; 800 long end; 801 try { 802 begin = Long.valueOf(uri.getPathSegments().get(2)); 803 } catch (NumberFormatException nfe) { 804 throw new IllegalArgumentException("Cannot parse begin " 805 + uri.getPathSegments().get(2)); 806 } 807 try { 808 end = Long.valueOf(uri.getPathSegments().get(3)); 809 } catch (NumberFormatException nfe) { 810 throw new IllegalArgumentException("Cannot parse end " 811 + uri.getPathSegments().get(3)); 812 } 813 instancesTimezone = mCalendarCache.readTimezoneInstances(); 814 return handleInstanceQuery(qb, begin, end, projection, selection, selectionArgs, 815 sortOrder, match == INSTANCES_BY_DAY, false /* don't force an expansion */, 816 instancesTimezone, isHomeTimezone()); 817 case INSTANCES_SEARCH: 818 case INSTANCES_SEARCH_BY_DAY: 819 try { 820 begin = Long.valueOf(uri.getPathSegments().get(2)); 821 } catch (NumberFormatException nfe) { 822 throw new IllegalArgumentException("Cannot parse begin " 823 + uri.getPathSegments().get(2)); 824 } 825 try { 826 end = Long.valueOf(uri.getPathSegments().get(3)); 827 } catch (NumberFormatException nfe) { 828 throw new IllegalArgumentException("Cannot parse end " 829 + uri.getPathSegments().get(3)); 830 } 831 instancesTimezone = mCalendarCache.readTimezoneInstances(); 832 // this is already decoded 833 String query = uri.getPathSegments().get(4); 834 return handleInstanceSearchQuery(qb, begin, end, query, projection, selection, 835 selectionArgs, sortOrder, match == INSTANCES_SEARCH_BY_DAY, 836 instancesTimezone, isHomeTimezone()); 837 case EVENT_DAYS: 838 int startDay; 839 int endDay; 840 try { 841 startDay = Integer.valueOf(uri.getPathSegments().get(2)); 842 } catch (NumberFormatException nfe) { 843 throw new IllegalArgumentException("Cannot parse start day " 844 + uri.getPathSegments().get(2)); 845 } 846 try { 847 endDay = Integer.valueOf(uri.getPathSegments().get(3)); 848 } catch (NumberFormatException nfe) { 849 throw new IllegalArgumentException("Cannot parse end day " 850 + uri.getPathSegments().get(3)); 851 } 852 instancesTimezone = mCalendarCache.readTimezoneInstances(); 853 return handleEventDayQuery(qb, startDay, endDay, projection, selection, 854 instancesTimezone, isHomeTimezone()); 855 case ATTENDEES: 856 qb.setTables(Tables.ATTENDEES + ", " + Tables.EVENTS + ", " + Tables.CALENDARS); 857 qb.setProjectionMap(sAttendeesProjectionMap); 858 qb.appendWhere(Tables.EVENTS + "." + Events._ID + "=" 859 + Tables.ATTENDEES + "." + Attendees.EVENT_ID); 860 break; 861 case ATTENDEES_ID: 862 qb.setTables(Tables.ATTENDEES + ", " + Tables.EVENTS + ", " + Tables.CALENDARS); 863 qb.setProjectionMap(sAttendeesProjectionMap); 864 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 865 qb.appendWhere(SQL_WHERE_ATTENDEES_ID); 866 break; 867 case REMINDERS: 868 qb.setTables(Tables.REMINDERS); 869 break; 870 case REMINDERS_ID: 871 qb.setTables(Tables.REMINDERS + ", " + Tables.EVENTS + ", " + Tables.CALENDARS); 872 qb.setProjectionMap(sRemindersProjectionMap); 873 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 874 qb.appendWhere(SQL_WHERE_REMINDERS_ID); 875 break; 876 case CALENDAR_ALERTS: 877 qb.setTables(Tables.CALENDAR_ALERTS + ", " + CalendarDatabaseHelper.Views.EVENTS); 878 qb.setProjectionMap(sCalendarAlertsProjectionMap); 879 qb.appendWhere(SQL_WHERE_CALENDAR_ALERT); 880 break; 881 case CALENDAR_ALERTS_BY_INSTANCE: 882 qb.setTables(Tables.CALENDAR_ALERTS + ", " + CalendarDatabaseHelper.Views.EVENTS); 883 qb.setProjectionMap(sCalendarAlertsProjectionMap); 884 qb.appendWhere(SQL_WHERE_CALENDAR_ALERT); 885 groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN; 886 break; 887 case CALENDAR_ALERTS_ID: 888 qb.setTables(Tables.CALENDAR_ALERTS + ", " + CalendarDatabaseHelper.Views.EVENTS); 889 qb.setProjectionMap(sCalendarAlertsProjectionMap); 890 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 891 qb.appendWhere(SQL_WHERE_CALENDAR_ALERT_ID); 892 break; 893 case EXTENDED_PROPERTIES: 894 qb.setTables(Tables.EXTENDED_PROPERTIES); 895 break; 896 case EXTENDED_PROPERTIES_ID: 897 qb.setTables(Tables.EXTENDED_PROPERTIES); 898 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1)); 899 qb.appendWhere(SQL_WHERE_EXTENDED_PROPERTIES_ID); 900 break; 901 case PROVIDER_PROPERTIES: 902 qb.setTables(Tables.CALENDAR_CACHE); 903 qb.setProjectionMap(sCalendarCacheProjectionMap); 904 break; 905 default: 906 throw new IllegalArgumentException("Unknown URL " + uri); 907 } 908 909 // run the query 910 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 911 } 912 913 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 914 String selection, String[] selectionArgs, String sortOrder, String groupBy, 915 String limit) { 916 917 if (projection != null && projection.length == 1 918 && BaseColumns._COUNT.equals(projection[0])) { 919 qb.setProjectionMap(sCountProjectionMap); 920 } 921 922 if (Log.isLoggable(TAG, Log.VERBOSE)) { 923 Log.v(TAG, "query sql - projection: " + Arrays.toString(projection) + 924 " selection: " + selection + 925 " selectionArgs: " + Arrays.toString(selectionArgs) + 926 " sortOrder: " + sortOrder + 927 " groupBy: " + groupBy + 928 " limit: " + limit); 929 } 930 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 931 sortOrder, limit); 932 if (c != null) { 933 // TODO: is this the right notification Uri? 934 c.setNotificationUri(mContentResolver, CalendarContract.Events.CONTENT_URI); 935 } 936 return c; 937 } 938 939 /* 940 * Fills the Instances table, if necessary, for the given range and then 941 * queries the Instances table. 942 * 943 * @param qb The query 944 * @param rangeBegin start of range (Julian days or ms) 945 * @param rangeEnd end of range (Julian days or ms) 946 * @param projection The projection 947 * @param selection The selection 948 * @param sort How to sort 949 * @param searchByDay if true, range is in Julian days, if false, range is in ms 950 * @param forceExpansion force the Instance deletion and expansion if set to true 951 * @param instancesTimezone timezone we need to use for computing the instances 952 * @param isHomeTimezone if true, we are in the "home" timezone 953 * @return 954 */ 955 private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, 956 long rangeEnd, String[] projection, String selection, String[] selectionArgs, 957 String sort, boolean searchByDay, boolean forceExpansion, 958 String instancesTimezone, boolean isHomeTimezone) { 959 960 qb.setTables(INSTANCE_QUERY_TABLES); 961 qb.setProjectionMap(sInstancesProjectionMap); 962 if (searchByDay) { 963 // Convert the first and last Julian day range to a range that uses 964 // UTC milliseconds. 965 Time time = new Time(instancesTimezone); 966 long beginMs = time.setJulianDay((int) rangeBegin); 967 // We add one to lastDay because the time is set to 12am on the given 968 // Julian day and we want to include all the events on the last day. 969 long endMs = time.setJulianDay((int) rangeEnd + 1); 970 // will lock the database. 971 acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */, 972 forceExpansion, instancesTimezone, isHomeTimezone); 973 qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN_DAY); 974 } else { 975 // will lock the database. 976 acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */, 977 forceExpansion, instancesTimezone, isHomeTimezone); 978 qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN); 979 } 980 981 String[] newSelectionArgs = new String[] {String.valueOf(rangeEnd), 982 String.valueOf(rangeBegin)}; 983 if (selectionArgs == null) { 984 selectionArgs = newSelectionArgs; 985 } else { 986 // The appendWhere pieces get added first, so put the 987 // newSelectionArgs first. 988 selectionArgs = combine(newSelectionArgs, selectionArgs); 989 } 990 return qb.query(mDb, projection, selection, selectionArgs, null /* groupBy */, 991 null /* having */, sort); 992 } 993 994 /** 995 * Combine a set of arrays in the order they are passed in. All arrays must 996 * be of the same type. 997 */ 998 private static <T> T[] combine(T[]... arrays) { 999 if (arrays.length == 0) { 1000 throw new IllegalArgumentException("Must supply at least 1 array to combine"); 1001 } 1002 1003 int totalSize = 0; 1004 for (T[] array : arrays) { 1005 totalSize += array.length; 1006 } 1007 1008 T[] finalArray = (T[]) (Array.newInstance(arrays[0].getClass().getComponentType(), 1009 totalSize)); 1010 1011 int currentPos = 0; 1012 for (T[] array : arrays) { 1013 int length = array.length; 1014 System.arraycopy(array, 0, finalArray, currentPos, length); 1015 currentPos += array.length; 1016 } 1017 return finalArray; 1018 } 1019 1020 /** 1021 * Escape any special characters in the search token 1022 * @param token the token to escape 1023 * @return the escaped token 1024 */ 1025 @VisibleForTesting 1026 String escapeSearchToken(String token) { 1027 Matcher matcher = SEARCH_ESCAPE_PATTERN.matcher(token); 1028 return matcher.replaceAll(SEARCH_ESCAPE_CHAR + "$1"); 1029 } 1030 1031 /** 1032 * Splits the search query into individual search tokens based on whitespace 1033 * and punctuation. Leaves both single quoted and double quoted strings 1034 * intact. 1035 * 1036 * @param query the search query 1037 * @return an array of tokens from the search query 1038 */ 1039 @VisibleForTesting 1040 String[] tokenizeSearchQuery(String query) { 1041 List<String> matchList = new ArrayList<String>(); 1042 Matcher matcher = SEARCH_TOKEN_PATTERN.matcher(query); 1043 String token; 1044 while (matcher.find()) { 1045 if (matcher.group(1) != null) { 1046 // double quoted string 1047 token = matcher.group(1); 1048 } else { 1049 // unquoted token 1050 token = matcher.group(); 1051 } 1052 matchList.add(escapeSearchToken(token)); 1053 } 1054 return matchList.toArray(new String[matchList.size()]); 1055 } 1056 1057 /** 1058 * In order to support what most people would consider a reasonable 1059 * search behavior, we have to do some interesting things here. We 1060 * assume that when a user searches for something like "lunch meeting", 1061 * they really want any event that matches both "lunch" and "meeting", 1062 * not events that match the string "lunch meeting" itself. In order to 1063 * do this across multiple columns, we have to construct a WHERE clause 1064 * that looks like: 1065 * <code> 1066 * WHERE (title LIKE "%lunch%" 1067 * OR description LIKE "%lunch%" 1068 * OR eventLocation LIKE "%lunch%") 1069 * AND (title LIKE "%meeting%" 1070 * OR description LIKE "%meeting%" 1071 * OR eventLocation LIKE "%meeting%") 1072 * </code> 1073 * This "product of clauses" is a bit ugly, but produced a fairly good 1074 * approximation of full-text search across multiple columns. 1075 */ 1076 @VisibleForTesting 1077 String constructSearchWhere(String[] tokens) { 1078 if (tokens.length == 0) { 1079 return ""; 1080 } 1081 StringBuilder sb = new StringBuilder(); 1082 String column, token; 1083 for (int j = 0; j < tokens.length; j++) { 1084 sb.append("("); 1085 for (int i = 0; i < SEARCH_COLUMNS.length; i++) { 1086 sb.append(SEARCH_COLUMNS[i]); 1087 sb.append(" LIKE ? ESCAPE \""); 1088 sb.append(SEARCH_ESCAPE_CHAR); 1089 sb.append("\" "); 1090 if (i < SEARCH_COLUMNS.length - 1) { 1091 sb.append("OR "); 1092 } 1093 } 1094 sb.append(")"); 1095 if (j < tokens.length - 1) { 1096 sb.append(" AND "); 1097 } 1098 } 1099 return sb.toString(); 1100 } 1101 1102 @VisibleForTesting 1103 String[] constructSearchArgs(String[] tokens, long rangeBegin, long rangeEnd) { 1104 int numCols = SEARCH_COLUMNS.length; 1105 int numArgs = tokens.length * numCols + 2; 1106 // the additional two elements here are for begin/end time 1107 String[] selectionArgs = new String[numArgs]; 1108 selectionArgs[0] = String.valueOf(rangeEnd); 1109 selectionArgs[1] = String.valueOf(rangeBegin); 1110 for (int j = 0; j < tokens.length; j++) { 1111 int start = 2 + numCols * j; 1112 for (int i = start; i < start + numCols; i++) { 1113 selectionArgs[i] = "%" + tokens[j] + "%"; 1114 } 1115 } 1116 return selectionArgs; 1117 } 1118 1119 private Cursor handleInstanceSearchQuery(SQLiteQueryBuilder qb, 1120 long rangeBegin, long rangeEnd, String query, String[] projection, 1121 String selection, String[] selectionArgs, String sort, boolean searchByDay, 1122 String instancesTimezone, boolean isHomeTimezone) { 1123 qb.setTables(INSTANCE_SEARCH_QUERY_TABLES); 1124 qb.setProjectionMap(sInstancesProjectionMap); 1125 1126 String[] tokens = tokenizeSearchQuery(query); 1127 String[] newSelectionArgs = constructSearchArgs(tokens, rangeBegin, rangeEnd); 1128 if (selectionArgs == null) { 1129 selectionArgs = newSelectionArgs; 1130 } else { 1131 // The appendWhere pieces get added first, so put the 1132 // newSelectionArgs first. 1133 selectionArgs = combine(newSelectionArgs, selectionArgs); 1134 } 1135 // we pass this in as a HAVING instead of a WHERE so the filtering 1136 // happens after the grouping 1137 String searchWhere = constructSearchWhere(tokens); 1138 1139 if (searchByDay) { 1140 // Convert the first and last Julian day range to a range that uses 1141 // UTC milliseconds. 1142 Time time = new Time(instancesTimezone); 1143 long beginMs = time.setJulianDay((int) rangeBegin); 1144 // We add one to lastDay because the time is set to 12am on the given 1145 // Julian day and we want to include all the events on the last day. 1146 long endMs = time.setJulianDay((int) rangeEnd + 1); 1147 // will lock the database. 1148 // we expand the instances here because we might be searching over 1149 // a range where instance expansion has not occurred yet 1150 acquireInstanceRange(beginMs, endMs, 1151 true /* use minimum expansion window */, 1152 false /* do not force Instances deletion and expansion */, 1153 instancesTimezone, 1154 isHomeTimezone 1155 ); 1156 qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN_DAY); 1157 } else { 1158 // will lock the database. 1159 // we expand the instances here because we might be searching over 1160 // a range where instance expansion has not occurred yet 1161 acquireInstanceRange(rangeBegin, rangeEnd, 1162 true /* use minimum expansion window */, 1163 false /* do not force Instances deletion and expansion */, 1164 instancesTimezone, 1165 isHomeTimezone 1166 ); 1167 qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN); 1168 } 1169 1170 return qb.query(mDb, projection, selection, selectionArgs, 1171 Instances._ID /* groupBy */, searchWhere /* having */, sort); 1172 } 1173 1174 private Cursor handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end, 1175 String[] projection, String selection, String instancesTimezone, 1176 boolean isHomeTimezone) { 1177 qb.setTables(INSTANCE_QUERY_TABLES); 1178 qb.setProjectionMap(sInstancesProjectionMap); 1179 // Convert the first and last Julian day range to a range that uses 1180 // UTC milliseconds. 1181 Time time = new Time(instancesTimezone); 1182 long beginMs = time.setJulianDay(begin); 1183 // We add one to lastDay because the time is set to 12am on the given 1184 // Julian day and we want to include all the events on the last day. 1185 long endMs = time.setJulianDay(end + 1); 1186 1187 acquireInstanceRange(beginMs, endMs, true, 1188 false /* do not force Instances expansion */, instancesTimezone, isHomeTimezone); 1189 qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN_DAY); 1190 String selectionArgs[] = new String[] {String.valueOf(end), String.valueOf(begin)}; 1191 1192 return qb.query(mDb, projection, selection, selectionArgs, 1193 Instances.START_DAY /* groupBy */, null /* having */, null); 1194 } 1195 1196 /** 1197 * Ensure that the date range given has all elements in the instance 1198 * table. Acquires the database lock and calls 1199 * {@link #acquireInstanceRangeLocked(long, long, boolean, boolean, String, boolean)}. 1200 * 1201 * @param begin start of range (ms) 1202 * @param end end of range (ms) 1203 * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN 1204 * @param forceExpansion force the Instance deletion and expansion if set to true 1205 * @param instancesTimezone timezone we need to use for computing the instances 1206 * @param isHomeTimezone if true, we are in the "home" timezone 1207 */ 1208 private void acquireInstanceRange(final long begin, final long end, 1209 final boolean useMinimumExpansionWindow, final boolean forceExpansion, 1210 final String instancesTimezone, final boolean isHomeTimezone) { 1211 mDb.beginTransaction(); 1212 try { 1213 acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow, 1214 forceExpansion, instancesTimezone, isHomeTimezone); 1215 mDb.setTransactionSuccessful(); 1216 } finally { 1217 mDb.endTransaction(); 1218 } 1219 } 1220 1221 /** 1222 * Ensure that the date range given has all elements in the instance 1223 * table. The database lock must be held when calling this method. 1224 * 1225 * @param begin start of range (ms) 1226 * @param end end of range (ms) 1227 * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN 1228 * @param forceExpansion force the Instance deletion and expansion if set to true 1229 * @param instancesTimezone timezone we need to use for computing the instances 1230 * @param isHomeTimezone if true, we are in the "home" timezone 1231 */ 1232 void acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow, 1233 boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone) { 1234 long expandBegin = begin; 1235 long expandEnd = end; 1236 1237 if (instancesTimezone == null) { 1238 Log.e(TAG, "Cannot run acquireInstanceRangeLocked() because instancesTimezone is null"); 1239 return; 1240 } 1241 1242 if (useMinimumExpansionWindow) { 1243 // if we end up having to expand events into the instances table, expand 1244 // events for a minimal amount of time, so we do not have to perform 1245 // expansions frequently. 1246 long span = end - begin; 1247 if (span < MINIMUM_EXPANSION_SPAN) { 1248 long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2; 1249 expandBegin -= additionalRange; 1250 expandEnd += additionalRange; 1251 } 1252 } 1253 1254 // Check if the timezone has changed. 1255 // We do this check here because the database is locked and we can 1256 // safely delete all the entries in the Instances table. 1257 MetaData.Fields fields = mMetaData.getFieldsLocked(); 1258 long maxInstance = fields.maxInstance; 1259 long minInstance = fields.minInstance; 1260 boolean timezoneChanged; 1261 if (isHomeTimezone) { 1262 String previousTimezone = mCalendarCache.readTimezoneInstancesPrevious(); 1263 timezoneChanged = !instancesTimezone.equals(previousTimezone); 1264 } else { 1265 String localTimezone = TimeZone.getDefault().getID(); 1266 timezoneChanged = !instancesTimezone.equals(localTimezone); 1267 // if we're in auto make sure we are using the device time zone 1268 if (timezoneChanged) { 1269 instancesTimezone = localTimezone; 1270 } 1271 } 1272 // if "home", then timezoneChanged only if current != previous 1273 // if "auto", then timezoneChanged, if !instancesTimezone.equals(localTimezone); 1274 if (maxInstance == 0 || timezoneChanged || forceExpansion) { 1275 // Empty the Instances table and expand from scratch. 1276 mDb.execSQL("DELETE FROM " + Tables.INSTANCES + ";"); 1277 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1278 Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances," 1279 + " timezone changed: " + timezoneChanged); 1280 } 1281 mInstancesHelper.expandInstanceRangeLocked(expandBegin, expandEnd, instancesTimezone); 1282 1283 mMetaData.writeLocked(instancesTimezone, expandBegin, expandEnd); 1284 1285 String timezoneType = mCalendarCache.readTimezoneType(); 1286 // This may cause some double writes but guarantees the time zone in 1287 // the db and the time zone the instances are in is the same, which 1288 // future changes may affect. 1289 mCalendarCache.writeTimezoneInstances(instancesTimezone); 1290 1291 // If we're in auto check if we need to fix the previous tz value 1292 if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) { 1293 String prevTZ = mCalendarCache.readTimezoneInstancesPrevious(); 1294 if (TextUtils.equals(TIMEZONE_GMT, prevTZ)) { 1295 mCalendarCache.writeTimezoneInstancesPrevious(instancesTimezone); 1296 } 1297 } 1298 return; 1299 } 1300 1301 // If the desired range [begin, end] has already been 1302 // expanded, then simply return. The range is inclusive, that is, 1303 // events that touch either endpoint are included in the expansion. 1304 // This means that a zero-duration event that starts and ends at 1305 // the endpoint will be included. 1306 // We use [begin, end] here and not [expandBegin, expandEnd] for 1307 // checking the range because a common case is for the client to 1308 // request successive days or weeks, for example. If we checked 1309 // that the expanded range [expandBegin, expandEnd] then we would 1310 // always be expanding because there would always be one more day 1311 // or week that hasn't been expanded. 1312 if ((begin >= minInstance) && (end <= maxInstance)) { 1313 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1314 Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd 1315 + ") falls within previously expanded range."); 1316 } 1317 return; 1318 } 1319 1320 // If the requested begin point has not been expanded, then include 1321 // more events than requested in the expansion (use "expandBegin"). 1322 if (begin < minInstance) { 1323 mInstancesHelper.expandInstanceRangeLocked(expandBegin, minInstance, instancesTimezone); 1324 minInstance = expandBegin; 1325 } 1326 1327 // If the requested end point has not been expanded, then include 1328 // more events than requested in the expansion (use "expandEnd"). 1329 if (end > maxInstance) { 1330 mInstancesHelper.expandInstanceRangeLocked(maxInstance, expandEnd, instancesTimezone); 1331 maxInstance = expandEnd; 1332 } 1333 1334 // Update the bounds on the Instances table. 1335 mMetaData.writeLocked(instancesTimezone, minInstance, maxInstance); 1336 } 1337 1338 @Override 1339 public String getType(Uri url) { 1340 int match = sUriMatcher.match(url); 1341 switch (match) { 1342 case EVENTS: 1343 return "vnd.android.cursor.dir/event"; 1344 case EVENTS_ID: 1345 return "vnd.android.cursor.item/event"; 1346 case REMINDERS: 1347 return "vnd.android.cursor.dir/reminder"; 1348 case REMINDERS_ID: 1349 return "vnd.android.cursor.item/reminder"; 1350 case CALENDAR_ALERTS: 1351 return "vnd.android.cursor.dir/calendar-alert"; 1352 case CALENDAR_ALERTS_BY_INSTANCE: 1353 return "vnd.android.cursor.dir/calendar-alert-by-instance"; 1354 case CALENDAR_ALERTS_ID: 1355 return "vnd.android.cursor.item/calendar-alert"; 1356 case INSTANCES: 1357 case INSTANCES_BY_DAY: 1358 case EVENT_DAYS: 1359 return "vnd.android.cursor.dir/event-instance"; 1360 case TIME: 1361 return "time/epoch"; 1362 case PROVIDER_PROPERTIES: 1363 return "vnd.android.cursor.dir/property"; 1364 default: 1365 throw new IllegalArgumentException("Unknown URL " + url); 1366 } 1367 } 1368 1369 /** 1370 * Determines if the event is recurrent, based on the provided values. 1371 */ 1372 public static boolean isRecurrenceEvent(String rrule, String rdate, String originalId, 1373 String originalSyncId) { 1374 return (!TextUtils.isEmpty(rrule) || 1375 !TextUtils.isEmpty(rdate) || 1376 !TextUtils.isEmpty(originalId) || 1377 !TextUtils.isEmpty(originalSyncId)); 1378 } 1379 1380 /** 1381 * Takes an event and corrects the hrs, mins, secs if it is an allDay event. 1382 * 1383 * AllDay events should have hrs, mins, secs set to zero. This checks if this is true and 1384 * corrects the fields DTSTART, DTEND, and DURATION if necessary. Also checks to ensure that 1385 * either both DTSTART and DTEND or DTSTART and DURATION are set for each event. 1386 * 1387 * @param updatedValues The values to check and correct 1388 * @return Returns true if a correction was necessary, false otherwise 1389 */ 1390 private boolean fixAllDayTime(Uri uri, ContentValues updatedValues) { 1391 boolean neededCorrection = false; 1392 if (updatedValues.containsKey(Events.ALL_DAY) 1393 && updatedValues.getAsInteger(Events.ALL_DAY).intValue() == 1) { 1394 Long dtstart = updatedValues.getAsLong(Events.DTSTART); 1395 Long dtend = updatedValues.getAsLong(Events.DTEND); 1396 String duration = updatedValues.getAsString(Events.DURATION); 1397 Time time = new Time(); 1398 Cursor currentTimesCursor = null; 1399 String tempValue; 1400 // If a complete set of time fields doesn't exist query the db for them. A complete set 1401 // is dtstart and dtend for non-recurring events or dtstart and duration for recurring 1402 // events. 1403 if(dtstart == null || (dtend == null && duration == null)) { 1404 // Make sure we have an id to search for, if not this is probably a new event 1405 if (uri.getPathSegments().size() == 2) { 1406 currentTimesCursor = query(uri, 1407 ALLDAY_TIME_PROJECTION, 1408 null /* selection */, 1409 null /* selectionArgs */, 1410 null /* sort */); 1411 if (currentTimesCursor != null) { 1412 if (!currentTimesCursor.moveToFirst() || 1413 currentTimesCursor.getCount() != 1) { 1414 // Either this is a new event or the query is too general to get data 1415 // from the db. In either case don't try to use the query and catch 1416 // errors when trying to update the time fields. 1417 currentTimesCursor.close(); 1418 currentTimesCursor = null; 1419 } 1420 } 1421 } 1422 } 1423 1424 // Ensure dtstart exists for this event (always required) and set so h,m,s are 0 if 1425 // necessary. 1426 // TODO Move this somewhere to check all events, not just allDay events. 1427 if (dtstart == null) { 1428 if (currentTimesCursor != null) { 1429 // getLong returns 0 for empty fields, we'd like to know if a field is empty 1430 // so getString is used instead. 1431 tempValue = currentTimesCursor.getString(ALLDAY_DTSTART_INDEX); 1432 try { 1433 dtstart = Long.valueOf(tempValue); 1434 } catch (NumberFormatException e) { 1435 currentTimesCursor.close(); 1436 throw new IllegalArgumentException("Event has no DTSTART field, the db " + 1437 "may be damaged. Set DTSTART for this event to fix."); 1438 } 1439 } else { 1440 throw new IllegalArgumentException("DTSTART cannot be empty for new events."); 1441 } 1442 } 1443 time.clear(Time.TIMEZONE_UTC); 1444 time.set(dtstart.longValue()); 1445 if (time.hour != 0 || time.minute != 0 || time.second != 0) { 1446 time.hour = 0; 1447 time.minute = 0; 1448 time.second = 0; 1449 updatedValues.put(Events.DTSTART, time.toMillis(true)); 1450 neededCorrection = true; 1451 } 1452 1453 // If dtend exists for this event make sure it's h,m,s are 0. 1454 if (dtend == null && currentTimesCursor != null) { 1455 // getLong returns 0 for empty fields. We'd like to know if a field is empty 1456 // so getString is used instead. 1457 tempValue = currentTimesCursor.getString(ALLDAY_DTEND_INDEX); 1458 try { 1459 dtend = Long.valueOf(tempValue); 1460 } catch (NumberFormatException e) { 1461 dtend = null; 1462 } 1463 } 1464 if (dtend != null) { 1465 time.clear(Time.TIMEZONE_UTC); 1466 time.set(dtend.longValue()); 1467 if (time.hour != 0 || time.minute != 0 || time.second != 0) { 1468 time.hour = 0; 1469 time.minute = 0; 1470 time.second = 0; 1471 dtend = time.toMillis(true); 1472 updatedValues.put(Events.DTEND, dtend); 1473 neededCorrection = true; 1474 } 1475 } 1476 1477 if (currentTimesCursor != null) { 1478 if (duration == null) { 1479 duration = currentTimesCursor.getString(ALLDAY_DURATION_INDEX); 1480 } 1481 currentTimesCursor.close(); 1482 } 1483 1484 if (duration != null) { 1485 int len = duration.length(); 1486 /* duration is stored as either "P<seconds>S" or "P<days>D". This checks if it's 1487 * in the seconds format, and if so converts it to days. 1488 */ 1489 if (len == 0) { 1490 duration = null; 1491 } else if (duration.charAt(0) == 'P' && 1492 duration.charAt(len - 1) == 'S') { 1493 int seconds = Integer.parseInt(duration.substring(1, len - 1)); 1494 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS; 1495 duration = "P" + days + "D"; 1496 updatedValues.put(Events.DURATION, duration); 1497 neededCorrection = true; 1498 } 1499 } 1500 1501 if (duration == null && dtend == null) { 1502 throw new IllegalArgumentException("DTEND and DURATION cannot both be null for " + 1503 "an event."); 1504 } 1505 } 1506 return neededCorrection; 1507 } 1508 1509 1510 /** 1511 * Determines whether the strings in the set name columns that may be overridden 1512 * when creating a recurring event exception. 1513 * <p> 1514 * This uses a white list because it screens out unknown columns and is a bit safer to 1515 * maintain than a black list. 1516 */ 1517 private void checkAllowedInException(Set<String> keys) { 1518 for (String str : keys) { 1519 if (!ALLOWED_IN_EXCEPTION.contains(str.intern())) { 1520 throw new IllegalArgumentException("Exceptions can't overwrite " + str); 1521 } 1522 } 1523 } 1524 1525 /** 1526 * Splits a recurrent event at a specified instance. This is useful when modifying "this 1527 * and all future events". 1528 *<p> 1529 * If the recurrence rule has a COUNT specified, we need to split that at the point of the 1530 * exception. If the exception is instance N (0-based), the original COUNT is reduced 1531 * to N, and the exception's COUNT is set to (COUNT - N). 1532 *<p> 1533 * If the recurrence doesn't have a COUNT, we need to update or introduce an UNTIL value, 1534 * so that the original recurrence will end just before the exception instance. (Note 1535 * that UNTIL dates are inclusive.) 1536 *<p> 1537 * This should not be used to update the first instance ("update all events" action). 1538 * 1539 * @param values The original event values; must include EVENT_TIMEZONE and DTSTART. 1540 * The RRULE value may be modified (with the expectation that this will propagate 1541 * into the exception event). 1542 * @param endTimeMillis The time before which the event must end (i.e. the start time of the 1543 * exception event instance). 1544 * @return Values to apply to the original event. 1545 */ 1546 private static ContentValues setRecurrenceEnd(ContentValues values, long endTimeMillis) { 1547 boolean origAllDay = values.getAsBoolean(Events.ALL_DAY); 1548 String origRrule = values.getAsString(Events.RRULE); 1549 1550 EventRecurrence origRecurrence = new EventRecurrence(); 1551 origRecurrence.parse(origRrule); 1552 1553 // Get the start time of the first instance in the original recurrence. 1554 long startTimeMillis = values.getAsLong(Events.DTSTART); 1555 Time dtstart = new Time(); 1556 dtstart.timezone = values.getAsString(Events.EVENT_TIMEZONE); 1557 dtstart.set(startTimeMillis); 1558 1559 ContentValues updateValues = new ContentValues(); 1560 1561 if (origRecurrence.count > 0) { 1562 /* 1563 * Generate the full set of instances for this recurrence, from the first to the 1564 * one just before endTimeMillis. The list should never be empty, because this method 1565 * should not be called for the first instance. All we're really interested in is 1566 * the *number* of instances found. 1567 */ 1568 RecurrenceSet recurSet = new RecurrenceSet(values); 1569 RecurrenceProcessor recurProc = new RecurrenceProcessor(); 1570 long[] recurrences; 1571 try { 1572 recurrences = recurProc.expand(dtstart, recurSet, startTimeMillis, endTimeMillis); 1573 } catch (DateException de) { 1574 throw new RuntimeException(de); 1575 } 1576 1577 if (recurrences.length == 0) { 1578 throw new RuntimeException("can't use this method on first instance"); 1579 } 1580 1581 EventRecurrence excepRecurrence = new EventRecurrence(); 1582 excepRecurrence.parse(origRrule); // TODO: add/use a copy constructor to EventRecurrence 1583 excepRecurrence.count -= recurrences.length; 1584 values.put(Events.RRULE, excepRecurrence.toString()); 1585 1586 origRecurrence.count = recurrences.length; 1587 1588 } else { 1589 Time untilTime = new Time(); 1590 1591 // The "until" time must be in UTC time in order for Google calendar 1592 // to display it properly. For all-day events, the "until" time string 1593 // must include just the date field, and not the time field. The 1594 // repeating events repeat up to and including the "until" time. 1595 untilTime.timezone = Time.TIMEZONE_UTC; 1596 1597 // Subtract one second from the exception begin time to get the "until" time. 1598 untilTime.set(endTimeMillis - 1000); // subtract one second (1000 millis) 1599 if (origAllDay) { 1600 untilTime.hour = untilTime.minute = untilTime.second = 0; 1601 untilTime.allDay = true; 1602 untilTime.normalize(false); 1603 1604 // This should no longer be necessary -- DTSTART should already be in the correct 1605 // format for an all-day event. 1606 dtstart.hour = dtstart.minute = dtstart.second = 0; 1607 dtstart.allDay = true; 1608 dtstart.timezone = Time.TIMEZONE_UTC; 1609 } 1610 origRecurrence.until = untilTime.format2445(); 1611 } 1612 1613 updateValues.put(Events.RRULE, origRecurrence.toString()); 1614 updateValues.put(Events.DTSTART, dtstart.normalize(true)); 1615 return updateValues; 1616 } 1617 1618 /** 1619 * Handles insertion of an exception to a recurring event. 1620 * <p> 1621 * There are two modes, selected based on the presence of "rrule" in modValues: 1622 * <ol> 1623 * <li> Create a single instance exception ("modify current event only"). 1624 * <li> Cap the original event, and create a new recurring event ("modify this and all 1625 * future events"). 1626 * </ol> 1627 * This may be used for "modify all instances of the event" by simply selecting the 1628 * very first instance as the exception target. In that case, the ID of the "new" 1629 * exception event will be the same as the originalEventId. 1630 * 1631 * @param originalEventId The _id of the event to be modified 1632 * @param modValues Event columns to update 1633 * @param callerIsSyncAdapter Set if the content provider client is the sync adapter 1634 * @return the ID of the new "exception" event, or -1 on failure 1635 */ 1636 private long handleInsertException(long originalEventId, ContentValues modValues, 1637 boolean callerIsSyncAdapter) { 1638 if (DEBUG_EXCEPTION) { 1639 Log.i(TAG, "RE: values: " + modValues.toString()); 1640 } 1641 1642 // Make sure they have specified an instance via originalInstanceTime. 1643 Long originalInstanceTime = modValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1644 if (originalInstanceTime == null) { 1645 throw new IllegalArgumentException("Exceptions must specify " + 1646 Events.ORIGINAL_INSTANCE_TIME); 1647 } 1648 1649 // Check for attempts to override values that shouldn't be touched. 1650 checkAllowedInException(modValues.keySet()); 1651 1652 // If this isn't the sync adapter, set the "dirty" flag in any Event we modify. 1653 if (!callerIsSyncAdapter) { 1654 modValues.put(Events.DIRTY, true); 1655 } 1656 1657 // Wrap all database accesses in a transaction. 1658 mDb.beginTransaction(); 1659 Cursor cursor = null; 1660 try { 1661 // TODO: verify that there's an instance corresponding to the specified time 1662 // (does this matter? it's weird, but not fatal?) 1663 1664 // Grab the full set of columns for this event. 1665 cursor = mDb.query(Tables.EVENTS, null /* columns */, 1666 SQL_WHERE_ID, new String[] { String.valueOf(originalEventId) }, 1667 null /* groupBy */, null /* having */, null /* sortOrder */); 1668 if (cursor.getCount() != 1) { 1669 Log.e(TAG, "Original event ID " + originalEventId + " lookup failed (count is " + 1670 cursor.getCount() + ")"); 1671 return -1; 1672 } 1673 //DatabaseUtils.dumpCursor(cursor); 1674 1675 /* 1676 * Verify that the original event is in fact a recurring event by checking for the 1677 * presence of an RRULE. If it's there, we assume that the event is otherwise 1678 * properly constructed (e.g. no DTEND). 1679 */ 1680 cursor.moveToFirst(); 1681 int rruleCol = cursor.getColumnIndex(Events.RRULE); 1682 if (TextUtils.isEmpty(cursor.getString(rruleCol))) { 1683 Log.e(TAG, "Original event has no rrule"); 1684 return -1; 1685 } 1686 if (DEBUG_EXCEPTION) { 1687 Log.d(TAG, "RE: old RRULE is " + cursor.getString(rruleCol)); 1688 } 1689 1690 // Verify that the original event is not itself a (single-instance) exception. 1691 int originalIdCol = cursor.getColumnIndex(Events.ORIGINAL_ID); 1692 if (!TextUtils.isEmpty(cursor.getString(originalIdCol))) { 1693 Log.e(TAG, "Original event is an exception"); 1694 return -1; 1695 } 1696 1697 boolean createSingleException = TextUtils.isEmpty(modValues.getAsString(Events.RRULE)); 1698 1699 // TODO: check for the presence of an existing exception on this event+instance? 1700 // The caller should be modifying that, not creating another exception. 1701 // (Alternatively, we could do that for them.) 1702 1703 // Create a new ContentValues for the new event. Start with the original event, 1704 // and drop in the new caller-supplied values. This will set originalInstanceTime. 1705 ContentValues values = new ContentValues(); 1706 DatabaseUtils.cursorRowToContentValues(cursor, values); 1707 1708 // TODO: if we're changing this to an all-day event, we should ensure that 1709 // hours/mins/secs on DTSTART are zeroed out (before computing DTEND). 1710 // See fixAllDayTime(). 1711 1712 boolean createNewEvent = true; 1713 if (createSingleException) { 1714 /* 1715 * Save a copy of a few fields that will migrate to new places. 1716 */ 1717 String _id = values.getAsString(Events._ID); 1718 String _sync_id = values.getAsString(Events._SYNC_ID); 1719 boolean allDay = values.getAsBoolean(Events.ALL_DAY); 1720 1721 /* 1722 * Wipe out some fields that we don't want to clone into the exception event. 1723 */ 1724 for (String str : DONT_CLONE_INTO_EXCEPTION) { 1725 values.remove(str); 1726 } 1727 1728 /* 1729 * Merge the new values on top of the existing values. Note this sets 1730 * originalInstanceTime. 1731 */ 1732 values.putAll(modValues); 1733 1734 /* 1735 * Copy some fields to their "original" counterparts: 1736 * _id --> original_id 1737 * _sync_id --> original_sync_id 1738 * allDay --> originalAllDay 1739 * 1740 * If this event hasn't been sync'ed with the server yet, the _sync_id field will 1741 * be null. We will need to fill original_sync_id in later. (May not be able to 1742 * do it right when our own _sync_id field gets populated, because the order of 1743 * events from the server may not be what we want -- could update the exception 1744 * before updating the original event.) 1745 * 1746 * _id is removed later (right before we write the event). 1747 */ 1748 values.put(Events.ORIGINAL_ID, _id); 1749 values.put(Events.ORIGINAL_SYNC_ID, _sync_id); 1750 values.put(Events.ORIGINAL_ALL_DAY, allDay); 1751 1752 // Mark the exception event status as "tentative", unless the caller has some 1753 // other value in mind (like STATUS_CANCELED). 1754 if (!values.containsKey(Events.STATUS)) { 1755 values.put(Events.STATUS, Events.STATUS_TENTATIVE); 1756 } 1757 1758 // We're converting from recurring to non-recurring. Clear out RRULE and replace 1759 // DURATION with DTEND. 1760 values.remove(Events.RRULE); 1761 1762 Duration duration = new Duration(); 1763 String durationStr = values.getAsString(Events.DURATION); 1764 try { 1765 duration.parse(durationStr); 1766 } catch (Exception ex) { 1767 // NullPointerException if the original event had no duration. 1768 // DateException if the duration was malformed. 1769 Log.w(TAG, "Bad duration in recurring event: " + durationStr, ex); 1770 return -1; 1771 } 1772 1773 /* 1774 * We want to compute DTEND as an offset from the start time of the instance. 1775 * If the caller specified a new value for DTSTART, we want to use that; if not, 1776 * the DTSTART in "values" will be the start time of the first instance in the 1777 * recurrence, so we want to replace it with ORIGINAL_INSTANCE_TIME. 1778 */ 1779 long start; 1780 if (modValues.containsKey(Events.DTSTART)) { 1781 start = values.getAsLong(Events.DTSTART); 1782 } else { 1783 start = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1784 values.put(Events.DTSTART, start); 1785 } 1786 values.put(Events.DTEND, start + duration.getMillis()); 1787 if (DEBUG_EXCEPTION) { 1788 Log.d(TAG, "RE: ORIG_INST_TIME=" + start + 1789 ", duration=" + duration.getMillis() + 1790 ", generated DTEND=" + values.getAsLong(Events.DTEND)); 1791 } 1792 } else { 1793 /* 1794 * We're going to "split" the recurring event, making the old one stop before 1795 * this instance, and creating a new recurring event that starts here. 1796 * 1797 * No need to fill out the "original" fields -- the new event is not tied to 1798 * the previous event in any way. 1799 * 1800 * If this is the first event in the series, we can just update the existing 1801 * event with the values. 1802 */ 1803 boolean canceling = (values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED); 1804 1805 if (originalInstanceTime.equals(values.getAsLong(Events.DTSTART))) { 1806 /* 1807 * Update fields in the existing event. Rather than use the merged data 1808 * from the cursor, we just do the update with the new value set after 1809 * removing the ORIGINAL_INSTANCE_TIME entry. 1810 */ 1811 if (canceling) { 1812 // TODO: should we just call deleteEventInternal? 1813 Log.d(TAG, "Note: canceling entire event via exception call"); 1814 } 1815 if (DEBUG_EXCEPTION) { 1816 Log.d(TAG, "RE: updating full event"); 1817 } 1818 modValues.remove(Events.ORIGINAL_INSTANCE_TIME); 1819 mDb.update(Tables.EVENTS, modValues, SQL_WHERE_ID, 1820 new String[] { Long.toString(originalEventId) }); 1821 createNewEvent = false; // skip event creation and related-table cloning 1822 } else { 1823 if (DEBUG_EXCEPTION) { 1824 Log.d(TAG, "RE: splitting event"); 1825 } 1826 1827 /* 1828 * Cap the original event so it ends just before the target instance. In 1829 * some cases (nonzero COUNT) this will also update the RRULE in "values", 1830 * so that the exception we're creating terminates appropriately. If a 1831 * new RRULE was specified by the caller, the new rule will overwrite our 1832 * changes when we merge the new values in below (which is the desired 1833 * behavior). 1834 */ 1835 ContentValues splitValues = setRecurrenceEnd(values, originalInstanceTime); 1836 mDb.update(Tables.EVENTS, splitValues, SQL_WHERE_ID, 1837 new String[] { Long.toString(originalEventId) }); 1838 1839 /* 1840 * Prepare the new event. We remove originalInstanceTime, because we're now 1841 * creating a new event rather than an exception. 1842 * 1843 * We're always cloning a non-exception event (we tested to make sure the 1844 * event doesn't specify original_id, and we don't allow original_id in the 1845 * modValues), so we shouldn't end up creating a new event that looks like 1846 * an exception. 1847 */ 1848 values.putAll(modValues); 1849 values.remove(Events.ORIGINAL_INSTANCE_TIME); 1850 } 1851 } 1852 1853 long newEventId; 1854 if (createNewEvent) { 1855 values.remove(Events._ID); // don't try to set this explicitly 1856 validateEventData(values); 1857 1858 newEventId = mDb.insert(Tables.EVENTS, null, values); 1859 if (newEventId < 0) { 1860 Log.w(TAG, "Unable to add exception to recurring event"); 1861 Log.w(TAG, "Values: " + values); 1862 return -1; 1863 } 1864 if (DEBUG_EXCEPTION) { 1865 Log.d(TAG, "RE: new ID is " + newEventId); 1866 } 1867 1868 // TODO: do we need to do something like this? 1869 //updateEventRawTimesLocked(id, updatedValues); 1870 1871 /* 1872 * Force re-computation of the Instances associated with the recurrence event. 1873 */ 1874 mInstancesHelper.updateInstancesLocked(values, newEventId, true, mDb); 1875 1876 /* 1877 * Some of the other tables (Attendees, Reminders, ExtendedProperties) reference 1878 * the Event ID. We need to copy the entries from the old event, filling in the 1879 * new event ID, so that somebody doing a SELECT on those tables will find 1880 * matching entries. 1881 */ 1882 CalendarDatabaseHelper.copyEventRelatedTables(mDb, newEventId, originalEventId); 1883 1884 /* 1885 * If we modified Event.selfAttendeeStatus, we need to keep the corresponding 1886 * entry in the Attendees table in sync. 1887 */ 1888 if (modValues.containsKey(Events.SELF_ATTENDEE_STATUS)) { 1889 /* 1890 * Each Attendee is identified by email address. To find the entry that 1891 * corresponds to "self", we want to compare that address to the owner of 1892 * the Calendar. We're expecting to find one matching entry in Attendees. 1893 */ 1894 long calendarId = values.getAsLong(Events.CALENDAR_ID); 1895 cursor = mDb.query(Tables.CALENDARS, new String[] { Calendars.OWNER_ACCOUNT }, 1896 SQL_WHERE_ID, new String[] { String.valueOf(calendarId) }, 1897 null /* groupBy */, null /* having */, null /* sortOrder */); 1898 if (!cursor.moveToFirst()) { 1899 Log.w(TAG, "Can't get calendar account_name for calendar " + calendarId); 1900 } else { 1901 String accountName = cursor.getString(0); 1902 ContentValues attValues = new ContentValues(); 1903 attValues.put(Attendees.ATTENDEE_STATUS, 1904 modValues.getAsString(Events.SELF_ATTENDEE_STATUS)); 1905 1906 if (DEBUG_EXCEPTION) { 1907 Log.d(TAG, "Updating attendee status for event=" + newEventId + 1908 " name=" + accountName + " to " + 1909 attValues.getAsString(Attendees.ATTENDEE_STATUS)); 1910 } 1911 int count = mDb.update(Tables.ATTENDEES, attValues, 1912 Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_EMAIL + "=?", 1913 new String[] { String.valueOf(newEventId), accountName }); 1914 if (count != 1 && count != 2) { 1915 // We're only expecting one matching entry. We might briefly see 1916 // two during a server sync. 1917 Log.e(TAG, "Attendee status update on event=" + newEventId + 1918 " name=" + accountName + " touched " + count + " rows"); 1919 if (false) { 1920 // This dumps PII in the log, don't ship with it enabled. 1921 Cursor debugCursor = mDb.query(Tables.ATTENDEES, null, 1922 Attendees.EVENT_ID + "=? AND " + 1923 Attendees.ATTENDEE_EMAIL + "=?", 1924 new String[] { String.valueOf(newEventId), accountName }, 1925 null, null, null); 1926 DatabaseUtils.dumpCursor(debugCursor); 1927 } 1928 throw new RuntimeException("Status update WTF"); 1929 } 1930 } 1931 cursor.close(); 1932 } 1933 } else { 1934 /* 1935 * Update any Instances changed by the update to this Event. 1936 */ 1937 mInstancesHelper.updateInstancesLocked(values, originalEventId, false, mDb); 1938 newEventId = originalEventId; 1939 } 1940 1941 mDb.setTransactionSuccessful(); 1942 return newEventId; 1943 } finally { 1944 if (cursor != null) { 1945 cursor.close(); 1946 } 1947 mDb.endTransaction(); 1948 } 1949 } 1950 1951 @Override 1952 protected Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 1953 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1954 Log.v(TAG, "insertInTransaction: " + uri); 1955 } 1956 final int match = sUriMatcher.match(uri); 1957 verifyTransactionAllowed(TRANSACTION_INSERT, uri, values, callerIsSyncAdapter, match, 1958 null /* selection */, null /* selection args */); 1959 1960 long id = 0; 1961 1962 switch (match) { 1963 case SYNCSTATE: 1964 id = mDbHelper.getSyncState().insert(mDb, values); 1965 break; 1966 case EVENTS: 1967 if (!callerIsSyncAdapter) { 1968 values.put(Events.DIRTY, 1); 1969 } 1970 if (!values.containsKey(Events.DTSTART)) { 1971 throw new RuntimeException("DTSTART field missing from event"); 1972 } 1973 // TODO: do we really need to make a copy? 1974 ContentValues updatedValues = new ContentValues(values); 1975 validateEventData(updatedValues); 1976 // updateLastDate must be after validation, to ensure proper last date computation 1977 updatedValues = updateLastDate(updatedValues); 1978 if (updatedValues == null) { 1979 throw new RuntimeException("Could not insert event."); 1980 // return null; 1981 } 1982 String owner = null; 1983 if (updatedValues.containsKey(Events.CALENDAR_ID) && 1984 !updatedValues.containsKey(Events.ORGANIZER)) { 1985 owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID)); 1986 // TODO: This isn't entirely correct. If a guest is adding a recurrence 1987 // exception to an event, the organizer should stay the original organizer. 1988 // This value doesn't go to the server and it will get fixed on sync, 1989 // so it shouldn't really matter. 1990 if (owner != null) { 1991 updatedValues.put(Events.ORGANIZER, owner); 1992 } 1993 } 1994 if (updatedValues.containsKey(Events.ORIGINAL_SYNC_ID) 1995 && !updatedValues.containsKey(Events.ORIGINAL_ID)) { 1996 long originalId = getOriginalId(updatedValues 1997 .getAsString(Events.ORIGINAL_SYNC_ID)); 1998 if (originalId != -1) { 1999 updatedValues.put(Events.ORIGINAL_ID, originalId); 2000 } 2001 } else if (!updatedValues.containsKey(Events.ORIGINAL_SYNC_ID) 2002 && updatedValues.containsKey(Events.ORIGINAL_ID)) { 2003 String originalSyncId = getOriginalSyncId(updatedValues 2004 .getAsLong(Events.ORIGINAL_ID)); 2005 if (!TextUtils.isEmpty(originalSyncId)) { 2006 updatedValues.put(Events.ORIGINAL_SYNC_ID, originalSyncId); 2007 } 2008 } 2009 if (fixAllDayTime(uri, updatedValues)) { 2010 if (Log.isLoggable(TAG, Log.WARN)) { 2011 Log.w(TAG, "insertInTransaction: " + 2012 "allDay is true but sec, min, hour were not 0."); 2013 } 2014 } 2015 // Insert the row 2016 id = mDbHelper.eventsInsert(updatedValues); 2017 if (id != -1) { 2018 updateEventRawTimesLocked(id, updatedValues); 2019 mInstancesHelper.updateInstancesLocked(updatedValues, id, 2020 true /* new event */, mDb); 2021 2022 // If we inserted a new event that specified the self-attendee 2023 // status, then we need to add an entry to the attendees table. 2024 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) { 2025 int status = values.getAsInteger(Events.SELF_ATTENDEE_STATUS); 2026 if (owner == null) { 2027 owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID)); 2028 } 2029 createAttendeeEntry(id, status, owner); 2030 } 2031 // if the Event Timezone is defined, store it as the original one in the 2032 // ExtendedProperties table 2033 if (values.containsKey(Events.EVENT_TIMEZONE) && !callerIsSyncAdapter) { 2034 String originalTimezone = values.getAsString(Events.EVENT_TIMEZONE); 2035 2036 ContentValues expropsValues = new ContentValues(); 2037 expropsValues.put(CalendarContract.ExtendedProperties.EVENT_ID, id); 2038 expropsValues.put(CalendarContract.ExtendedProperties.NAME, 2039 EXT_PROP_ORIGINAL_TIMEZONE); 2040 expropsValues.put(CalendarContract.ExtendedProperties.VALUE, 2041 originalTimezone); 2042 2043 // Insert the extended property 2044 long exPropId = mDbHelper.extendedPropertiesInsert(expropsValues); 2045 if (exPropId == -1) { 2046 if (Log.isLoggable(TAG, Log.ERROR)) { 2047 Log.e(TAG, "Cannot add the original Timezone in the " 2048 + "ExtendedProperties table for Event: " + id); 2049 } 2050 } else { 2051 // Update the Event for saying it has some extended properties 2052 ContentValues eventValues = new ContentValues(); 2053 eventValues.put(Events.HAS_EXTENDED_PROPERTIES, "1"); 2054 int result = mDb.update("Events", eventValues, SQL_WHERE_ID, 2055 new String[] {String.valueOf(id)}); 2056 if (result <= 0) { 2057 if (Log.isLoggable(TAG, Log.ERROR)) { 2058 Log.e(TAG, "Cannot update hasExtendedProperties column" 2059 + " for Event: " + id); 2060 } 2061 } 2062 } 2063 } 2064 2065 /* 2066 * The server might send exceptions before the event they refer to. When 2067 * this happens, the originalId field will not have been set in the 2068 * exception events (it's the recurrence events' _id field, so it can't be 2069 * known until the recurrence event is created). When we add a recurrence 2070 * event with a non-empty _sync_id field, we write that event's _id to the 2071 * originalId field of any events whose originalSyncId matches _sync_id. 2072 */ 2073 if (values.containsKey(Events._SYNC_ID) && values.containsKey(Events.RRULE) 2074 && !TextUtils.isEmpty(values.getAsString(Events.RRULE))) { 2075 String syncId = values.getAsString(Events._SYNC_ID); 2076 if (TextUtils.isEmpty(syncId)) { 2077 break; 2078 } 2079 ContentValues originalValues = new ContentValues(); 2080 originalValues.put(Events.ORIGINAL_ID, id); 2081 mDb.update(Tables.EVENTS, originalValues, Events.ORIGINAL_SYNC_ID + "=?", 2082 new String[] {syncId}); 2083 } 2084 sendUpdateNotification(id, callerIsSyncAdapter); 2085 } 2086 break; 2087 case EXCEPTION_ID: 2088 long originalEventId = ContentUris.parseId(uri); 2089 id = handleInsertException(originalEventId, values, callerIsSyncAdapter); 2090 break; 2091 case CALENDARS: 2092 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS); 2093 if (syncEvents != null && syncEvents == 1) { 2094 String accountName = values.getAsString(Calendars.ACCOUNT_NAME); 2095 String accountType = values.getAsString( 2096 Calendars.ACCOUNT_TYPE); 2097 final Account account = new Account(accountName, accountType); 2098 String eventsUrl = values.getAsString(Calendars.CAL_SYNC1); 2099 mDbHelper.scheduleSync(account, false /* two-way sync */, eventsUrl); 2100 } 2101 id = mDbHelper.calendarsInsert(values); 2102 sendUpdateNotification(id, callerIsSyncAdapter); 2103 break; 2104 case ATTENDEES: 2105 if (!values.containsKey(Attendees.EVENT_ID)) { 2106 throw new IllegalArgumentException("Attendees values must " 2107 + "contain an event_id"); 2108 } 2109 if (!callerIsSyncAdapter) { 2110 final Long eventId = values.getAsLong(Attendees.EVENT_ID); 2111 mDbHelper.duplicateEvent(eventId); 2112 setEventDirty(eventId); 2113 } 2114 id = mDbHelper.attendeesInsert(values); 2115 2116 // Copy the attendee status value to the Events table. 2117 updateEventAttendeeStatus(mDb, values); 2118 break; 2119 case REMINDERS: 2120 if (!values.containsKey(Reminders.EVENT_ID)) { 2121 throw new IllegalArgumentException("Reminders values must " 2122 + "contain an event_id"); 2123 } 2124 if (!callerIsSyncAdapter) { 2125 final Long eventId = values.getAsLong(Reminders.EVENT_ID); 2126 mDbHelper.duplicateEvent(eventId); 2127 setEventDirty(eventId); 2128 } 2129 id = mDbHelper.remindersInsert(values); 2130 2131 // Schedule another event alarm, if necessary 2132 if (Log.isLoggable(TAG, Log.DEBUG)) { 2133 Log.d(TAG, "insertInternal() changing reminder"); 2134 } 2135 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 2136 break; 2137 case CALENDAR_ALERTS: 2138 if (!values.containsKey(CalendarAlerts.EVENT_ID)) { 2139 throw new IllegalArgumentException("CalendarAlerts values must " 2140 + "contain an event_id"); 2141 } 2142 id = mDbHelper.calendarAlertsInsert(values); 2143 // Note: dirty bit is not set for Alerts because it is not synced. 2144 // It is generated from Reminders, which is synced. 2145 break; 2146 case EXTENDED_PROPERTIES: 2147 if (!values.containsKey(CalendarContract.ExtendedProperties.EVENT_ID)) { 2148 throw new IllegalArgumentException("ExtendedProperties values must " 2149 + "contain an event_id"); 2150 } 2151 if (!callerIsSyncAdapter) { 2152 final Long eventId = values 2153 .getAsLong(CalendarContract.ExtendedProperties.EVENT_ID); 2154 mDbHelper.duplicateEvent(eventId); 2155 setEventDirty(eventId); 2156 } 2157 id = mDbHelper.extendedPropertiesInsert(values); 2158 break; 2159 case EVENTS_ID: 2160 case REMINDERS_ID: 2161 case CALENDAR_ALERTS_ID: 2162 case EXTENDED_PROPERTIES_ID: 2163 case INSTANCES: 2164 case INSTANCES_BY_DAY: 2165 case EVENT_DAYS: 2166 case PROVIDER_PROPERTIES: 2167 throw new UnsupportedOperationException("Cannot insert into that URL: " + uri); 2168 default: 2169 throw new IllegalArgumentException("Unknown URL " + uri); 2170 } 2171 2172 if (id < 0) { 2173 return null; 2174 } 2175 2176 return ContentUris.withAppendedId(uri, id); 2177 } 2178 2179 /** 2180 * Do some validation on event data before inserting. In particular make 2181 * sure calendar_id exists and dtend, duration, etc make sense for the type 2182 * of event (regular, recurrence, exception). Remove any unexpected fields. 2183 * 2184 * @param values the ContentValues to insert 2185 */ 2186 private void validateEventData(ContentValues values) { 2187 boolean hasDtend = values.getAsLong(Events.DTEND) != null; 2188 boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION)); 2189 boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE)); 2190 boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE)); 2191 boolean hasCalId = !TextUtils.isEmpty(values.getAsString(Events.CALENDAR_ID)); 2192 boolean hasOriginalEvent = !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_SYNC_ID)); 2193 boolean hasOriginalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME) != null; 2194 if (!hasCalId) { 2195 throw new IllegalArgumentException("New events must include a calendar_id."); 2196 } 2197 if (hasRrule || hasRdate) { 2198 // Recurrence: 2199 // dtstart is start time of first event 2200 // dtend is null 2201 // duration is the duration of the event 2202 // rrule is the recurrence rule 2203 // lastDate is the end of the last event or null if it repeats forever 2204 // originalEvent is null 2205 // originalInstanceTime is null 2206 if (hasDtend || !hasDuration || hasOriginalEvent || hasOriginalInstanceTime) { 2207 if (Log.isLoggable(TAG, Log.DEBUG)) { 2208 Log.e(TAG, "Invalid values for recurrence: " + values); 2209 } 2210 values.remove(Events.DTEND); 2211 values.remove(Events.ORIGINAL_SYNC_ID); 2212 values.remove(Events.ORIGINAL_INSTANCE_TIME); 2213 } 2214 } else if (hasOriginalEvent || hasOriginalInstanceTime) { 2215 // Recurrence exception 2216 // dtstart is start time of exception event 2217 // dtend is end time of exception event 2218 // duration is null 2219 // rrule is null 2220 // lastdate is same as dtend 2221 // originalEvent is the _sync_id of the recurrence 2222 // originalInstanceTime is the start time of the event being replaced 2223 if (!hasDtend || hasDuration || !hasOriginalEvent || !hasOriginalInstanceTime) { 2224 if (Log.isLoggable(TAG, Log.DEBUG)) { 2225 Log.e(TAG, "Invalid values for recurrence exception: " + values); 2226 } 2227 values.remove(Events.DURATION); 2228 } 2229 } else { 2230 // Regular event 2231 // dtstart is the start time 2232 // dtend is the end time 2233 // duration is null 2234 // rrule is null 2235 // lastDate is the same as dtend 2236 // originalEvent is null 2237 // originalInstanceTime is null 2238 if (!hasDtend || hasDuration) { 2239 if (Log.isLoggable(TAG, Log.DEBUG)) { 2240 Log.e(TAG, "Invalid values for event: " + values); 2241 } 2242 values.remove(Events.DURATION); 2243 } 2244 } 2245 } 2246 2247 private void setEventDirty(long eventId) { 2248 mDb.execSQL(SQL_UPDATE_EVENT_SET_DIRTY, new Object[] {eventId}); 2249 } 2250 2251 private long getOriginalId(String originalSyncId) { 2252 if (TextUtils.isEmpty(originalSyncId)) { 2253 return -1; 2254 } 2255 // Get the original id for this event 2256 long originalId = -1; 2257 Cursor c = null; 2258 try { 2259 c = query(Events.CONTENT_URI, ID_ONLY_PROJECTION, 2260 Events._SYNC_ID + "=?", new String[] {originalSyncId}, null); 2261 if (c != null && c.moveToFirst()) { 2262 originalId = c.getLong(0); 2263 } 2264 } finally { 2265 if (c != null) { 2266 c.close(); 2267 } 2268 } 2269 return originalId; 2270 } 2271 2272 private String getOriginalSyncId(long originalId) { 2273 if (originalId == -1) { 2274 return null; 2275 } 2276 // Get the original id for this event 2277 String originalSyncId = null; 2278 Cursor c = null; 2279 try { 2280 c = query(Events.CONTENT_URI, new String[] {Events._SYNC_ID}, 2281 Events._ID + "=?", new String[] {Long.toString(originalId)}, null); 2282 if (c != null && c.moveToFirst()) { 2283 originalSyncId = c.getString(0); 2284 } 2285 } finally { 2286 if (c != null) { 2287 c.close(); 2288 } 2289 } 2290 return originalSyncId; 2291 } 2292 2293 /** 2294 * Gets the calendar's owner for an event. 2295 * @param calId 2296 * @return email of owner or null 2297 */ 2298 private String getOwner(long calId) { 2299 if (calId < 0) { 2300 if (Log.isLoggable(TAG, Log.ERROR)) { 2301 Log.e(TAG, "Calendar Id is not valid: " + calId); 2302 } 2303 return null; 2304 } 2305 // Get the email address of this user from this Calendar 2306 String emailAddress = null; 2307 Cursor cursor = null; 2308 try { 2309 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 2310 new String[] { Calendars.OWNER_ACCOUNT }, 2311 null /* selection */, 2312 null /* selectionArgs */, 2313 null /* sort */); 2314 if (cursor == null || !cursor.moveToFirst()) { 2315 if (Log.isLoggable(TAG, Log.DEBUG)) { 2316 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 2317 } 2318 return null; 2319 } 2320 emailAddress = cursor.getString(0); 2321 } finally { 2322 if (cursor != null) { 2323 cursor.close(); 2324 } 2325 } 2326 return emailAddress; 2327 } 2328 2329 /** 2330 * Creates an entry in the Attendees table that refers to the given event 2331 * and that has the given response status. 2332 * 2333 * @param eventId the event id that the new entry in the Attendees table 2334 * should refer to 2335 * @param status the response status 2336 * @param emailAddress the email of the attendee 2337 */ 2338 private void createAttendeeEntry(long eventId, int status, String emailAddress) { 2339 ContentValues values = new ContentValues(); 2340 values.put(Attendees.EVENT_ID, eventId); 2341 values.put(Attendees.ATTENDEE_STATUS, status); 2342 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 2343 // TODO: The relationship could actually be ORGANIZER, but it will get straightened out 2344 // on sync. 2345 values.put(Attendees.ATTENDEE_RELATIONSHIP, 2346 Attendees.RELATIONSHIP_ATTENDEE); 2347 values.put(Attendees.ATTENDEE_EMAIL, emailAddress); 2348 2349 // We don't know the ATTENDEE_NAME but that will be filled in by the 2350 // server and sent back to us. 2351 mDbHelper.attendeesInsert(values); 2352 } 2353 2354 /** 2355 * Updates the attendee status in the Events table to be consistent with 2356 * the value in the Attendees table. 2357 * 2358 * @param db the database 2359 * @param attendeeValues the column values for one row in the Attendees 2360 * table. 2361 */ 2362 private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) { 2363 // Get the event id for this attendee 2364 long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID); 2365 2366 if (MULTIPLE_ATTENDEES_PER_EVENT) { 2367 // Get the calendar id for this event 2368 Cursor cursor = null; 2369 long calId; 2370 try { 2371 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 2372 new String[] { Events.CALENDAR_ID }, 2373 null /* selection */, 2374 null /* selectionArgs */, 2375 null /* sort */); 2376 if (cursor == null || !cursor.moveToFirst()) { 2377 if (Log.isLoggable(TAG, Log.DEBUG)) { 2378 Log.d(TAG, "Couldn't find " + eventId + " in Events table"); 2379 } 2380 return; 2381 } 2382 calId = cursor.getLong(0); 2383 } finally { 2384 if (cursor != null) { 2385 cursor.close(); 2386 } 2387 } 2388 2389 // Get the owner email for this Calendar 2390 String calendarEmail = null; 2391 cursor = null; 2392 try { 2393 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId), 2394 new String[] { Calendars.OWNER_ACCOUNT }, 2395 null /* selection */, 2396 null /* selectionArgs */, 2397 null /* sort */); 2398 if (cursor == null || !cursor.moveToFirst()) { 2399 if (Log.isLoggable(TAG, Log.DEBUG)) { 2400 Log.d(TAG, "Couldn't find " + calId + " in Calendars table"); 2401 } 2402 return; 2403 } 2404 calendarEmail = cursor.getString(0); 2405 } finally { 2406 if (cursor != null) { 2407 cursor.close(); 2408 } 2409 } 2410 2411 if (calendarEmail == null) { 2412 return; 2413 } 2414 2415 // Get the email address for this attendee 2416 String attendeeEmail = null; 2417 if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 2418 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL); 2419 } 2420 2421 // If the attendee email does not match the calendar email, then this 2422 // attendee is not the owner of this calendar so we don't update the 2423 // selfAttendeeStatus in the event. 2424 if (!calendarEmail.equals(attendeeEmail)) { 2425 return; 2426 } 2427 } 2428 2429 int status = Attendees.ATTENDEE_STATUS_NONE; 2430 if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) { 2431 int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 2432 if (rel == Attendees.RELATIONSHIP_ORGANIZER) { 2433 status = Attendees.ATTENDEE_STATUS_ACCEPTED; 2434 } 2435 } 2436 2437 if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) { 2438 status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS); 2439 } 2440 2441 ContentValues values = new ContentValues(); 2442 values.put(Events.SELF_ATTENDEE_STATUS, status); 2443 db.update(Tables.EVENTS, values, SQL_WHERE_ID, 2444 new String[] {String.valueOf(eventId)}); 2445 } 2446 2447 long calculateLastDate(ContentValues values) 2448 throws DateException { 2449 // Allow updates to some event fields like the title or hasAlarm 2450 // without requiring DTSTART. 2451 if (!values.containsKey(Events.DTSTART)) { 2452 if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE) 2453 || values.containsKey(Events.DURATION) 2454 || values.containsKey(Events.EVENT_TIMEZONE) 2455 || values.containsKey(Events.RDATE) 2456 || values.containsKey(Events.EXRULE) 2457 || values.containsKey(Events.EXDATE)) { 2458 throw new RuntimeException("DTSTART field missing from event"); 2459 } 2460 return -1; 2461 } 2462 long dtstartMillis = values.getAsLong(Events.DTSTART); 2463 long lastMillis = -1; 2464 2465 // Can we use dtend with a repeating event? What does that even 2466 // mean? 2467 // NOTE: if the repeating event has a dtend, we convert it to a 2468 // duration during event processing, so this situation should not 2469 // occur. 2470 Long dtEnd = values.getAsLong(Events.DTEND); 2471 if (dtEnd != null) { 2472 lastMillis = dtEnd; 2473 } else { 2474 // find out how long it is 2475 Duration duration = new Duration(); 2476 String durationStr = values.getAsString(Events.DURATION); 2477 if (durationStr != null) { 2478 duration.parse(durationStr); 2479 } 2480 2481 RecurrenceSet recur = null; 2482 try { 2483 recur = new RecurrenceSet(values); 2484 } catch (EventRecurrence.InvalidFormatException e) { 2485 if (Log.isLoggable(TAG, Log.WARN)) { 2486 Log.w(TAG, "Could not parse RRULE recurrence string: " + 2487 values.get(CalendarContract.Events.RRULE), e); 2488 } 2489 return lastMillis; // -1 2490 } 2491 2492 if (null != recur && recur.hasRecurrence()) { 2493 // the event is repeating, so find the last date it 2494 // could appear on 2495 2496 String tz = values.getAsString(Events.EVENT_TIMEZONE); 2497 2498 if (TextUtils.isEmpty(tz)) { 2499 // floating timezone 2500 tz = Time.TIMEZONE_UTC; 2501 } 2502 Time dtstartLocal = new Time(tz); 2503 2504 dtstartLocal.set(dtstartMillis); 2505 2506 RecurrenceProcessor rp = new RecurrenceProcessor(); 2507 lastMillis = rp.getLastOccurence(dtstartLocal, recur); 2508 if (lastMillis == -1) { 2509 return lastMillis; // -1 2510 } 2511 } else { 2512 // the event is not repeating, just use dtstartMillis 2513 lastMillis = dtstartMillis; 2514 } 2515 2516 // that was the beginning of the event. this is the end. 2517 lastMillis = duration.addTo(lastMillis); 2518 } 2519 return lastMillis; 2520 } 2521 2522 /** 2523 * Add LAST_DATE to values. 2524 * @param values the ContentValues (in/out) 2525 * @return values on success, null on failure 2526 */ 2527 private ContentValues updateLastDate(ContentValues values) { 2528 try { 2529 long last = calculateLastDate(values); 2530 if (last != -1) { 2531 values.put(Events.LAST_DATE, last); 2532 } 2533 2534 return values; 2535 } catch (DateException e) { 2536 // don't add it if there was an error 2537 if (Log.isLoggable(TAG, Log.WARN)) { 2538 Log.w(TAG, "Could not calculate last date.", e); 2539 } 2540 return null; 2541 } 2542 } 2543 2544 private void updateEventRawTimesLocked(long eventId, ContentValues values) { 2545 ContentValues rawValues = new ContentValues(); 2546 2547 rawValues.put(CalendarContract.EventsRawTimes.EVENT_ID, eventId); 2548 2549 String timezone = values.getAsString(Events.EVENT_TIMEZONE); 2550 2551 boolean allDay = false; 2552 Integer allDayInteger = values.getAsInteger(Events.ALL_DAY); 2553 if (allDayInteger != null) { 2554 allDay = allDayInteger != 0; 2555 } 2556 2557 if (allDay || TextUtils.isEmpty(timezone)) { 2558 // floating timezone 2559 timezone = Time.TIMEZONE_UTC; 2560 } 2561 2562 Time time = new Time(timezone); 2563 time.allDay = allDay; 2564 Long dtstartMillis = values.getAsLong(Events.DTSTART); 2565 if (dtstartMillis != null) { 2566 time.set(dtstartMillis); 2567 rawValues.put(CalendarContract.EventsRawTimes.DTSTART_2445, time.format2445()); 2568 } 2569 2570 Long dtendMillis = values.getAsLong(Events.DTEND); 2571 if (dtendMillis != null) { 2572 time.set(dtendMillis); 2573 rawValues.put(CalendarContract.EventsRawTimes.DTEND_2445, time.format2445()); 2574 } 2575 2576 Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 2577 if (originalInstanceMillis != null) { 2578 // This is a recurrence exception so we need to get the all-day 2579 // status of the original recurring event in order to format the 2580 // date correctly. 2581 allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY); 2582 if (allDayInteger != null) { 2583 time.allDay = allDayInteger != 0; 2584 } 2585 time.set(originalInstanceMillis); 2586 rawValues.put(CalendarContract.EventsRawTimes.ORIGINAL_INSTANCE_TIME_2445, 2587 time.format2445()); 2588 } 2589 2590 Long lastDateMillis = values.getAsLong(Events.LAST_DATE); 2591 if (lastDateMillis != null) { 2592 time.allDay = allDay; 2593 time.set(lastDateMillis); 2594 rawValues.put(CalendarContract.EventsRawTimes.LAST_DATE_2445, time.format2445()); 2595 } 2596 2597 mDbHelper.eventsRawTimesReplace(rawValues); 2598 } 2599 2600 @Override 2601 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, 2602 boolean callerIsSyncAdapter) { 2603 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2604 Log.v(TAG, "deleteInTransaction: " + uri); 2605 } 2606 final int match = sUriMatcher.match(uri); 2607 verifyTransactionAllowed(TRANSACTION_DELETE, uri, null, callerIsSyncAdapter, match, 2608 selection, selectionArgs); 2609 2610 switch (match) { 2611 case SYNCSTATE: 2612 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 2613 2614 case SYNCSTATE_ID: 2615 String selectionWithId = (SyncState._ID + "=?") 2616 + (selection == null ? "" : " AND (" + selection + ")"); 2617 // Prepend id to selectionArgs 2618 selectionArgs = insertSelectionArg(selectionArgs, 2619 String.valueOf(ContentUris.parseId(uri))); 2620 return mDbHelper.getSyncState().delete(mDb, selectionWithId, 2621 selectionArgs); 2622 2623 case EVENTS: 2624 { 2625 int result = 0; 2626 selection = appendSyncAccountToSelection(uri, selection); 2627 2628 // Query this event to get the ids to delete. 2629 Cursor cursor = mDb.query(Views.EVENTS, ID_ONLY_PROJECTION, 2630 selection, selectionArgs, null /* groupBy */, 2631 null /* having */, null /* sortOrder */); 2632 try { 2633 while (cursor.moveToNext()) { 2634 long id = cursor.getLong(0); 2635 result += deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */); 2636 } 2637 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 2638 sendUpdateNotification(callerIsSyncAdapter); 2639 } finally { 2640 cursor.close(); 2641 cursor = null; 2642 } 2643 return result; 2644 } 2645 case EVENTS_ID: 2646 { 2647 long id = ContentUris.parseId(uri); 2648 if (selection != null) { 2649 throw new UnsupportedOperationException("CalendarProvider2 " 2650 + "doesn't support selection based deletion for type " 2651 + match); 2652 } 2653 return deleteEventInternal(id, callerIsSyncAdapter, false /* isBatch */); 2654 } 2655 case EXCEPTION_ID2: 2656 { 2657 // This will throw NumberFormatException on missing or malformed input. 2658 List<String> segments = uri.getPathSegments(); 2659 long eventId = Long.parseLong(segments.get(1)); 2660 long excepId = Long.parseLong(segments.get(2)); 2661 // TODO: verify that this is an exception instance (has an ORIGINAL_ID field 2662 // that matches the supplied eventId) 2663 return deleteEventInternal(excepId, callerIsSyncAdapter, false /* isBatch */); 2664 } 2665 case ATTENDEES: 2666 { 2667 if (callerIsSyncAdapter) { 2668 return mDb.delete(Tables.ATTENDEES, selection, selectionArgs); 2669 } else { 2670 return deleteFromTable(Tables.ATTENDEES, uri, selection, selectionArgs); 2671 } 2672 } 2673 case ATTENDEES_ID: 2674 { 2675 if (selection != null) { 2676 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2677 } 2678 if (callerIsSyncAdapter) { 2679 long id = ContentUris.parseId(uri); 2680 return mDb.delete(Tables.ATTENDEES, SQL_WHERE_ID, 2681 new String[] {String.valueOf(id)}); 2682 } else { 2683 return deleteFromTable(Tables.ATTENDEES, uri, null /* selection */, 2684 null /* selectionArgs */); 2685 } 2686 } 2687 case REMINDERS: 2688 { 2689 if (callerIsSyncAdapter) { 2690 return mDb.delete(Tables.REMINDERS, selection, selectionArgs); 2691 } else { 2692 return deleteFromTable(Tables.REMINDERS, uri, selection, selectionArgs); 2693 } 2694 } 2695 case REMINDERS_ID: 2696 { 2697 if (selection != null) { 2698 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2699 } 2700 if (callerIsSyncAdapter) { 2701 long id = ContentUris.parseId(uri); 2702 return mDb.delete(Tables.REMINDERS, SQL_WHERE_ID, 2703 new String[] {String.valueOf(id)}); 2704 } else { 2705 return deleteFromTable(Tables.REMINDERS, uri, null /* selection */, 2706 null /* selectionArgs */); 2707 } 2708 } 2709 case EXTENDED_PROPERTIES: 2710 { 2711 if (callerIsSyncAdapter) { 2712 return mDb.delete(Tables.EXTENDED_PROPERTIES, selection, selectionArgs); 2713 } else { 2714 return deleteFromTable(Tables.EXTENDED_PROPERTIES, uri, selection, 2715 selectionArgs); 2716 } 2717 } 2718 case EXTENDED_PROPERTIES_ID: 2719 { 2720 if (selection != null) { 2721 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2722 } 2723 if (callerIsSyncAdapter) { 2724 long id = ContentUris.parseId(uri); 2725 return mDb.delete(Tables.EXTENDED_PROPERTIES, SQL_WHERE_ID, 2726 new String[] {String.valueOf(id)}); 2727 } else { 2728 return deleteFromTable(Tables.EXTENDED_PROPERTIES, uri, null /* selection */, 2729 null /* selectionArgs */); 2730 } 2731 } 2732 case CALENDAR_ALERTS: 2733 { 2734 if (callerIsSyncAdapter) { 2735 return mDb.delete(Tables.CALENDAR_ALERTS, selection, selectionArgs); 2736 } else { 2737 return deleteFromTable(Tables.CALENDAR_ALERTS, uri, selection, selectionArgs); 2738 } 2739 } 2740 case CALENDAR_ALERTS_ID: 2741 { 2742 if (selection != null) { 2743 throw new UnsupportedOperationException("Selection not permitted for " + uri); 2744 } 2745 // Note: dirty bit is not set for Alerts because it is not synced. 2746 // It is generated from Reminders, which is synced. 2747 long id = ContentUris.parseId(uri); 2748 return mDb.delete(Tables.CALENDAR_ALERTS, SQL_WHERE_ID, 2749 new String[] {String.valueOf(id)}); 2750 } 2751 case CALENDARS_ID: 2752 StringBuilder selectionSb = new StringBuilder(Calendars._ID + "="); 2753 selectionSb.append(uri.getPathSegments().get(1)); 2754 if (!TextUtils.isEmpty(selection)) { 2755 selectionSb.append(" AND ("); 2756 selectionSb.append(selection); 2757 selectionSb.append(')'); 2758 } 2759 selection = selectionSb.toString(); 2760 // fall through to CALENDARS for the actual delete 2761 case CALENDARS: 2762 selection = appendAccountToSelection(uri, selection); 2763 return deleteMatchingCalendars(selection, selectionArgs); 2764 case INSTANCES: 2765 case INSTANCES_BY_DAY: 2766 case EVENT_DAYS: 2767 case PROVIDER_PROPERTIES: 2768 throw new UnsupportedOperationException("Cannot delete that URL"); 2769 default: 2770 throw new IllegalArgumentException("Unknown URL " + uri); 2771 } 2772 } 2773 2774 private int deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch) { 2775 int result = 0; 2776 String selectionArgs[] = new String[] {String.valueOf(id)}; 2777 2778 // Query this event to get the fields needed for deleting. 2779 Cursor cursor = mDb.query(Tables.EVENTS, EVENTS_PROJECTION, 2780 SQL_WHERE_ID, selectionArgs, 2781 null /* groupBy */, 2782 null /* having */, null /* sortOrder */); 2783 try { 2784 if (cursor.moveToNext()) { 2785 result = 1; 2786 String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX); 2787 boolean emptySyncId = TextUtils.isEmpty(syncId); 2788 2789 // If this was a recurring event or a recurrence 2790 // exception, then force a recalculation of the 2791 // instances. 2792 String rrule = cursor.getString(EVENTS_RRULE_INDEX); 2793 String rdate = cursor.getString(EVENTS_RDATE_INDEX); 2794 String origId = cursor.getString(EVENTS_ORIGINAL_ID_INDEX); 2795 String origSyncId = cursor.getString(EVENTS_ORIGINAL_SYNC_ID_INDEX); 2796 if (isRecurrenceEvent(rrule, rdate, origId, origSyncId)) { 2797 mMetaData.clearInstanceRange(); 2798 } 2799 2800 // we clean the Events and Attendees table if the caller is CalendarSyncAdapter 2801 // or if the event is local (no syncId) 2802 // 2803 // The EVENTS_CLEANUP_TRIGGER_SQL trigger will remove all associated data 2804 // (Attendees, Instances, Reminders, etc). 2805 if (callerIsSyncAdapter || emptySyncId) { 2806 mDb.delete(Tables.EVENTS, SQL_WHERE_ID, selectionArgs); 2807 } else { 2808 // Event is on the server, so we "soft delete", i.e. mark as deleted so that 2809 // the sync adapter has a chance to tell the server about the deletion. After 2810 // the server sees the change, the sync adapter will do the "hard delete" 2811 // (above). 2812 ContentValues values = new ContentValues(); 2813 values.put(Events.DELETED, 1); 2814 values.put(Events.DIRTY, 1); 2815 mDb.update(Tables.EVENTS, values, SQL_WHERE_ID, selectionArgs); 2816 2817 // Delete associated data; attendees, however, are deleted with the actual event 2818 // so that the sync adapter is able to notify attendees of the cancellation. 2819 mDb.delete(Tables.INSTANCES, SQL_WHERE_EVENT_ID, selectionArgs); 2820 mDb.delete(Tables.EVENTS_RAW_TIMES, SQL_WHERE_EVENT_ID, selectionArgs); 2821 mDb.delete(Tables.REMINDERS, SQL_WHERE_EVENT_ID, selectionArgs); 2822 mDb.delete(Tables.CALENDAR_ALERTS, SQL_WHERE_EVENT_ID, selectionArgs); 2823 mDb.delete(Tables.EXTENDED_PROPERTIES, SQL_WHERE_EVENT_ID, 2824 selectionArgs); 2825 } 2826 } 2827 } finally { 2828 cursor.close(); 2829 cursor = null; 2830 } 2831 2832 if (!isBatch) { 2833 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 2834 sendUpdateNotification(callerIsSyncAdapter); 2835 } 2836 return result; 2837 } 2838 2839 /** 2840 * Delete rows from a table and mark corresponding events as dirty. 2841 * @param table The table to delete from 2842 * @param uri The URI specifying the rows 2843 * @param selection for the query 2844 * @param selectionArgs for the query 2845 */ 2846 private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) { 2847 // Note that the query will return data according to the access restrictions, 2848 // so we don't need to worry about deleting data we don't have permission to read. 2849 final Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null); 2850 final ContentValues values = new ContentValues(); 2851 values.put(Events.DIRTY, "1"); 2852 int count = 0; 2853 try { 2854 while(c.moveToNext()) { 2855 final long id = c.getLong(ID_INDEX); 2856 final long event_id = c.getLong(EVENT_ID_INDEX); 2857 mDbHelper.duplicateEvent(event_id); 2858 mDb.delete(table, SQL_WHERE_ID, new String[]{String.valueOf(id)}); 2859 mDb.update(Tables.EVENTS, values, SQL_WHERE_ID, 2860 new String[] {String.valueOf(event_id)}); 2861 count++; 2862 } 2863 } finally { 2864 c.close(); 2865 } 2866 return count; 2867 } 2868 2869 /** 2870 * Update rows in a table and mark corresponding events as dirty. 2871 * @param table The table to delete from 2872 * @param values The values to update 2873 * @param uri The URI specifying the rows 2874 * @param selection for the query 2875 * @param selectionArgs for the query 2876 */ 2877 private int updateInTable(String table, ContentValues values, Uri uri, String selection, 2878 String[] selectionArgs) { 2879 // Note that the query will return data according to the access restrictions, 2880 // so we don't need to worry about deleting data we don't have permission to read. 2881 final Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null); 2882 final ContentValues dirtyValues = new ContentValues(); 2883 dirtyValues.put(Events.DIRTY, "1"); 2884 int count = 0; 2885 try { 2886 while(c.moveToNext()) { 2887 final long id = c.getLong(ID_INDEX); 2888 final long event_id = c.getLong(EVENT_ID_INDEX); 2889 mDbHelper.duplicateEvent(event_id); 2890 mDb.update(table, values, SQL_WHERE_ID, new String[] {String.valueOf(id)}); 2891 mDb.update(Tables.EVENTS, dirtyValues, SQL_WHERE_ID, 2892 new String[] {String.valueOf(event_id)}); 2893 count++; 2894 } 2895 } finally { 2896 c.close(); 2897 } 2898 return count; 2899 } 2900 2901 private int deleteMatchingCalendars(String selection, String[] selectionArgs) { 2902 // query to find all the calendars that match, for each 2903 // - delete calendar subscription 2904 // - delete calendar 2905 Cursor c = mDb.query(Tables.CALENDARS, sCalendarsIdProjection, selection, 2906 selectionArgs, 2907 null /* groupBy */, 2908 null /* having */, 2909 null /* sortOrder */); 2910 if (c == null) { 2911 return 0; 2912 } 2913 try { 2914 while (c.moveToNext()) { 2915 long id = c.getLong(CALENDARS_INDEX_ID); 2916 modifyCalendarSubscription(id, false /* not selected */); 2917 } 2918 } finally { 2919 c.close(); 2920 } 2921 return mDb.delete(Tables.CALENDARS, selection, selectionArgs); 2922 } 2923 2924 private Cursor getCursorForEventIdAndProjection(String eventId, String[] projection) { 2925 return mDb.query(Tables.EVENTS, 2926 projection, 2927 SQL_WHERE_ID, 2928 new String[] { eventId }, 2929 null /* group by */, 2930 null /* having */, 2931 null /* order by*/); 2932 } 2933 2934 private boolean doesEventExistForSyncId(String syncId) { 2935 if (syncId == null) { 2936 if (Log.isLoggable(TAG, Log.WARN)) { 2937 Log.w(TAG, "SyncID cannot be null: " + syncId); 2938 } 2939 return false; 2940 } 2941 long count = DatabaseUtils.longForQuery(mDb, SQL_SELECT_COUNT_FOR_SYNC_ID, 2942 new String[] { syncId }); 2943 return (count > 0); 2944 } 2945 2946 // Check if an UPDATE with STATUS_CANCEL means that we will need to do an Update (instead of 2947 // a Deletion) 2948 // 2949 // Deletion will be done only and only if: 2950 // - event status = canceled 2951 // - event is a recurrence exception that does not have its original (parent) event anymore 2952 // 2953 // This is due to the Server semantics that generate STATUS_CANCELED for both creation 2954 // and deletion of a recurrence exception 2955 // See bug #3218104 2956 private boolean doesStatusCancelUpdateMeanUpdate(String eventId, ContentValues values) { 2957 boolean isStatusCanceled = values.containsKey(Events.STATUS) && 2958 (values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED); 2959 if (isStatusCanceled) { 2960 Cursor cursor = null; 2961 try { 2962 cursor = getCursorForEventIdAndProjection(eventId, 2963 new String[] { Events.ORIGINAL_SYNC_ID }); 2964 if (!cursor.moveToFirst()) { 2965 if (Log.isLoggable(TAG, Log.WARN)) { 2966 Log.w(TAG, "Cannot find Event with id: " + eventId); 2967 } 2968 return false; 2969 } 2970 String originalSyncId = cursor.getString(0); 2971 2972 if (!TextUtils.isEmpty(originalSyncId)) { 2973 // This event is an exception. See if the recurring event still exists. 2974 return doesEventExistForSyncId(originalSyncId); 2975 } 2976 } finally { 2977 cursor.close(); 2978 } 2979 } 2980 // This is the normal case, we just want an UPDATE 2981 return true; 2982 } 2983 2984 // TODO: call calculateLastDate()! 2985 @Override 2986 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2987 String[] selectionArgs, boolean callerIsSyncAdapter) { 2988 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2989 Log.v(TAG, "updateInTransaction: " + uri); 2990 } 2991 final int match = sUriMatcher.match(uri); 2992 verifyTransactionAllowed(TRANSACTION_UPDATE, uri, values, callerIsSyncAdapter, match, 2993 selection, selectionArgs); 2994 2995 int count = 0; 2996 2997 // TODO: remove this restriction 2998 if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS 2999 && match != EVENTS && match != CALENDARS && match != PROVIDER_PROPERTIES) { 3000 throw new IllegalArgumentException("WHERE based updates not supported"); 3001 } 3002 3003 switch (match) { 3004 case SYNCSTATE: 3005 return mDbHelper.getSyncState().update(mDb, values, 3006 appendAccountToSelection(uri, selection), selectionArgs); 3007 3008 case SYNCSTATE_ID: { 3009 selection = appendAccountToSelection(uri, selection); 3010 String selectionWithId = (SyncState._ID + "=?") 3011 + (selection == null ? "" : " AND (" + selection + ")"); 3012 // Prepend id to selectionArgs 3013 selectionArgs = insertSelectionArg(selectionArgs, 3014 String.valueOf(ContentUris.parseId(uri))); 3015 return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs); 3016 } 3017 3018 case CALENDARS: 3019 case CALENDARS_ID: 3020 { 3021 long id; 3022 if (match == CALENDARS_ID) { 3023 if (selection != null) { 3024 throw new UnsupportedOperationException("Selection not permitted for " 3025 + uri); 3026 } 3027 id = ContentUris.parseId(uri); 3028 } else { 3029 // TODO: for supporting other sync adapters, we will need to 3030 // be able to deal with the following cases: 3031 // 1) selection to "_id=?" and pass in a selectionArgs 3032 // 2) selection to "_id IN (1, 2, 3)" 3033 // 3) selection to "delete=0 AND _id=1" 3034 if (selection != null && TextUtils.equals(selection,"_id=?")) { 3035 id = Long.parseLong(selectionArgs[0]); 3036 } else if (selection != null && selection.startsWith("_id=")) { 3037 // The ContentProviderOperation generates an _id=n string instead of 3038 // adding the id to the URL, so parse that out here. 3039 id = Long.parseLong(selection.substring(4)); 3040 } else { 3041 return mDb.update(Tables.CALENDARS, values, selection, selectionArgs); 3042 } 3043 } 3044 if (!callerIsSyncAdapter) { 3045 values.put(Calendars.DIRTY, 1); 3046 } 3047 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS); 3048 if (syncEvents != null) { 3049 modifyCalendarSubscription(id, syncEvents == 1); 3050 } 3051 3052 int result = mDb.update(Tables.CALENDARS, values, SQL_WHERE_ID, 3053 new String[] {String.valueOf(id)}); 3054 3055 if (result > 0) { 3056 // if visibility was toggled, we need to update alarms 3057 if (values.containsKey(Calendars.VISIBLE)) { 3058 // pass false for removeAlarms since the call to 3059 // scheduleNextAlarmLocked will remove any alarms for 3060 // non-visible events anyways. removeScheduledAlarmsLocked 3061 // does not actually have the effect we want 3062 mCalendarAlarm.scheduleNextAlarm(false); 3063 } 3064 // update the widget 3065 sendUpdateNotification(callerIsSyncAdapter); 3066 } 3067 3068 return result; 3069 } 3070 case EVENTS: 3071 case EVENTS_ID: 3072 { 3073 long id = 0; 3074 if (match == EVENTS_ID) { 3075 id = ContentUris.parseId(uri); 3076 } else if (callerIsSyncAdapter) { 3077 // TODO: same remark as for CALENDARS/CALENDARS_ID case as this is not 3078 // sufficient to deal with all the "_id" case in selection 3079 if (selection != null && selection.startsWith("_id=?")) { 3080 id = Long.parseLong(selectionArgs[0]); 3081 } else if (selection != null && selection.startsWith("_id=")) { 3082 // The ContentProviderOperation generates an _id=n string instead of 3083 // adding the id to the URL, so parse that out here. 3084 id = Long.parseLong(selection.substring(4)); 3085 } else { 3086 // Sync adapter Events operation affects just Events table, not associated 3087 // tables. 3088 if (fixAllDayTime(uri, values)) { 3089 if (Log.isLoggable(TAG, Log.WARN)) { 3090 Log.w(TAG, "updateInTransaction: Caller is sync adapter. " + 3091 "allDay is true but sec, min, hour were not 0."); 3092 } 3093 } 3094 return mDb.update("Events", values, selection, selectionArgs); 3095 } 3096 } else { 3097 throw new IllegalArgumentException("Unknown URL " + uri); 3098 } 3099 if (!callerIsSyncAdapter) { 3100 values.put(Events.DIRTY, 1); 3101 } 3102 // Disallow updating the attendee status in the Events 3103 // table. In the future, we could support this but we 3104 // would have to query and update the attendees table 3105 // to keep the values consistent. 3106 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) { 3107 throw new IllegalArgumentException("Updating " 3108 + Events.SELF_ATTENDEE_STATUS 3109 + " in Events table is not allowed."); 3110 } 3111 String strId = String.valueOf(id); 3112 // For taking care about recurrences exceptions cancelations, check if this needs 3113 // to be an UPDATE or a DELETE 3114 boolean isUpdate = doesStatusCancelUpdateMeanUpdate(strId, values); 3115 ContentValues updatedValues = new ContentValues(values); 3116 // TODO: should extend validateEventData to work with updates and call it here 3117 updatedValues = updateLastDate(updatedValues); 3118 if (updatedValues == null) { 3119 if (Log.isLoggable(TAG, Log.WARN)) { 3120 Log.w(TAG, "Could not update event."); 3121 } 3122 return 0; 3123 } 3124 // Make sure we pass in a uri with the id appended to fixAllDayTime 3125 Uri allDayUri; 3126 if (uri.getPathSegments().size() == 1) { 3127 allDayUri = ContentUris.withAppendedId(uri, id); 3128 } else { 3129 allDayUri = uri; 3130 } 3131 if (fixAllDayTime(allDayUri, updatedValues)) { 3132 if (Log.isLoggable(TAG, Log.WARN)) { 3133 Log.w(TAG, "updateInTransaction: " + 3134 "allDay is true but sec, min, hour were not 0."); 3135 } 3136 } 3137 3138 int result; 3139 3140 if (isUpdate) { 3141 // If a user made a change, possibly duplicate the event so we can do a partial 3142 // update. If a sync adapter made a change and that change marks an event as 3143 // un-dirty, remove any duplicates that may have been created earlier. 3144 if (!callerIsSyncAdapter) { 3145 mDbHelper.duplicateEvent(id); 3146 } else { 3147 if (values.containsKey(Events.DIRTY) 3148 && values.getAsInteger(Events.DIRTY) == 0) { 3149 mDbHelper.removeDuplicateEvent(id); 3150 } 3151 } 3152 result = mDb.update(Tables.EVENTS, updatedValues, SQL_WHERE_ID, 3153 new String[] { strId }); 3154 if (result > 0) { 3155 updateEventRawTimesLocked(id, updatedValues); 3156 mInstancesHelper.updateInstancesLocked(updatedValues, id, 3157 false /* not a new event */, mDb); 3158 3159 if (values.containsKey(Events.DTSTART) || 3160 values.containsKey(Events.STATUS)) { 3161 // If this is a cancellation knock it out 3162 // of the instances table 3163 if (values.containsKey(Events.STATUS) && 3164 values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED) { 3165 String[] args = new String[] {String.valueOf(id)}; 3166 mDb.delete(Tables.INSTANCES, SQL_WHERE_EVENT_ID, args); 3167 } 3168 3169 // The start time of the event changed, so run the 3170 // event alarm scheduler. 3171 if (Log.isLoggable(TAG, Log.DEBUG)) { 3172 Log.d(TAG, "updateInternal() changing event"); 3173 } 3174 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 3175 } 3176 3177 sendUpdateNotification(id, callerIsSyncAdapter); 3178 } 3179 } else { 3180 result = deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */); 3181 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 3182 sendUpdateNotification(callerIsSyncAdapter); 3183 } 3184 3185 return result; 3186 } 3187 case ATTENDEES_ID: { 3188 if (selection != null) { 3189 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3190 } 3191 // Copy the attendee status value to the Events table. 3192 updateEventAttendeeStatus(mDb, values); 3193 3194 if (callerIsSyncAdapter) { 3195 long id = ContentUris.parseId(uri); 3196 return mDb.update(Tables.ATTENDEES, values, SQL_WHERE_ID, 3197 new String[] {String.valueOf(id)}); 3198 } else { 3199 return updateInTable(Tables.ATTENDEES, values, uri, null /* selection */, 3200 null /* selectionArgs */); 3201 } 3202 } 3203 case CALENDAR_ALERTS_ID: { 3204 if (selection != null) { 3205 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3206 } 3207 // Note: dirty bit is not set for Alerts because it is not synced. 3208 // It is generated from Reminders, which is synced. 3209 long id = ContentUris.parseId(uri); 3210 return mDb.update(Tables.CALENDAR_ALERTS, values, SQL_WHERE_ID, 3211 new String[] {String.valueOf(id)}); 3212 } 3213 case CALENDAR_ALERTS: { 3214 // Note: dirty bit is not set for Alerts because it is not synced. 3215 // It is generated from Reminders, which is synced. 3216 return mDb.update(Tables.CALENDAR_ALERTS, values, selection, selectionArgs); 3217 } 3218 case REMINDERS_ID: { 3219 if (selection != null) { 3220 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3221 } 3222 if (callerIsSyncAdapter) { 3223 long id = ContentUris.parseId(uri); 3224 count = mDb.update(Tables.REMINDERS, values, SQL_WHERE_ID, 3225 new String[] {String.valueOf(id)}); 3226 } else { 3227 count = updateInTable(Tables.REMINDERS, values, uri, null /* selection */, 3228 null /* selectionArgs */); 3229 } 3230 3231 // Reschedule the event alarms because the 3232 // "minutes" field may have changed. 3233 if (Log.isLoggable(TAG, Log.DEBUG)) { 3234 Log.d(TAG, "updateInternal() changing reminder"); 3235 } 3236 mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */); 3237 return count; 3238 } 3239 case EXTENDED_PROPERTIES_ID: { 3240 if (selection != null) { 3241 throw new UnsupportedOperationException("Selection not permitted for " + uri); 3242 } 3243 if (callerIsSyncAdapter) { 3244 long id = ContentUris.parseId(uri); 3245 return mDb.update(Tables.EXTENDED_PROPERTIES, values, SQL_WHERE_ID, 3246 new String[] {String.valueOf(id)}); 3247 } else { 3248 return updateInTable(Tables.EXTENDED_PROPERTIES, values, uri, 3249 null /* selection */, null /* selectionArgs */); 3250 } 3251 } 3252 // TODO: replace the SCHEDULE_ALARM private URIs with a 3253 // service 3254 case SCHEDULE_ALARM: { 3255 mCalendarAlarm.scheduleNextAlarm(false); 3256 return 0; 3257 } 3258 case SCHEDULE_ALARM_REMOVE: { 3259 mCalendarAlarm.scheduleNextAlarm(true); 3260 return 0; 3261 } 3262 3263 case PROVIDER_PROPERTIES: { 3264 if (selection == null) { 3265 throw new UnsupportedOperationException("Selection cannot be null for " + uri); 3266 } 3267 if (!selection.equals("key=?")) { 3268 throw new UnsupportedOperationException("Selection should be key=? for " + uri); 3269 } 3270 3271 List<String> list = Arrays.asList(selectionArgs); 3272 3273 if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) { 3274 throw new UnsupportedOperationException("Invalid selection key: " + 3275 CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + " for " + uri); 3276 } 3277 3278 // Before it may be changed, save current Instances timezone for later use 3279 String timezoneInstancesBeforeUpdate = mCalendarCache.readTimezoneInstances(); 3280 3281 // Update the database with the provided values (this call may change the value 3282 // of timezone Instances) 3283 int result = mDb.update(Tables.CALENDAR_CACHE, values, selection, selectionArgs); 3284 3285 // if successful, do some house cleaning: 3286 // if the timezone type is set to "home", set the Instances 3287 // timezone to the previous 3288 // if the timezone type is set to "auto", set the Instances 3289 // timezone to the current 3290 // device one 3291 // if the timezone Instances is set AND if we are in "home" 3292 // timezone type, then save the timezone Instance into 3293 // "previous" too 3294 if (result > 0) { 3295 // If we are changing timezone type... 3296 if (list.contains(CalendarCache.KEY_TIMEZONE_TYPE)) { 3297 String value = values.getAsString(CalendarCache.COLUMN_NAME_VALUE); 3298 if (value != null) { 3299 // if we are setting timezone type to "home" 3300 if (value.equals(CalendarCache.TIMEZONE_TYPE_HOME)) { 3301 String previousTimezone = 3302 mCalendarCache.readTimezoneInstancesPrevious(); 3303 if (previousTimezone != null) { 3304 mCalendarCache.writeTimezoneInstances(previousTimezone); 3305 } 3306 // Regenerate Instances if the "home" timezone has changed 3307 // and notify widgets 3308 if (!timezoneInstancesBeforeUpdate.equals(previousTimezone) ) { 3309 regenerateInstancesTable(); 3310 sendUpdateNotification(callerIsSyncAdapter); 3311 } 3312 } 3313 // if we are setting timezone type to "auto" 3314 else if (value.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) { 3315 String localTimezone = TimeZone.getDefault().getID(); 3316 mCalendarCache.writeTimezoneInstances(localTimezone); 3317 if (!timezoneInstancesBeforeUpdate.equals(localTimezone)) { 3318 regenerateInstancesTable(); 3319 sendUpdateNotification(callerIsSyncAdapter); 3320 } 3321 } 3322 } 3323 } 3324 // If we are changing timezone Instances... 3325 else if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES)) { 3326 // if we are in "home" timezone type... 3327 if (isHomeTimezone()) { 3328 String timezoneInstances = mCalendarCache.readTimezoneInstances(); 3329 // Update the previous value 3330 mCalendarCache.writeTimezoneInstancesPrevious(timezoneInstances); 3331 // Recompute Instances if the "home" timezone has changed 3332 // and send notifications to any widgets 3333 if (timezoneInstancesBeforeUpdate != null && 3334 !timezoneInstancesBeforeUpdate.equals(timezoneInstances)) { 3335 regenerateInstancesTable(); 3336 sendUpdateNotification(callerIsSyncAdapter); 3337 } 3338 } 3339 } 3340 } 3341 return result; 3342 } 3343 3344 default: 3345 throw new IllegalArgumentException("Unknown URL " + uri); 3346 } 3347 } 3348 3349 private String appendAccountFromParameterToSelection(String selection, Uri uri) { 3350 final String accountName = QueryParameterUtils.getQueryParameter(uri, 3351 CalendarContract.EventsEntity.ACCOUNT_NAME); 3352 final String accountType = QueryParameterUtils.getQueryParameter(uri, 3353 CalendarContract.EventsEntity.ACCOUNT_TYPE); 3354 if (!TextUtils.isEmpty(accountName)) { 3355 final StringBuilder sb = new StringBuilder(); 3356 sb.append(Calendars.ACCOUNT_NAME + "=") 3357 .append(DatabaseUtils.sqlEscapeString(accountName)) 3358 .append(" AND ") 3359 .append(Calendars.ACCOUNT_TYPE) 3360 .append(" = ") 3361 .append(DatabaseUtils.sqlEscapeString(accountType)); 3362 return appendSelection(sb, selection); 3363 } else { 3364 return selection; 3365 } 3366 } 3367 3368 private String appendLastSyncedColumnToSelection(String selection, Uri uri) { 3369 if (getIsCallerSyncAdapter(uri)) { 3370 return selection; 3371 } 3372 final StringBuilder sb = new StringBuilder(); 3373 sb.append(CalendarContract.Events.LAST_SYNCED).append(" = 0"); 3374 return appendSelection(sb, selection); 3375 } 3376 3377 private String appendAccountToSelection(Uri uri, String selection) { 3378 final String accountName = QueryParameterUtils.getQueryParameter(uri, 3379 CalendarContract.EventsEntity.ACCOUNT_NAME); 3380 final String accountType = QueryParameterUtils.getQueryParameter(uri, 3381 CalendarContract.EventsEntity.ACCOUNT_TYPE); 3382 if (!TextUtils.isEmpty(accountName)) { 3383 StringBuilder selectionSb = new StringBuilder(CalendarContract.Calendars.ACCOUNT_NAME 3384 + "=" + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3385 + CalendarContract.Calendars.ACCOUNT_TYPE + "=" 3386 + DatabaseUtils.sqlEscapeString(accountType)); 3387 return appendSelection(selectionSb, selection); 3388 } else { 3389 return selection; 3390 } 3391 } 3392 3393 private String appendSyncAccountToSelection(Uri uri, String selection) { 3394 final String accountName = QueryParameterUtils.getQueryParameter(uri, 3395 CalendarContract.EventsEntity.ACCOUNT_NAME); 3396 final String accountType = QueryParameterUtils.getQueryParameter(uri, 3397 CalendarContract.EventsEntity.ACCOUNT_TYPE); 3398 if (!TextUtils.isEmpty(accountName)) { 3399 StringBuilder selectionSb = new StringBuilder(CalendarContract.Events.ACCOUNT_NAME + "=" 3400 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3401 + CalendarContract.Events.ACCOUNT_TYPE + "=" 3402 + DatabaseUtils.sqlEscapeString(accountType)); 3403 return appendSelection(selectionSb, selection); 3404 } else { 3405 return selection; 3406 } 3407 } 3408 3409 private String appendSelection(StringBuilder sb, String selection) { 3410 if (!TextUtils.isEmpty(selection)) { 3411 sb.append(" AND ("); 3412 sb.append(selection); 3413 sb.append(')'); 3414 } 3415 return sb.toString(); 3416 } 3417 3418 /** 3419 * Verifies that the operation is allowed and throws an exception if it 3420 * isn't. This defines the limits of a sync adapter call vs an app call. 3421 * 3422 * @param type The type of call, {@link #TRANSACTION_QUERY}, 3423 * {@link #TRANSACTION_INSERT}, {@link #TRANSACTION_UPDATE}, or 3424 * {@link #TRANSACTION_DELETE} 3425 * @param uri 3426 * @param values 3427 * @param isSyncAdapter 3428 */ 3429 private void verifyTransactionAllowed(int type, Uri uri, ContentValues values, 3430 boolean isSyncAdapter, int uriMatch, String selection, String[] selectionArgs) { 3431 switch (type) { 3432 case TRANSACTION_QUERY: 3433 return; 3434 case TRANSACTION_INSERT: 3435 if (uriMatch == INSTANCES) { 3436 throw new UnsupportedOperationException( 3437 "Inserting into instances not supported"); 3438 } 3439 // Check there are no columns restricted to the provider 3440 verifyColumns(values, uriMatch); 3441 if (isSyncAdapter) { 3442 // check that account and account type are specified 3443 verifyHasAccount(uri, selection, selectionArgs); 3444 } else { 3445 // check that sync only columns aren't included 3446 verifyNoSyncColumns(values, uriMatch); 3447 } 3448 return; 3449 case TRANSACTION_UPDATE: 3450 if (uriMatch == INSTANCES) { 3451 throw new UnsupportedOperationException("Updating instances not supported"); 3452 } 3453 // Check there are no columns restricted to the provider 3454 verifyColumns(values, uriMatch); 3455 if (isSyncAdapter) { 3456 // check that account and account type are specified 3457 verifyHasAccount(uri, selection, selectionArgs); 3458 } else { 3459 // check that sync only columns aren't included 3460 verifyNoSyncColumns(values, uriMatch); 3461 } 3462 return; 3463 case TRANSACTION_DELETE: 3464 if (uriMatch == INSTANCES) { 3465 throw new UnsupportedOperationException("Deleting instances not supported"); 3466 } 3467 if (isSyncAdapter) { 3468 // check that account and account type are specified 3469 verifyHasAccount(uri, selection, selectionArgs); 3470 } 3471 return; 3472 } 3473 } 3474 3475 private void verifyHasAccount(Uri uri, String selection, String[] selectionArgs) { 3476 String accountName = QueryParameterUtils.getQueryParameter(uri, Calendars.ACCOUNT_NAME); 3477 String accountType = QueryParameterUtils.getQueryParameter(uri, 3478 Calendars.ACCOUNT_TYPE); 3479 if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { 3480 if (selection != null && selection.startsWith(ACCOUNT_SELECTION_PREFIX)) { 3481 accountName = selectionArgs[0]; 3482 accountType = selectionArgs[1]; 3483 } 3484 } 3485 if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { 3486 throw new IllegalArgumentException( 3487 "Sync adapters must specify an account and account type: " + uri); 3488 } 3489 } 3490 3491 private void verifyColumns(ContentValues values, int uriMatch) { 3492 if (values == null || values.size() == 0) { 3493 return; 3494 } 3495 String[] columns; 3496 switch (uriMatch) { 3497 case EVENTS: 3498 case EVENTS_ID: 3499 case EVENT_ENTITIES: 3500 case EVENT_ENTITIES_ID: 3501 columns = Events.PROVIDER_WRITABLE_COLUMNS; 3502 break; 3503 default: 3504 columns = PROVIDER_WRITABLE_DEFAULT_COLUMNS; 3505 break; 3506 } 3507 3508 for (int i = 0; i < columns.length; i++) { 3509 if (values.containsKey(columns[i])) { 3510 throw new IllegalArgumentException("Only the provider may write to " + columns[i]); 3511 } 3512 } 3513 } 3514 3515 private void verifyNoSyncColumns(ContentValues values, int uriMatch) { 3516 if (values == null || values.size() == 0) { 3517 return; 3518 } 3519 String[] syncColumns; 3520 switch (uriMatch) { 3521 case CALENDARS: 3522 case CALENDARS_ID: 3523 case CALENDAR_ENTITIES: 3524 case CALENDAR_ENTITIES_ID: 3525 syncColumns = Calendars.SYNC_WRITABLE_COLUMNS; 3526 break; 3527 case EVENTS: 3528 case EVENTS_ID: 3529 case EVENT_ENTITIES: 3530 case EVENT_ENTITIES_ID: 3531 syncColumns = Events.SYNC_WRITABLE_COLUMNS; 3532 break; 3533 default: 3534 syncColumns = SYNC_WRITABLE_DEFAULT_COLUMNS; 3535 break; 3536 3537 } 3538 for (int i = 0; i < syncColumns.length; i++) { 3539 if (values.containsKey(syncColumns[i])) { 3540 throw new IllegalArgumentException("Only sync adapters may write to " 3541 + syncColumns[i]); 3542 } 3543 } 3544 } 3545 3546 private void modifyCalendarSubscription(long id, boolean syncEvents) { 3547 // get the account, url, and current selected state 3548 // for this calendar. 3549 Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id), 3550 new String[] {Calendars.ACCOUNT_NAME, Calendars.ACCOUNT_TYPE, 3551 Calendars.CAL_SYNC1, Calendars.SYNC_EVENTS}, 3552 null /* selection */, 3553 null /* selectionArgs */, 3554 null /* sort */); 3555 3556 Account account = null; 3557 String calendarUrl = null; 3558 boolean oldSyncEvents = false; 3559 if (cursor != null) { 3560 try { 3561 if (cursor.moveToFirst()) { 3562 final String accountName = cursor.getString(0); 3563 final String accountType = cursor.getString(1); 3564 account = new Account(accountName, accountType); 3565 calendarUrl = cursor.getString(2); 3566 oldSyncEvents = (cursor.getInt(3) != 0); 3567 } 3568 } finally { 3569 cursor.close(); 3570 } 3571 } 3572 3573 if (account == null) { 3574 // should not happen? 3575 if (Log.isLoggable(TAG, Log.WARN)) { 3576 Log.w(TAG, "Cannot update subscription because account " 3577 + "is empty -- should not happen."); 3578 } 3579 return; 3580 } 3581 3582 if (TextUtils.isEmpty(calendarUrl)) { 3583 // Passing in a null Url will cause it to not add any extras 3584 // Should only happen for non-google calendars. 3585 calendarUrl = null; 3586 } 3587 3588 if (oldSyncEvents == syncEvents) { 3589 // nothing to do 3590 return; 3591 } 3592 3593 // If the calendar is not selected for syncing, then don't download 3594 // events. 3595 mDbHelper.scheduleSync(account, !syncEvents, calendarUrl); 3596 } 3597 3598 /** 3599 * Call this to trigger a broadcast of the ACTION_PROVIDER_CHANGED intent. 3600 * This also provides a timeout, so any calls to this method will be batched 3601 * over a period of BROADCAST_TIMEOUT_MILLIS defined in this class. 3602 * 3603 * @param callerIsSyncAdapter whether or not the update is being triggered by a sync 3604 */ 3605 private void sendUpdateNotification(boolean callerIsSyncAdapter) { 3606 // We use -1 to represent an update to all events 3607 sendUpdateNotification(-1, callerIsSyncAdapter); 3608 } 3609 3610 /** 3611 * Call this to trigger a broadcast of the ACTION_PROVIDER_CHANGED intent. 3612 * This also provides a timeout, so any calls to this method will be batched 3613 * over a period of BROADCAST_TIMEOUT_MILLIS defined in this class. The 3614 * actual sending of the intent is done in 3615 * {@link #doSendUpdateNotification()}. 3616 * 3617 * TODO add support for eventId 3618 * 3619 * @param eventId the ID of the event that changed, or -1 for no specific event 3620 * @param callerIsSyncAdapter whether or not the update is being triggered by a sync 3621 */ 3622 private void sendUpdateNotification(long eventId, 3623 boolean callerIsSyncAdapter) { 3624 // Are there any pending broadcast requests? 3625 if (mBroadcastHandler.hasMessages(UPDATE_BROADCAST_MSG)) { 3626 // Delete any pending requests, before requeuing a fresh one 3627 mBroadcastHandler.removeMessages(UPDATE_BROADCAST_MSG); 3628 } else { 3629 // Because the handler does not guarantee message delivery in 3630 // the case that the provider is killed, we need to make sure 3631 // that the provider stays alive long enough to deliver the 3632 // notification. This empty service is sufficient to "wedge" the 3633 // process until we stop it here. 3634 mContext.startService(new Intent(mContext, EmptyService.class)); 3635 } 3636 // We use a much longer delay for sync-related updates, to prevent any 3637 // receivers from slowing down the sync 3638 long delay = callerIsSyncAdapter ? 3639 SYNC_UPDATE_BROADCAST_TIMEOUT_MILLIS : 3640 UPDATE_BROADCAST_TIMEOUT_MILLIS; 3641 // Despite the fact that we actually only ever use one message at a time 3642 // for now, it is really important to call obtainMessage() to get a 3643 // clean instance. This avoids potentially infinite loops resulting 3644 // adding the same instance to the message queue twice, since the 3645 // message queue implements its linked list using a field from Message. 3646 Message msg = mBroadcastHandler.obtainMessage(UPDATE_BROADCAST_MSG); 3647 mBroadcastHandler.sendMessageDelayed(msg, delay); 3648 } 3649 3650 /** 3651 * This method should not ever be called directly, to prevent sending too 3652 * many potentially expensive broadcasts. Instead, call 3653 * {@link #sendUpdateNotification(boolean)} instead. 3654 * 3655 * @see #sendUpdateNotification(boolean) 3656 */ 3657 private void doSendUpdateNotification() { 3658 Intent intent = new Intent(Intent.ACTION_PROVIDER_CHANGED, 3659 CalendarContract.CONTENT_URI); 3660 if (Log.isLoggable(TAG, Log.INFO)) { 3661 Log.i(TAG, "Sending notification intent: " + intent); 3662 } 3663 mContext.sendBroadcast(intent, null); 3664 } 3665 3666 private static final int TRANSACTION_QUERY = 0; 3667 private static final int TRANSACTION_INSERT = 1; 3668 private static final int TRANSACTION_UPDATE = 2; 3669 private static final int TRANSACTION_DELETE = 3; 3670 3671 // @formatter:off 3672 private static final String[] SYNC_WRITABLE_DEFAULT_COLUMNS = new String[] { 3673 CalendarContract.Calendars.DIRTY, 3674 CalendarContract.Calendars._SYNC_ID 3675 }; 3676 private static final String[] PROVIDER_WRITABLE_DEFAULT_COLUMNS = new String[] { 3677 }; 3678 // @formatter:on 3679 3680 private static final int EVENTS = 1; 3681 private static final int EVENTS_ID = 2; 3682 private static final int INSTANCES = 3; 3683 private static final int CALENDARS = 4; 3684 private static final int CALENDARS_ID = 5; 3685 private static final int ATTENDEES = 6; 3686 private static final int ATTENDEES_ID = 7; 3687 private static final int REMINDERS = 8; 3688 private static final int REMINDERS_ID = 9; 3689 private static final int EXTENDED_PROPERTIES = 10; 3690 private static final int EXTENDED_PROPERTIES_ID = 11; 3691 private static final int CALENDAR_ALERTS = 12; 3692 private static final int CALENDAR_ALERTS_ID = 13; 3693 private static final int CALENDAR_ALERTS_BY_INSTANCE = 14; 3694 private static final int INSTANCES_BY_DAY = 15; 3695 private static final int SYNCSTATE = 16; 3696 private static final int SYNCSTATE_ID = 17; 3697 private static final int EVENT_ENTITIES = 18; 3698 private static final int EVENT_ENTITIES_ID = 19; 3699 private static final int EVENT_DAYS = 20; 3700 private static final int SCHEDULE_ALARM = 21; 3701 private static final int SCHEDULE_ALARM_REMOVE = 22; 3702 private static final int TIME = 23; 3703 private static final int CALENDAR_ENTITIES = 24; 3704 private static final int CALENDAR_ENTITIES_ID = 25; 3705 private static final int INSTANCES_SEARCH = 26; 3706 private static final int INSTANCES_SEARCH_BY_DAY = 27; 3707 private static final int PROVIDER_PROPERTIES = 28; 3708 private static final int EXCEPTION_ID = 29; 3709 private static final int EXCEPTION_ID2 = 30; 3710 3711 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 3712 private static final HashMap<String, String> sInstancesProjectionMap; 3713 protected static final HashMap<String, String> sEventsProjectionMap; 3714 private static final HashMap<String, String> sEventEntitiesProjectionMap; 3715 private static final HashMap<String, String> sAttendeesProjectionMap; 3716 private static final HashMap<String, String> sRemindersProjectionMap; 3717 private static final HashMap<String, String> sCalendarAlertsProjectionMap; 3718 private static final HashMap<String, String> sCalendarCacheProjectionMap; 3719 private static final HashMap<String, String> sCountProjectionMap; 3720 3721 static { 3722 sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/when/*/*", INSTANCES); 3723 sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY); 3724 sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/search/*/*/*", INSTANCES_SEARCH); 3725 sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/searchbyday/*/*/*", 3726 INSTANCES_SEARCH_BY_DAY); 3727 sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS); 3728 sUriMatcher.addURI(CalendarContract.AUTHORITY, "events", EVENTS); 3729 sUriMatcher.addURI(CalendarContract.AUTHORITY, "events/#", EVENTS_ID); 3730 sUriMatcher.addURI(CalendarContract.AUTHORITY, "event_entities", EVENT_ENTITIES); 3731 sUriMatcher.addURI(CalendarContract.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID); 3732 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendars", CALENDARS); 3733 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendars/#", CALENDARS_ID); 3734 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_entities", CALENDAR_ENTITIES); 3735 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_entities/#", CALENDAR_ENTITIES_ID); 3736 sUriMatcher.addURI(CalendarContract.AUTHORITY, "attendees", ATTENDEES); 3737 sUriMatcher.addURI(CalendarContract.AUTHORITY, "attendees/#", ATTENDEES_ID); 3738 sUriMatcher.addURI(CalendarContract.AUTHORITY, "reminders", REMINDERS); 3739 sUriMatcher.addURI(CalendarContract.AUTHORITY, "reminders/#", REMINDERS_ID); 3740 sUriMatcher.addURI(CalendarContract.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES); 3741 sUriMatcher.addURI(CalendarContract.AUTHORITY, "extendedproperties/#", 3742 EXTENDED_PROPERTIES_ID); 3743 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS); 3744 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID); 3745 sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_alerts/by_instance", 3746 CALENDAR_ALERTS_BY_INSTANCE); 3747 sUriMatcher.addURI(CalendarContract.AUTHORITY, "syncstate", SYNCSTATE); 3748 sUriMatcher.addURI(CalendarContract.AUTHORITY, "syncstate/#", SYNCSTATE_ID); 3749 sUriMatcher.addURI(CalendarContract.AUTHORITY, CalendarAlarmManager.SCHEDULE_ALARM_PATH, 3750 SCHEDULE_ALARM); 3751 sUriMatcher.addURI(CalendarContract.AUTHORITY, 3752 CalendarAlarmManager.SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE); 3753 sUriMatcher.addURI(CalendarContract.AUTHORITY, "time/#", TIME); 3754 sUriMatcher.addURI(CalendarContract.AUTHORITY, "time", TIME); 3755 sUriMatcher.addURI(CalendarContract.AUTHORITY, "properties", PROVIDER_PROPERTIES); 3756 sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#", EXCEPTION_ID); 3757 sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#/#", EXCEPTION_ID2); 3758 3759 /** Contains just BaseColumns._COUNT */ 3760 sCountProjectionMap = new HashMap<String, String>(); 3761 sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)"); 3762 3763 sEventsProjectionMap = new HashMap<String, String>(); 3764 // Events columns 3765 sEventsProjectionMap.put(Events.ACCOUNT_NAME, Events.ACCOUNT_NAME); 3766 sEventsProjectionMap.put(Events.ACCOUNT_TYPE, Events.ACCOUNT_TYPE); 3767 sEventsProjectionMap.put(Events.TITLE, Events.TITLE); 3768 sEventsProjectionMap.put(Events.EVENT_LOCATION, Events.EVENT_LOCATION); 3769 sEventsProjectionMap.put(Events.DESCRIPTION, Events.DESCRIPTION); 3770 sEventsProjectionMap.put(Events.STATUS, Events.STATUS); 3771 sEventsProjectionMap.put(Events.EVENT_COLOR, Events.EVENT_COLOR); 3772 sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, Events.SELF_ATTENDEE_STATUS); 3773 sEventsProjectionMap.put(Events.DTSTART, Events.DTSTART); 3774 sEventsProjectionMap.put(Events.DTEND, Events.DTEND); 3775 sEventsProjectionMap.put(Events.EVENT_TIMEZONE, Events.EVENT_TIMEZONE); 3776 sEventsProjectionMap.put(Events.EVENT_END_TIMEZONE, Events.EVENT_END_TIMEZONE); 3777 sEventsProjectionMap.put(Events.DURATION, Events.DURATION); 3778 sEventsProjectionMap.put(Events.ALL_DAY, Events.ALL_DAY); 3779 sEventsProjectionMap.put(Events.ACCESS_LEVEL, Events.ACCESS_LEVEL); 3780 sEventsProjectionMap.put(Events.AVAILABILITY, Events.AVAILABILITY); 3781 sEventsProjectionMap.put(Events.HAS_ALARM, Events.HAS_ALARM); 3782 sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, Events.HAS_EXTENDED_PROPERTIES); 3783 sEventsProjectionMap.put(Events.RRULE, Events.RRULE); 3784 sEventsProjectionMap.put(Events.RDATE, Events.RDATE); 3785 sEventsProjectionMap.put(Events.EXRULE, Events.EXRULE); 3786 sEventsProjectionMap.put(Events.EXDATE, Events.EXDATE); 3787 sEventsProjectionMap.put(Events.ORIGINAL_SYNC_ID, Events.ORIGINAL_SYNC_ID); 3788 sEventsProjectionMap.put(Events.ORIGINAL_ID, Events.ORIGINAL_ID); 3789 sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, Events.ORIGINAL_INSTANCE_TIME); 3790 sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_ALL_DAY); 3791 sEventsProjectionMap.put(Events.LAST_DATE, Events.LAST_DATE); 3792 sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, Events.HAS_ATTENDEE_DATA); 3793 sEventsProjectionMap.put(Events.CALENDAR_ID, Events.CALENDAR_ID); 3794 sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, Events.GUESTS_CAN_INVITE_OTHERS); 3795 sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, Events.GUESTS_CAN_MODIFY); 3796 sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, Events.GUESTS_CAN_SEE_GUESTS); 3797 sEventsProjectionMap.put(Events.ORGANIZER, Events.ORGANIZER); 3798 sEventsProjectionMap.put(Events.DELETED, Events.DELETED); 3799 sEventsProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID); 3800 3801 // Put the shared items into the Attendees, Reminders projection map 3802 sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3803 sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3804 3805 // Calendar columns 3806 sEventsProjectionMap.put(Calendars.CALENDAR_COLOR, Calendars.CALENDAR_COLOR); 3807 sEventsProjectionMap.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CALENDAR_ACCESS_LEVEL); 3808 sEventsProjectionMap.put(Calendars.VISIBLE, Calendars.VISIBLE); 3809 sEventsProjectionMap.put(Calendars.CALENDAR_TIME_ZONE, Calendars.CALENDAR_TIME_ZONE); 3810 sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, Calendars.OWNER_ACCOUNT); 3811 sEventsProjectionMap.put(Calendars.CALENDAR_DISPLAY_NAME, Calendars.CALENDAR_DISPLAY_NAME); 3812 sEventsProjectionMap.put(Calendars.ALLOWED_REMINDERS, Calendars.ALLOWED_REMINDERS); 3813 sEventsProjectionMap.put(Calendars.MAX_REMINDERS, Calendars.MAX_REMINDERS); 3814 sEventsProjectionMap.put(Calendars.CAN_ORGANIZER_RESPOND, Calendars.CAN_ORGANIZER_RESPOND); 3815 sEventsProjectionMap.put(Calendars.CAN_MODIFY_TIME_ZONE, Calendars.CAN_MODIFY_TIME_ZONE); 3816 3817 // Put the shared items into the Instances projection map 3818 // The Instances and CalendarAlerts are joined with Calendars, so the projections include 3819 // the above Calendar columns. 3820 sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3821 sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap); 3822 3823 sEventsProjectionMap.put(Events._ID, Events._ID); 3824 sEventsProjectionMap.put(Events.SYNC_DATA1, Events.SYNC_DATA1); 3825 sEventsProjectionMap.put(Events.SYNC_DATA2, Events.SYNC_DATA2); 3826 sEventsProjectionMap.put(Events.SYNC_DATA3, Events.SYNC_DATA3); 3827 sEventsProjectionMap.put(Events.SYNC_DATA4, Events.SYNC_DATA4); 3828 sEventsProjectionMap.put(Events.SYNC_DATA5, Events.SYNC_DATA5); 3829 sEventsProjectionMap.put(Events.SYNC_DATA6, Events.SYNC_DATA6); 3830 sEventsProjectionMap.put(Events.SYNC_DATA7, Events.SYNC_DATA7); 3831 sEventsProjectionMap.put(Events.SYNC_DATA8, Events.SYNC_DATA8); 3832 sEventsProjectionMap.put(Events.SYNC_DATA9, Events.SYNC_DATA9); 3833 sEventsProjectionMap.put(Events.SYNC_DATA10, Events.SYNC_DATA10); 3834 sEventsProjectionMap.put(Calendars.CAL_SYNC1, Calendars.CAL_SYNC1); 3835 sEventsProjectionMap.put(Calendars.CAL_SYNC2, Calendars.CAL_SYNC2); 3836 sEventsProjectionMap.put(Calendars.CAL_SYNC3, Calendars.CAL_SYNC3); 3837 sEventsProjectionMap.put(Calendars.CAL_SYNC4, Calendars.CAL_SYNC4); 3838 sEventsProjectionMap.put(Calendars.CAL_SYNC5, Calendars.CAL_SYNC5); 3839 sEventsProjectionMap.put(Calendars.CAL_SYNC6, Calendars.CAL_SYNC6); 3840 sEventsProjectionMap.put(Calendars.CAL_SYNC7, Calendars.CAL_SYNC7); 3841 sEventsProjectionMap.put(Calendars.CAL_SYNC8, Calendars.CAL_SYNC8); 3842 sEventsProjectionMap.put(Calendars.CAL_SYNC9, Calendars.CAL_SYNC9); 3843 sEventsProjectionMap.put(Calendars.CAL_SYNC10, Calendars.CAL_SYNC10); 3844 sEventsProjectionMap.put(Events.DIRTY, Events.DIRTY); 3845 sEventsProjectionMap.put(Events.LAST_SYNCED, Events.LAST_SYNCED); 3846 3847 sEventEntitiesProjectionMap = new HashMap<String, String>(); 3848 sEventEntitiesProjectionMap.put(Events.TITLE, Events.TITLE); 3849 sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, Events.EVENT_LOCATION); 3850 sEventEntitiesProjectionMap.put(Events.DESCRIPTION, Events.DESCRIPTION); 3851 sEventEntitiesProjectionMap.put(Events.STATUS, Events.STATUS); 3852 sEventEntitiesProjectionMap.put(Events.EVENT_COLOR, Events.EVENT_COLOR); 3853 sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, Events.SELF_ATTENDEE_STATUS); 3854 sEventEntitiesProjectionMap.put(Events.DTSTART, Events.DTSTART); 3855 sEventEntitiesProjectionMap.put(Events.DTEND, Events.DTEND); 3856 sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, Events.EVENT_TIMEZONE); 3857 sEventEntitiesProjectionMap.put(Events.EVENT_END_TIMEZONE, Events.EVENT_END_TIMEZONE); 3858 sEventEntitiesProjectionMap.put(Events.DURATION, Events.DURATION); 3859 sEventEntitiesProjectionMap.put(Events.ALL_DAY, Events.ALL_DAY); 3860 sEventEntitiesProjectionMap.put(Events.ACCESS_LEVEL, Events.ACCESS_LEVEL); 3861 sEventEntitiesProjectionMap.put(Events.AVAILABILITY, Events.AVAILABILITY); 3862 sEventEntitiesProjectionMap.put(Events.HAS_ALARM, Events.HAS_ALARM); 3863 sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, 3864 Events.HAS_EXTENDED_PROPERTIES); 3865 sEventEntitiesProjectionMap.put(Events.RRULE, Events.RRULE); 3866 sEventEntitiesProjectionMap.put(Events.RDATE, Events.RDATE); 3867 sEventEntitiesProjectionMap.put(Events.EXRULE, Events.EXRULE); 3868 sEventEntitiesProjectionMap.put(Events.EXDATE, Events.EXDATE); 3869 sEventEntitiesProjectionMap.put(Events.ORIGINAL_SYNC_ID, Events.ORIGINAL_SYNC_ID); 3870 sEventEntitiesProjectionMap.put(Events.ORIGINAL_ID, Events.ORIGINAL_ID); 3871 sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, 3872 Events.ORIGINAL_INSTANCE_TIME); 3873 sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_ALL_DAY); 3874 sEventEntitiesProjectionMap.put(Events.LAST_DATE, Events.LAST_DATE); 3875 sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, Events.HAS_ATTENDEE_DATA); 3876 sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, Events.CALENDAR_ID); 3877 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, 3878 Events.GUESTS_CAN_INVITE_OTHERS); 3879 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, Events.GUESTS_CAN_MODIFY); 3880 sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, Events.GUESTS_CAN_SEE_GUESTS); 3881 sEventEntitiesProjectionMap.put(Events.ORGANIZER, Events.ORGANIZER); 3882 sEventEntitiesProjectionMap.put(Events.DELETED, Events.DELETED); 3883 sEventEntitiesProjectionMap.put(Events._ID, Events._ID); 3884 sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID); 3885 sEventEntitiesProjectionMap.put(Events.SYNC_DATA1, Events.SYNC_DATA1); 3886 sEventEntitiesProjectionMap.put(Events.SYNC_DATA2, Events.SYNC_DATA2); 3887 sEventEntitiesProjectionMap.put(Events.SYNC_DATA3, Events.SYNC_DATA3); 3888 sEventEntitiesProjectionMap.put(Events.SYNC_DATA4, Events.SYNC_DATA4); 3889 sEventEntitiesProjectionMap.put(Events.SYNC_DATA5, Events.SYNC_DATA5); 3890 sEventEntitiesProjectionMap.put(Events.SYNC_DATA6, Events.SYNC_DATA6); 3891 sEventEntitiesProjectionMap.put(Events.SYNC_DATA7, Events.SYNC_DATA7); 3892 sEventEntitiesProjectionMap.put(Events.SYNC_DATA8, Events.SYNC_DATA8); 3893 sEventEntitiesProjectionMap.put(Events.SYNC_DATA9, Events.SYNC_DATA9); 3894 sEventEntitiesProjectionMap.put(Events.SYNC_DATA10, Events.SYNC_DATA10); 3895 sEventEntitiesProjectionMap.put(Events.DIRTY, Events.DIRTY); 3896 sEventEntitiesProjectionMap.put(Events.LAST_SYNCED, Events.LAST_SYNCED); 3897 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC1, Calendars.CAL_SYNC1); 3898 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC2, Calendars.CAL_SYNC2); 3899 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC3, Calendars.CAL_SYNC3); 3900 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC4, Calendars.CAL_SYNC4); 3901 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC5, Calendars.CAL_SYNC5); 3902 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC6, Calendars.CAL_SYNC6); 3903 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC7, Calendars.CAL_SYNC7); 3904 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC8, Calendars.CAL_SYNC8); 3905 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC9, Calendars.CAL_SYNC9); 3906 sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC10, Calendars.CAL_SYNC10); 3907 3908 // Instances columns 3909 sInstancesProjectionMap.put(Events.DELETED, "Events.deleted as deleted"); 3910 sInstancesProjectionMap.put(Instances.BEGIN, "begin"); 3911 sInstancesProjectionMap.put(Instances.END, "end"); 3912 sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id"); 3913 sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id"); 3914 sInstancesProjectionMap.put(Instances.START_DAY, "startDay"); 3915 sInstancesProjectionMap.put(Instances.END_DAY, "endDay"); 3916 sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute"); 3917 sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute"); 3918 3919 // Attendees columns 3920 sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id"); 3921 sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id"); 3922 sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName"); 3923 sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail"); 3924 sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus"); 3925 sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship"); 3926 sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType"); 3927 sAttendeesProjectionMap.put(Events.DELETED, "Events.deleted AS deleted"); 3928 sAttendeesProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id"); 3929 3930 // Reminders columns 3931 sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id"); 3932 sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id"); 3933 sRemindersProjectionMap.put(Reminders.MINUTES, "minutes"); 3934 sRemindersProjectionMap.put(Reminders.METHOD, "method"); 3935 3936 // CalendarAlerts columns 3937 sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id"); 3938 sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id"); 3939 sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin"); 3940 sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end"); 3941 sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime"); 3942 sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state"); 3943 sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes"); 3944 3945 // CalendarCache columns 3946 sCalendarCacheProjectionMap = new HashMap<String, String>(); 3947 sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key"); 3948 sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value"); 3949 } 3950 3951 /** 3952 * Make sure that there are no entries for accounts that no longer 3953 * exist. We are overriding this since we need to delete from the 3954 * Calendars table, which is not syncable, which has triggers that 3955 * will delete from the Events and tables, which are 3956 * syncable. TODO: update comment, make sure deletes don't get synced. 3957 */ 3958 @Override 3959 public void onAccountsUpdated(Account[] accounts) { 3960 if (mDb == null) { 3961 mDb = mDbHelper.getWritableDatabase(); 3962 } 3963 if (mDb == null) { 3964 return; 3965 } 3966 3967 HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>(); 3968 HashSet<Account> validAccounts = new HashSet<Account>(); 3969 for (Account account : accounts) { 3970 validAccounts.add(new Account(account.name, account.type)); 3971 accountHasCalendar.put(account, false); 3972 } 3973 ArrayList<Account> accountsToDelete = new ArrayList<Account>(); 3974 3975 mDb.beginTransaction(); 3976 try { 3977 3978 for (String table : new String[]{Tables.CALENDARS}) { 3979 // Find all the accounts the calendar DB knows about, mark the ones that aren't 3980 // in the valid set for deletion. 3981 Cursor c = mDb.rawQuery("SELECT DISTINCT " + 3982 Calendars.ACCOUNT_NAME + 3983 "," + 3984 Calendars.ACCOUNT_TYPE + 3985 " FROM " + table, null); 3986 while (c.moveToNext()) { 3987 // ACCOUNT_TYPE_LOCAL is to store calendars not associated 3988 // with a system account. Typically, a calendar must be 3989 // associated with an account on the device or it will be 3990 // deleted. 3991 if (c.getString(0) != null 3992 && c.getString(1) != null 3993 && !TextUtils.equals(c.getString(1), 3994 CalendarContract.ACCOUNT_TYPE_LOCAL)) { 3995 Account currAccount = new Account(c.getString(0), c.getString(1)); 3996 if (!validAccounts.contains(currAccount)) { 3997 accountsToDelete.add(currAccount); 3998 } 3999 } 4000 } 4001 c.close(); 4002 } 4003 4004 for (Account account : accountsToDelete) { 4005 if (Log.isLoggable(TAG, Log.DEBUG)) { 4006 Log.d(TAG, "removing data for removed account " + account); 4007 } 4008 String[] params = new String[]{account.name, account.type}; 4009 mDb.execSQL(SQL_DELETE_FROM_CALENDARS, params); 4010 } 4011 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 4012 mDb.setTransactionSuccessful(); 4013 } finally { 4014 mDb.endTransaction(); 4015 } 4016 4017 // make sure the widget reflects the account changes 4018 sendUpdateNotification(false); 4019 } 4020 4021 /** 4022 * Inserts an argument at the beginning of the selection arg list. 4023 * 4024 * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is 4025 * prepended to the user's where clause (combined with 'AND') to generate 4026 * the final where close, so arguments associated with the QueryBuilder are 4027 * prepended before any user selection args to keep them in the right order. 4028 */ 4029 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 4030 if (selectionArgs == null) { 4031 return new String[] {arg}; 4032 } else { 4033 int newLength = selectionArgs.length + 1; 4034 String[] newSelectionArgs = new String[newLength]; 4035 newSelectionArgs[0] = arg; 4036 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 4037 return newSelectionArgs; 4038 } 4039 } 4040} 4041