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