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