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