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