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