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