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