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