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