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