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