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