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