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