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