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