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