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