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