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