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