1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.providers.calendar; 18 19 import com.google.common.annotations.VisibleForTesting; 20 21 import com.android.internal.content.SyncStateContentProviderHelper; 22 23 import android.accounts.Account; 24 import android.content.ContentResolver; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.database.DatabaseUtils; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteException; 32 import android.database.sqlite.SQLiteOpenHelper; 33 import android.os.Bundle; 34 import android.provider.Calendar; 35 import android.provider.ContactsContract; 36 import android.provider.SyncStateContract; 37 import android.text.TextUtils; 38 import android.text.format.Time; 39 import android.util.Log; 40 41 import java.io.UnsupportedEncodingException; 42 import java.net.URLDecoder; 43 import java.util.TimeZone; 44 45 /** 46 * Database helper for calendar. Designed as a singleton to make sure that all 47 * {@link android.content.ContentProvider} users get the same reference. 48 */ 49 /* package */ class CalendarDatabaseHelper extends SQLiteOpenHelper { 50 private static final String TAG = "CalendarDatabaseHelper"; 51 52 private static final String DATABASE_NAME = "calendar.db"; 53 54 private static final int DAY_IN_SECONDS = 24 * 60 * 60; 55 56 // TODO: change the Calendar contract so these are defined there. 57 static final String ACCOUNT_NAME = "_sync_account"; 58 static final String ACCOUNT_TYPE = "_sync_account_type"; 59 60 // Note: if you update the version number, you must also update the code 61 // in upgradeDatabase() to modify the database (gracefully, if possible). 62 static final int DATABASE_VERSION = 102; 63 64 private static final int PRE_FROYO_SYNC_STATE_VERSION = 3; 65 66 // Copied from SyncStateContentProviderHelper. Don't really want to make them public there. 67 private static final String SYNC_STATE_TABLE = "_sync_state"; 68 private static final String SYNC_STATE_META_TABLE = "_sync_state_metadata"; 69 private static final String SYNC_STATE_META_VERSION_COLUMN = "version"; 70 71 /** Selects rows from Attendees for which the event_id refers to a nonexistent Event */ 72 private static final String WHERE_ATTENDEES_ORPHANS = 73 "event_id IN (SELECT event_id FROM Attendees " + 74 "LEFT OUTER JOIN Events ON event_id=Events._id " + 75 "WHERE Events._id IS NULL)"; 76 /** Selects rows from Reminders for which the event_id refers to a nonexistent Event */ 77 private static final String WHERE_REMINDERS_ORPHANS = 78 "event_id IN (SELECT event_id FROM Reminders " + 79 "LEFT OUTER JOIN Events ON event_id=Events._id " + 80 "WHERE Events._id IS NULL)"; 81 82 private final Context mContext; 83 private final SyncStateContentProviderHelper mSyncState; 84 85 private static CalendarDatabaseHelper sSingleton = null; 86 87 private DatabaseUtils.InsertHelper mCalendarsInserter; 88 private DatabaseUtils.InsertHelper mEventsInserter; 89 private DatabaseUtils.InsertHelper mEventsRawTimesInserter; 90 private DatabaseUtils.InsertHelper mInstancesInserter; 91 private DatabaseUtils.InsertHelper mAttendeesInserter; 92 private DatabaseUtils.InsertHelper mRemindersInserter; 93 private DatabaseUtils.InsertHelper mCalendarAlertsInserter; 94 private DatabaseUtils.InsertHelper mExtendedPropertiesInserter; 95 96 public long calendarsInsert(ContentValues values) { 97 return mCalendarsInserter.insert(values); 98 } 99 100 public long eventsInsert(ContentValues values) { 101 return mEventsInserter.insert(values); 102 } 103 104 public long eventsRawTimesInsert(ContentValues values) { 105 return mEventsRawTimesInserter.insert(values); 106 } 107 108 public long eventsRawTimesReplace(ContentValues values) { 109 return mEventsRawTimesInserter.replace(values); 110 } 111 112 public long instancesInsert(ContentValues values) { 113 return mInstancesInserter.insert(values); 114 } 115 116 public long instancesReplace(ContentValues values) { 117 return mInstancesInserter.replace(values); 118 } 119 120 public long attendeesInsert(ContentValues values) { 121 return mAttendeesInserter.insert(values); 122 } 123 124 public long remindersInsert(ContentValues values) { 125 return mRemindersInserter.insert(values); 126 } 127 128 public long calendarAlertsInsert(ContentValues values) { 129 return mCalendarAlertsInserter.insert(values); 130 } 131 132 public long extendedPropertiesInsert(ContentValues values) { 133 return mExtendedPropertiesInserter.insert(values); 134 } 135 136 public static synchronized CalendarDatabaseHelper getInstance(Context context) { 137 if (sSingleton == null) { 138 sSingleton = new CalendarDatabaseHelper(context); 139 } 140 return sSingleton; 141 } 142 143 /** 144 * Private constructor, callers except unit tests should obtain an instance through 145 * {@link #getInstance(android.content.Context)} instead. 146 */ 147 /* package */ CalendarDatabaseHelper(Context context) { 148 super(context, DATABASE_NAME, null, DATABASE_VERSION); 149 if (false) Log.i(TAG, "Creating OpenHelper"); 150 Resources resources = context.getResources(); 151 152 mContext = context; 153 mSyncState = new SyncStateContentProviderHelper(); 154 } 155 156 @Override 157 public void onOpen(SQLiteDatabase db) { 158 mSyncState.onDatabaseOpened(db); 159 160 mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars"); 161 mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events"); 162 mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes"); 163 mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances"); 164 mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees"); 165 mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders"); 166 mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts"); 167 mExtendedPropertiesInserter = 168 new DatabaseUtils.InsertHelper(db, "ExtendedProperties"); 169 } 170 171 /* 172 * Upgrade sync state table if necessary. Note that the data bundle 173 * in the table is not upgraded. 174 * 175 * The sync state used to be stored with version 3, but now uses the 176 * same sync state code as contacts, which is version 1. This code 177 * upgrades from 3 to 1 if necessary. (Yes, the numbers are unfortunately 178 * backwards.) 179 * 180 * This code is only called when upgrading from an old calendar version, 181 * so there is no problem if sync state version 3 gets used again in the 182 * future. 183 */ 184 private void upgradeSyncState(SQLiteDatabase db) { 185 long version = DatabaseUtils.longForQuery(db, 186 "SELECT " + SYNC_STATE_META_VERSION_COLUMN 187 + " FROM " + SYNC_STATE_META_TABLE, 188 null); 189 if (version == PRE_FROYO_SYNC_STATE_VERSION) { 190 Log.i(TAG, "Upgrading calendar sync state table"); 191 db.execSQL("CREATE TEMPORARY TABLE state_backup(_sync_account TEXT, " 192 + "_sync_account_type TEXT, data TEXT);"); 193 db.execSQL("INSERT INTO state_backup SELECT _sync_account, _sync_account_type, data" 194 + " FROM " 195 + SYNC_STATE_TABLE 196 + " WHERE _sync_account is not NULL and _sync_account_type is not NULL;"); 197 db.execSQL("DROP TABLE " + SYNC_STATE_TABLE + ";"); 198 mSyncState.onDatabaseOpened(db); 199 db.execSQL("INSERT INTO " + SYNC_STATE_TABLE + "(" 200 + SyncStateContract.Columns.ACCOUNT_NAME + "," 201 + SyncStateContract.Columns.ACCOUNT_TYPE + "," 202 + SyncStateContract.Columns.DATA 203 + ") SELECT _sync_account, _sync_account_type, data from state_backup;"); 204 db.execSQL("DROP TABLE state_backup;"); 205 } else { 206 // Wrong version to upgrade. 207 // Don't need to do anything more here because mSyncState.onDatabaseOpened() will blow 208 // away and recreate the database (which will result in a resync). 209 Log.w(TAG, "upgradeSyncState: current version is " + version + ", skipping upgrade."); 210 } 211 } 212 213 @Override 214 public void onCreate(SQLiteDatabase db) { 215 bootstrapDB(db); 216 } 217 218 private void bootstrapDB(SQLiteDatabase db) { 219 Log.i(TAG, "Bootstrapping database"); 220 221 mSyncState.createDatabase(db); 222 223 db.execSQL("CREATE TABLE Calendars (" + 224 "_id INTEGER PRIMARY KEY," + 225 ACCOUNT_NAME + " TEXT," + 226 ACCOUNT_TYPE + " TEXT," + 227 "_sync_id TEXT," + 228 "_sync_version TEXT," + 229 "_sync_time TEXT," + // UTC 230 "_sync_local_id INTEGER," + 231 "_sync_dirty INTEGER," + 232 "_sync_mark INTEGER," + // Used to filter out new rows 233 "url TEXT," + 234 "name TEXT," + 235 "displayName TEXT," + 236 "hidden INTEGER NOT NULL DEFAULT 0," + 237 "color INTEGER," + 238 "access_level INTEGER," + 239 "selected INTEGER NOT NULL DEFAULT 1," + 240 "sync_events INTEGER NOT NULL DEFAULT 0," + 241 "location TEXT," + 242 "timezone TEXT," + 243 "ownerAccount TEXT, " + 244 "organizerCanRespond INTEGER NOT NULL DEFAULT 1" + 245 ");"); 246 247 // Trigger to remove a calendar's events when we delete the calendar 248 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 249 "BEGIN " + 250 "DELETE FROM Events WHERE calendar_id = old._id;" + 251 "END"); 252 253 // TODO: do we need both dtend and duration? 254 db.execSQL("CREATE TABLE Events (" + 255 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 256 ACCOUNT_NAME + " TEXT," + 257 ACCOUNT_TYPE + " TEXT," + 258 "_sync_id TEXT," + 259 "_sync_version TEXT," + 260 "_sync_time TEXT," + // UTC 261 "_sync_local_id INTEGER," + 262 "_sync_dirty INTEGER," + 263 "_sync_mark INTEGER," + // To filter out new rows 264 "calendar_id INTEGER NOT NULL," + 265 "htmlUri TEXT," + 266 "title TEXT," + 267 "eventLocation TEXT," + 268 "description TEXT," + 269 "eventStatus INTEGER," + 270 "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," + 271 "commentsUri TEXT," + 272 "dtstart INTEGER," + // millis since epoch 273 "dtend INTEGER," + // millis since epoch 274 "eventTimezone TEXT," + // timezone for event 275 "duration TEXT," + 276 "allDay INTEGER NOT NULL DEFAULT 0," + 277 "visibility INTEGER NOT NULL DEFAULT 0," + 278 "transparency INTEGER NOT NULL DEFAULT 0," + 279 "hasAlarm INTEGER NOT NULL DEFAULT 0," + 280 "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," + 281 "rrule TEXT," + 282 "rdate TEXT," + 283 "exrule TEXT," + 284 "exdate TEXT," + 285 "originalEvent TEXT," + // _sync_id of recurring event 286 "originalInstanceTime INTEGER," + // millis since epoch 287 "originalAllDay INTEGER," + 288 "lastDate INTEGER," + // millis since epoch 289 "hasAttendeeData INTEGER NOT NULL DEFAULT 0," + 290 "guestsCanModify INTEGER NOT NULL DEFAULT 0," + 291 "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," + 292 "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," + 293 "organizer STRING," + 294 "deleted INTEGER NOT NULL DEFAULT 0," + 295 "dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone 296 "dtend2 INTEGER," + //millis since epoch, allDay events in local timezone 297 "eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone 298 "syncAdapterData TEXT" + //available for use by sync adapters 299 ");"); 300 301 // Trigger to set event's sync_account 302 db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " + 303 "BEGIN " + 304 "UPDATE Events SET _sync_account=" + 305 "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," + 306 "_sync_account_type=" + 307 "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " + 308 "WHERE Events._id=new._id;" + 309 "END"); 310 311 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 312 + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " + Calendar.Events._SYNC_ACCOUNT + ", " 313 + Calendar.Events._SYNC_ID + ");"); 314 315 db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" + 316 Calendar.Events.CALENDAR_ID + 317 ");"); 318 319 db.execSQL("CREATE TABLE EventsRawTimes (" + 320 "_id INTEGER PRIMARY KEY," + 321 "event_id INTEGER NOT NULL," + 322 "dtstart2445 TEXT," + 323 "dtend2445 TEXT," + 324 "originalInstanceTime2445 TEXT," + 325 "lastDate2445 TEXT," + 326 "UNIQUE (event_id)" + 327 ");"); 328 329 db.execSQL("CREATE TABLE Instances (" + 330 "_id INTEGER PRIMARY KEY," + 331 "event_id INTEGER," + 332 "begin INTEGER," + // UTC millis 333 "end INTEGER," + // UTC millis 334 "startDay INTEGER," + // Julian start day 335 "endDay INTEGER," + // Julian end day 336 "startMinute INTEGER," + // minutes from midnight 337 "endMinute INTEGER," + // minutes from midnight 338 "UNIQUE (event_id, begin, end)" + 339 ");"); 340 341 db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" + 342 Calendar.Instances.START_DAY + 343 ");"); 344 345 createCalendarMetaDataTable(db); 346 347 createCalendarCacheTable(db, null); 348 349 db.execSQL("CREATE TABLE Attendees (" + 350 "_id INTEGER PRIMARY KEY," + 351 "event_id INTEGER," + 352 "attendeeName TEXT," + 353 "attendeeEmail TEXT," + 354 "attendeeStatus INTEGER," + 355 "attendeeRelationship INTEGER," + 356 "attendeeType INTEGER" + 357 ");"); 358 359 db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" + 360 Calendar.Attendees.EVENT_ID + 361 ");"); 362 363 db.execSQL("CREATE TABLE Reminders (" + 364 "_id INTEGER PRIMARY KEY," + 365 "event_id INTEGER," + 366 "minutes INTEGER," + 367 "method INTEGER NOT NULL" + 368 " DEFAULT " + Calendar.Reminders.METHOD_DEFAULT + 369 ");"); 370 371 db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" + 372 Calendar.Reminders.EVENT_ID + 373 ");"); 374 375 // This table stores the Calendar notifications that have gone off. 376 db.execSQL("CREATE TABLE CalendarAlerts (" + 377 "_id INTEGER PRIMARY KEY," + 378 "event_id INTEGER," + 379 "begin INTEGER NOT NULL," + // UTC millis 380 "end INTEGER NOT NULL," + // UTC millis 381 "alarmTime INTEGER NOT NULL," + // UTC millis 382 "creationTime INTEGER NOT NULL," + // UTC millis 383 "receivedTime INTEGER NOT NULL," + // UTC millis 384 "notifyTime INTEGER NOT NULL," + // UTC millis 385 "state INTEGER NOT NULL," + 386 "minutes INTEGER," + 387 "UNIQUE (alarmTime, begin, event_id)" + 388 ");"); 389 390 db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" + 391 Calendar.CalendarAlerts.EVENT_ID + 392 ");"); 393 394 db.execSQL("CREATE TABLE ExtendedProperties (" + 395 "_id INTEGER PRIMARY KEY," + 396 "event_id INTEGER," + 397 "name TEXT," + 398 "value TEXT" + 399 ");"); 400 401 db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" + 402 Calendar.ExtendedProperties.EVENT_ID + 403 ");"); 404 405 // Trigger to remove data tied to an event when we delete that event. 406 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " + 407 "BEGIN " + 408 "DELETE FROM Instances WHERE event_id = old._id;" + 409 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" + 410 "DELETE FROM Attendees WHERE event_id = old._id;" + 411 "DELETE FROM Reminders WHERE event_id = old._id;" + 412 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" + 413 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" + 414 "END"); 415 416 createEventsView(db); 417 418 ContentResolver.requestSync(null /* all accounts */, 419 ContactsContract.AUTHORITY, new Bundle()); 420 } 421 422 private void createCalendarMetaDataTable(SQLiteDatabase db) { 423 db.execSQL("CREATE TABLE CalendarMetaData (" + 424 "_id INTEGER PRIMARY KEY," + 425 "localTimezone TEXT," + 426 "minInstance INTEGER," + // UTC millis 427 "maxInstance INTEGER" + // UTC millis 428 ");"); 429 } 430 431 private void createCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) { 432 // This is a hack because versioning skipped version number 61 of schema 433 // TODO after version 70 this can be removed 434 db.execSQL("DROP TABLE IF EXISTS CalendarCache;"); 435 436 // IF NOT EXISTS should be normal pattern for table creation 437 db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" + 438 "_id INTEGER PRIMARY KEY," + 439 "key TEXT NOT NULL," + 440 "value TEXT" + 441 ");"); 442 443 initCalendarCacheTable(db, oldTimezoneDbVersion); 444 updateCalendarCacheTableTo101(db); 445 } 446 447 private void initCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) { 448 String timezoneDbVersion = (oldTimezoneDbVersion != null) ? 449 oldTimezoneDbVersion : CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION; 450 451 // Set the default timezone database version 452 db.execSQL("INSERT OR REPLACE INTO CalendarCache (_id, key, value) VALUES (" + 453 CalendarCache.KEY_TIMEZONE_DATABASE_VERSION.hashCode() + "," + 454 "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "'," + 455 "'" + timezoneDbVersion + "'" + 456 ");"); 457 } 458 459 private void updateCalendarCacheTableTo101(SQLiteDatabase db) { 460 // Define the default timezone type for Instances timezone management 461 db.execSQL("INSERT INTO CalendarCache (_id, key, value) VALUES (" + 462 CalendarCache.KEY_TIMEZONE_TYPE.hashCode() + "," + 463 "'" + CalendarCache.KEY_TIMEZONE_TYPE + "'," + 464 "'" + CalendarCache.TIMEZONE_TYPE_AUTO + "'" + 465 ");"); 466 467 String defaultTimezone = TimeZone.getDefault().getID(); 468 469 // Define the default timezone for Instances 470 db.execSQL("INSERT INTO CalendarCache (_id, key, value) VALUES (" + 471 CalendarCache.KEY_TIMEZONE_INSTANCES.hashCode() + "," + 472 "'" + CalendarCache.KEY_TIMEZONE_INSTANCES + "'," + 473 "'" + defaultTimezone + "'" + 474 ");"); 475 476 // Define the default previous timezone for Instances 477 db.execSQL("INSERT INTO CalendarCache (_id, key, value) VALUES (" + 478 CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS.hashCode() + "," + 479 "'" + CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + "'," + 480 "'" + defaultTimezone + "'" + 481 ");"); 482 } 483 484 /** 485 * Removes orphaned data from the database. Specifically: 486 * <ul> 487 * <li>Attendees with an event_id for a nonexistent Event 488 * <li>Reminders with an event_id for a nonexistent Event 489 * </ul> 490 */ 491 static void removeOrphans(SQLiteDatabase db) { 492 Log.d(TAG, "Checking for orphaned entries"); 493 int count; 494 495 count = db.delete("Attendees", WHERE_ATTENDEES_ORPHANS, null); 496 if (count != 0) { 497 Log.i(TAG, "Deleted " + count + " orphaned Attendees"); 498 } 499 500 count = db.delete("Reminders", WHERE_REMINDERS_ORPHANS, null); 501 if (count != 0) { 502 Log.i(TAG, "Deleted " + count + " orphaned Reminders"); 503 } 504 } 505 506 @Override 507 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 508 Log.i(TAG, "Upgrading DB from version " + oldVersion 509 + " to " + newVersion); 510 if (oldVersion < 49) { 511 dropTables(db); 512 mSyncState.createDatabase(db); 513 return; // this was lossy 514 } 515 516 // From schema versions 59 to version 66, the CalendarMetaData table definition had lost 517 // the primary key leading to having the CalendarMetaData with multiple rows instead of 518 // only one. The Instance table was then corrupted (during Instance expansion we are using 519 // the localTimezone, minInstance and maxInstance from CalendarMetaData table. 520 // This boolean helps us tracking the need to recreate the CalendarMetaData table and 521 // clear the Instance table (and thus force an Instance expansion). 522 boolean recreateMetaDataAndInstances = (oldVersion >= 59 && oldVersion <= 66); 523 524 try { 525 if (oldVersion < 51) { 526 upgradeToVersion51(db); // From 50 or 51 527 oldVersion = 51; 528 } 529 if (oldVersion == 51) { 530 upgradeToVersion52(db); 531 oldVersion += 1; 532 } 533 if (oldVersion == 52) { 534 upgradeToVersion53(db); 535 oldVersion += 1; 536 } 537 if (oldVersion == 53) { 538 upgradeToVersion54(db); 539 oldVersion += 1; 540 } 541 if (oldVersion == 54) { 542 upgradeToVersion55(db); 543 oldVersion += 1; 544 } 545 if (oldVersion == 55 || oldVersion == 56) { 546 // Both require resync, so just schedule it once 547 upgradeResync(db); 548 } 549 if (oldVersion == 55) { 550 upgradeToVersion56(db); 551 oldVersion += 1; 552 } 553 if (oldVersion == 56) { 554 upgradeToVersion57(db); 555 oldVersion += 1; 556 } 557 if (oldVersion == 57) { 558 // Changes are undone upgrading to 60, so don't do anything. 559 oldVersion += 1; 560 } 561 if (oldVersion == 58) { 562 upgradeToVersion59(db); 563 oldVersion += 1; 564 } 565 if (oldVersion == 59) { 566 upgradeToVersion60(db); 567 oldVersion += 1; 568 } 569 if (oldVersion == 60) { 570 upgradeToVersion61(db); 571 oldVersion += 1; 572 } 573 if (oldVersion == 61) { 574 upgradeToVersion62(db); 575 oldVersion += 1; 576 } 577 if (oldVersion == 62) { 578 upgradeToVersion63(db); 579 oldVersion += 1; 580 } 581 if (oldVersion == 63) { 582 upgradeToVersion64(db); 583 oldVersion += 1; 584 } 585 if (oldVersion == 64) { 586 upgradeToVersion65(db); 587 oldVersion += 1; 588 } 589 if (oldVersion == 65) { 590 upgradeToVersion66(db); 591 oldVersion += 1; 592 } 593 if (oldVersion == 66) { 594 // Changes are done thru recreateMetaDataAndInstances() method 595 oldVersion += 1; 596 } 597 if (recreateMetaDataAndInstances) { 598 recreateMetaDataAndInstances(db); 599 } 600 if(oldVersion == 67 || oldVersion == 68) { 601 upgradeToVersion69(db); 602 oldVersion = 69; 603 } 604 if(oldVersion == 69) { 605 upgradeToVersion100(db); 606 oldVersion = 100; 607 } 608 if(oldVersion == 70) { 609 // Froyo version "70" already has the CalendarCache fix 610 oldVersion = 100; 611 } 612 if(oldVersion == 100) { 613 upgradeToVersion101(db); 614 oldVersion = 101; 615 } 616 if(oldVersion == 101) { 617 upgradeToVersion102(db); 618 oldVersion = 102; 619 } 620 removeOrphans(db); 621 } catch (SQLiteException e) { 622 Log.e(TAG, "onUpgrade: SQLiteException, recreating db. " + e); 623 dropTables(db); 624 bootstrapDB(db); 625 return; // this was lossy 626 } 627 } 628 629 /** 630 * If the user_version of the database if between 59 and 66 (those versions has been deployed 631 * with no primary key for the CalendarMetaData table) 632 */ 633 private void recreateMetaDataAndInstances(SQLiteDatabase db) { 634 // Recreate the CalendarMetaData table with correct primary key 635 db.execSQL("DROP TABLE CalendarMetaData;"); 636 createCalendarMetaDataTable(db); 637 638 // Also clean the Instance table as this table may be corrupted 639 db.execSQL("DELETE FROM Instances;"); 640 } 641 642 private static boolean fixAllDayTime(Time time, String timezone, Long timeInMillis) { 643 time.set(timeInMillis); 644 if(time.hour != 0 || time.minute != 0 || time.second != 0) { 645 time.hour = 0; 646 time.minute = 0; 647 time.second = 0; 648 return true; 649 } 650 return false; 651 } 652 653 @VisibleForTesting 654 void upgradeToVersion102(SQLiteDatabase db) { 655 /* 656 * Changes from version 101 to 102: 657 * - Changed _id field to AUTOINCREMENT 658 */ 659 db.execSQL("ALTER TABLE Events RENAME TO Events_Backup;"); 660 db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete"); 661 db.execSQL("DROP TRIGGER IF EXISTS events_insert"); 662 db.execSQL("DROP INDEX IF EXISTS eventSyncAccountAndIdIndex"); 663 db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex"); 664 665 // copy & pasted from bootstrapDB 666 db.execSQL("CREATE TABLE Events (" + 667 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 668 ACCOUNT_NAME + " TEXT," + 669 ACCOUNT_TYPE + " TEXT," + 670 "_sync_id TEXT," + 671 "_sync_version TEXT," + 672 "_sync_time TEXT," + // UTC 673 "_sync_local_id INTEGER," + 674 "_sync_dirty INTEGER," + 675 "_sync_mark INTEGER," + // To filter out new rows 676 "calendar_id INTEGER NOT NULL," + 677 "htmlUri TEXT," + 678 "title TEXT," + 679 "eventLocation TEXT," + 680 "description TEXT," + 681 "eventStatus INTEGER," + 682 "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," + 683 "commentsUri TEXT," + 684 "dtstart INTEGER," + // millis since epoch 685 "dtend INTEGER," + // millis since epoch 686 "eventTimezone TEXT," + // timezone for event 687 "duration TEXT," + 688 "allDay INTEGER NOT NULL DEFAULT 0," + 689 "visibility INTEGER NOT NULL DEFAULT 0," + 690 "transparency INTEGER NOT NULL DEFAULT 0," + 691 "hasAlarm INTEGER NOT NULL DEFAULT 0," + 692 "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," + 693 "rrule TEXT," + 694 "rdate TEXT," + 695 "exrule TEXT," + 696 "exdate TEXT," + 697 "originalEvent TEXT," + // _sync_id of recurring event 698 "originalInstanceTime INTEGER," + // millis since epoch 699 "originalAllDay INTEGER," + 700 "lastDate INTEGER," + // millis since epoch 701 "hasAttendeeData INTEGER NOT NULL DEFAULT 0," + 702 "guestsCanModify INTEGER NOT NULL DEFAULT 0," + 703 "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," + 704 "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," + 705 "organizer STRING," + 706 "deleted INTEGER NOT NULL DEFAULT 0," + 707 "dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone 708 "dtend2 INTEGER," + //millis since epoch, allDay events in local timezone 709 "eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone 710 "syncAdapterData TEXT" + //available for use by sync adapters 711 ");"); 712 713 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 714 + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " + Calendar.Events._SYNC_ACCOUNT + ", " 715 + Calendar.Events._SYNC_ID + ");"); 716 717 db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" + 718 Calendar.Events.CALENDAR_ID + 719 ");"); 720 721 String FIELD_LIST = 722 "_id, " + 723 "_sync_account, " + 724 "_sync_account_type, " + 725 "_sync_id, " + 726 "_sync_version, " + 727 "_sync_time, " + 728 "_sync_local_id, " + 729 "_sync_dirty, " + 730 "_sync_mark, " + 731 "calendar_id, " + 732 "htmlUri, " + 733 "title, " + 734 "eventLocation, " + 735 "description, " + 736 "eventStatus, " + 737 "selfAttendeeStatus, " + 738 "commentsUri, " + 739 "dtstart, " + 740 "dtend, " + 741 "eventTimezone, " + 742 "duration, " + 743 "allDay, " + 744 "visibility, " + 745 "transparency, " + 746 "hasAlarm, " + 747 "hasExtendedProperties, " + 748 "rrule, " + 749 "rdate, " + 750 "exrule, " + 751 "exdate, " + 752 "originalEvent, " + 753 "originalInstanceTime, " + 754 "originalAllDay, " + 755 "lastDate, " + 756 "hasAttendeeData, " + 757 "guestsCanModify, " + 758 "guestsCanInviteOthers, " + 759 "guestsCanSeeGuests, " + 760 "organizer, " + 761 "deleted, " + 762 "dtstart2, " + 763 "dtend2, " + 764 "eventTimezone2, " + 765 "syncAdapterData"; 766 767 // copy fields from old to new 768 db.execSQL("INSERT INTO Events (" + FIELD_LIST + ") SELECT " + FIELD_LIST + 769 " FROM Events_Backup;"); 770 771 db.execSQL("DROP TABLE Events_Backup;"); 772 773 // Trigger to set event's sync_account 774 db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " + 775 "BEGIN " + 776 "UPDATE Events SET _sync_account=" + 777 "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," + 778 "_sync_account_type=" + 779 "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " + 780 "WHERE Events._id=new._id;" + 781 "END"); 782 783 // Trigger to remove data tied to an event when we delete that event. 784 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " + 785 "BEGIN " + 786 "DELETE FROM Instances WHERE event_id = old._id;" + 787 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" + 788 "DELETE FROM Attendees WHERE event_id = old._id;" + 789 "DELETE FROM Reminders WHERE event_id = old._id;" + 790 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" + 791 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" + 792 "END"); 793 } 794 795 796 @VisibleForTesting 797 void upgradeToVersion101(SQLiteDatabase db) { 798 updateCalendarCacheTableTo101(db); 799 } 800 801 @VisibleForTesting 802 void upgradeToVersion100(SQLiteDatabase db) { 803 Cursor cursor = db.rawQuery("SELECT value from CalendarCache WHERE key=?", 804 new String[] {"timezoneDatabaseVersion"}); 805 806 String oldTimezoneDbVersion = null; 807 if (cursor != null && cursor.moveToNext()) { 808 try { 809 oldTimezoneDbVersion = cursor.getString(0); 810 } finally { 811 cursor.close(); 812 } 813 // Also clean the CalendarCache table 814 db.execSQL("DELETE FROM CalendarCache;"); 815 } 816 initCalendarCacheTable(db, oldTimezoneDbVersion); 817 } 818 819 @VisibleForTesting 820 static void upgradeToVersion69(SQLiteDatabase db) { 821 // Clean up allDay events which could be in an invalid state from an earlier version 822 // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This 823 // will go through the allDay events and make sure they have proper values and are in the 824 // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that 825 // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2 826 // and dtend2 are at midnight in their timezone. 827 Cursor cursor = db.rawQuery("SELECT _id, dtstart, dtend, duration, dtstart2, dtend2, " + 828 "eventTimezone, eventTimezone2, rrule FROM Events WHERE allDay=?", 829 new String[] {"1"}); 830 if (cursor != null) { 831 try { 832 String timezone; 833 String timezone2; 834 String duration; 835 Long dtstart; 836 Long dtstart2; 837 Long dtend; 838 Long dtend2; 839 Time time = new Time(); 840 Long id; 841 // some things need to be in utc so we call this frequently, cache to make faster 842 final String utc = Time.TIMEZONE_UTC; 843 while (cursor.moveToNext()) { 844 String rrule = cursor.getString(8); 845 id = cursor.getLong(0); 846 dtstart = cursor.getLong(1); 847 dtstart2 = null; 848 timezone = cursor.getString(6); 849 timezone2 = cursor.getString(7); 850 duration = cursor.getString(3); 851 852 if (TextUtils.isEmpty(rrule)) { 853 // For non-recurring events dtstart and dtend should both have values 854 // and duration should be null. 855 dtend = cursor.getLong(2); 856 dtend2 = null; 857 // Since we made all three of these at the same time if timezone2 exists 858 // so should dtstart2 and dtend2. 859 if(!TextUtils.isEmpty(timezone2)) { 860 dtstart2 = cursor.getLong(4); 861 dtend2 = cursor.getLong(5); 862 } 863 864 boolean update = false; 865 if (!TextUtils.equals(timezone, utc)) { 866 update = true; 867 timezone = utc; 868 } 869 870 time.clear(timezone); 871 update |= fixAllDayTime(time, timezone, dtstart); 872 dtstart = time.normalize(false); 873 874 time.clear(timezone); 875 update |= fixAllDayTime(time, timezone, dtend); 876 dtend = time.normalize(false); 877 878 if (dtstart2 != null) { 879 time.clear(timezone2); 880 update |= fixAllDayTime(time, timezone2, dtstart2); 881 dtstart2 = time.normalize(false); 882 } 883 884 if (dtend2 != null) { 885 time.clear(timezone2); 886 update |= fixAllDayTime(time, timezone2, dtend2); 887 dtend2 = time.normalize(false); 888 } 889 890 if (!TextUtils.isEmpty(duration)) { 891 update = true; 892 } 893 894 if (update) { 895 // enforce duration being null 896 db.execSQL("UPDATE Events " + 897 "SET dtstart=?, dtend=?, dtstart2=?, dtend2=?, duration=?, " + 898 "eventTimezone=?, eventTimezone2=? WHERE _id=?", 899 new Object[] {dtstart, dtend, dtstart2, dtend2, null, timezone, 900 timezone2, id}); 901 } 902 903 } else { 904 // For recurring events only dtstart and duration should be used. 905 // We ignore dtend since it will be overwritten if the event changes to a 906 // non-recurring event and won't be used otherwise. 907 if(!TextUtils.isEmpty(timezone2)) { 908 dtstart2 = cursor.getLong(4); 909 } 910 911 boolean update = false; 912 if (!TextUtils.equals(timezone, utc)) { 913 update = true; 914 timezone = utc; 915 } 916 917 time.clear(timezone); 918 update |= fixAllDayTime(time, timezone, dtstart); 919 dtstart = time.normalize(false); 920 921 if (dtstart2 != null) { 922 time.clear(timezone2); 923 update |= fixAllDayTime(time, timezone2, dtstart2); 924 dtstart2 = time.normalize(false); 925 } 926 927 if (TextUtils.isEmpty(duration)) { 928 // If duration was missing assume a 1 day duration 929 duration = "P1D"; 930 update = true; 931 } else { 932 int len = duration.length(); 933 // TODO fix durations in other formats as well 934 if (duration.charAt(0) == 'P' && 935 duration.charAt(len - 1) == 'S') { 936 int seconds = Integer.parseInt(duration.substring(1, len - 1)); 937 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS; 938 duration = "P" + days + "D"; 939 update = true; 940 } 941 } 942 943 if (update) { 944 // If there were other problems also enforce dtend being null 945 db.execSQL("UPDATE Events " + 946 "SET dtstart=?,dtend=?,dtstart2=?,dtend2=?,duration=?," + 947 "eventTimezone=?, eventTimezone2=? WHERE _id=?", 948 new Object[] {dtstart, null, dtstart2, null, duration, 949 timezone, timezone2, id}); 950 } 951 } 952 } 953 } finally { 954 cursor.close(); 955 } 956 } 957 } 958 959 private void upgradeToVersion66(SQLiteDatabase db) { 960 // Add a column to indicate whether the event organizer can respond to his own events 961 // The UI should not show attendee status for events in calendars with this column = 0 962 db.execSQL("ALTER TABLE " + 963 "Calendars ADD COLUMN organizerCanRespond INTEGER NOT NULL DEFAULT 1;"); 964 } 965 966 private void upgradeToVersion65(SQLiteDatabase db) { 967 // we need to recreate the Events view 968 createEventsView(db); 969 } 970 971 private void upgradeToVersion64(SQLiteDatabase db) { 972 // Add a column that may be used by sync adapters 973 db.execSQL("ALTER TABLE Events ADD COLUMN syncAdapterData TEXT;"); 974 } 975 976 private void upgradeToVersion63(SQLiteDatabase db) { 977 // we need to recreate the Events view 978 createEventsView(db); 979 } 980 981 private void upgradeToVersion62(SQLiteDatabase db) { 982 // New columns are to transition to having allDay events in the local timezone 983 db.execSQL("ALTER TABLE Events ADD COLUMN dtstart2 INTEGER;"); 984 db.execSQL("ALTER TABLE Events ADD COLUMN dtend2 INTEGER;"); 985 db.execSQL("ALTER TABLE Events ADD COLUMN eventTimezone2 TEXT;"); 986 987 String[] allDayBit = new String[] {"0"}; 988 // Copy over all the data that isn't an all day event. 989 db.execSQL("UPDATE Events " + 990 "SET dtstart2=dtstart,dtend2=dtend,eventTimezone2=eventTimezone " + 991 "WHERE allDay=?;", 992 allDayBit /* selection args */); 993 994 // "cursor" iterates over all the calendars 995 allDayBit[0] = "1"; 996 Cursor cursor = db.rawQuery("SELECT Events._id,dtstart,dtend,eventTimezone,timezone " + 997 "FROM Events INNER JOIN Calendars " + 998 "WHERE Events.calendar_id=Calendars._id AND allDay=?", 999 allDayBit /* selection args */); 1000 1001 Time oldTime = new Time(); 1002 Time newTime = new Time(); 1003 // Update the allday events in the new columns 1004 if (cursor != null) { 1005 try { 1006 String[] newData = new String[4]; 1007 cursor.moveToPosition(-1); 1008 while (cursor.moveToNext()) { 1009 long id = cursor.getLong(0); // Order from query above 1010 long dtstart = cursor.getLong(1); 1011 long dtend = cursor.getLong(2); 1012 String eTz = cursor.getString(3); // current event timezone 1013 String tz = cursor.getString(4); // Calendar timezone 1014 //If there's no timezone for some reason use UTC by default. 1015 if(eTz == null) { 1016 eTz = Time.TIMEZONE_UTC; 1017 } 1018 1019 // Convert start time for all day events into the timezone of their calendar 1020 oldTime.clear(eTz); 1021 oldTime.set(dtstart); 1022 newTime.clear(tz); 1023 newTime.set(oldTime.monthDay, oldTime.month, oldTime.year); 1024 newTime.normalize(false); 1025 dtstart = newTime.toMillis(false /*ignoreDst*/); 1026 1027 // Convert end time for all day events into the timezone of their calendar 1028 oldTime.clear(eTz); 1029 oldTime.set(dtend); 1030 newTime.clear(tz); 1031 newTime.set(oldTime.monthDay, oldTime.month, oldTime.year); 1032 newTime.normalize(false); 1033 dtend = newTime.toMillis(false /*ignoreDst*/); 1034 1035 newData[0] = String.valueOf(dtstart); 1036 newData[1] = String.valueOf(dtend); 1037 newData[2] = tz; 1038 newData[3] = String.valueOf(id); 1039 db.execSQL("UPDATE Events " + 1040 "SET dtstart2=?,dtend2=?,eventTimezone2=? " + 1041 "WHERE _id=?", 1042 newData); 1043 } 1044 } finally { 1045 cursor.close(); 1046 } 1047 } 1048 } 1049 1050 private void upgradeToVersion61(SQLiteDatabase db) { 1051 db.execSQL("DROP TABLE IF EXISTS CalendarCache;"); 1052 1053 // IF NOT EXISTS should be normal pattern for table creation 1054 db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" + 1055 "_id INTEGER PRIMARY KEY," + 1056 "key TEXT NOT NULL," + 1057 "value TEXT" + 1058 ");"); 1059 1060 db.execSQL("INSERT INTO CalendarCache (key, value) VALUES (" + 1061 "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "'," + 1062 "'" + CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION + "'" + 1063 ");"); 1064 } 1065 1066 private void upgradeToVersion60(SQLiteDatabase db) { 1067 // Switch to CalendarProvider2 1068 upgradeSyncState(db); 1069 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 1070 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 1071 "BEGIN " + 1072 "DELETE FROM Events WHERE calendar_id = old._id;" + 1073 "END"); 1074 db.execSQL("ALTER TABLE Events ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;"); 1075 db.execSQL("DROP TRIGGER IF EXISTS events_insert"); 1076 db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " + 1077 "BEGIN " + 1078 "UPDATE Events SET _sync_account=" + 1079 "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," + 1080 "_sync_account_type=" + 1081 "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " + 1082 "WHERE Events._id=new._id;" + 1083 "END"); 1084 db.execSQL("DROP TABLE IF EXISTS DeletedEvents;"); 1085 db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete"); 1086 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " + 1087 "BEGIN " + 1088 "DELETE FROM Instances WHERE event_id = old._id;" + 1089 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" + 1090 "DELETE FROM Attendees WHERE event_id = old._id;" + 1091 "DELETE FROM Reminders WHERE event_id = old._id;" + 1092 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" + 1093 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" + 1094 "END"); 1095 db.execSQL("DROP TRIGGER IF EXISTS attendees_update"); 1096 db.execSQL("DROP TRIGGER IF EXISTS attendees_insert"); 1097 db.execSQL("DROP TRIGGER IF EXISTS attendees_delete"); 1098 db.execSQL("DROP TRIGGER IF EXISTS reminders_update"); 1099 db.execSQL("DROP TRIGGER IF EXISTS reminders_insert"); 1100 db.execSQL("DROP TRIGGER IF EXISTS reminders_delete"); 1101 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update"); 1102 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert"); 1103 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete"); 1104 1105 createEventsView(db); 1106 } 1107 1108 private void upgradeToVersion59(SQLiteDatabase db) { 1109 db.execSQL("DROP TABLE IF EXISTS BusyBits;"); 1110 db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup" + 1111 "(_id,localTimezone,minInstance,maxInstance);"); 1112 db.execSQL("INSERT INTO CalendarMetaData_Backup " + 1113 "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData;"); 1114 db.execSQL("DROP TABLE CalendarMetaData;"); 1115 createCalendarMetaDataTable(db); 1116 db.execSQL("INSERT INTO CalendarMetaData " + 1117 "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData_Backup;"); 1118 db.execSQL("DROP TABLE CalendarMetaData_Backup;"); 1119 } 1120 1121 private void upgradeToVersion57(SQLiteDatabase db) { 1122 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify" 1123 + " INTEGER NOT NULL DEFAULT 0;"); 1124 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers" 1125 + " INTEGER NOT NULL DEFAULT 1;"); 1126 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests" 1127 + " INTEGER NOT NULL DEFAULT 1;"); 1128 db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;"); 1129 db.execSQL("UPDATE Events SET organizer=" 1130 + "(SELECT attendeeEmail FROM Attendees WHERE " 1131 + "Attendees.event_id = Events._id" 1132 + " AND Attendees.attendeeRelationship=2);"); 1133 } 1134 1135 private void upgradeToVersion56(SQLiteDatabase db) { 1136 db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;"); 1137 db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;"); 1138 // Clear _sync_dirty to avoid a client-to-server sync that could blow away 1139 // server attendees. 1140 // Clear _sync_version to pull down the server's event (with attendees) 1141 // Change the URLs from full-selfattendance to full 1142 db.execSQL("UPDATE Events" 1143 + " SET _sync_dirty=0," 1144 + " _sync_version=NULL," 1145 + " _sync_id=" 1146 + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full')," 1147 + " commentsUri =" 1148 + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');"); 1149 db.execSQL("UPDATE Calendars" 1150 + " SET url=" 1151 + "REPLACE(url, '/private/full-selfattendance', '/private/full');"); 1152 1153 // "cursor" iterates over all the calendars 1154 Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars", 1155 null /* selection args */); 1156 // Add the owner column. 1157 if (cursor != null) { 1158 try { 1159 while (cursor.moveToNext()) { 1160 Long id = cursor.getLong(0); 1161 String url = cursor.getString(1); 1162 String owner = calendarEmailAddressFromFeedUrl(url); 1163 db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?", 1164 new Object[] {owner, id}); 1165 } 1166 } finally { 1167 cursor.close(); 1168 } 1169 } 1170 } 1171 1172 private void upgradeResync(SQLiteDatabase db) { 1173 // Delete sync state, so all records will be re-synced. 1174 db.execSQL("DELETE FROM _sync_state;"); 1175 1176 // "cursor" iterates over all the calendars 1177 Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url " 1178 + "FROM Calendars", 1179 null /* selection args */); 1180 if (cursor != null) { 1181 try { 1182 while (cursor.moveToNext()) { 1183 String accountName = cursor.getString(0); 1184 String accountType = cursor.getString(1); 1185 final Account account = new Account(accountName, accountType); 1186 String calendarUrl = cursor.getString(2); 1187 scheduleSync(account, false /* two-way sync */, calendarUrl); 1188 } 1189 } finally { 1190 cursor.close(); 1191 } 1192 } 1193 } 1194 1195 private void upgradeToVersion55(SQLiteDatabase db) { 1196 db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;"); 1197 db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;"); 1198 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;"); 1199 db.execSQL("UPDATE Calendars" 1200 + " SET _sync_account_type='com.google'" 1201 + " WHERE _sync_account IS NOT NULL"); 1202 db.execSQL("UPDATE Events" 1203 + " SET _sync_account_type='com.google'" 1204 + " WHERE _sync_account IS NOT NULL"); 1205 db.execSQL("UPDATE DeletedEvents" 1206 + " SET _sync_account_type='com.google'" 1207 + " WHERE _sync_account IS NOT NULL"); 1208 Log.w(TAG, "re-creating eventSyncAccountAndIdIndex"); 1209 db.execSQL("DROP INDEX eventSyncAccountAndIdIndex"); 1210 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 1211 + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " 1212 + Calendar.Events._SYNC_ACCOUNT + ", " 1213 + Calendar.Events._SYNC_ID + ");"); 1214 } 1215 1216 private void upgradeToVersion54(SQLiteDatabase db) { 1217 Log.w(TAG, "adding eventSyncAccountAndIdIndex"); 1218 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 1219 + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");"); 1220 } 1221 1222 private void upgradeToVersion53(SQLiteDatabase db) { 1223 Log.w(TAG, "Upgrading CalendarAlerts table"); 1224 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;"); 1225 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;"); 1226 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;"); 1227 } 1228 1229 private void upgradeToVersion52(SQLiteDatabase db) { 1230 // We added "originalAllDay" to the Events table to keep track of 1231 // the allDay status of the original recurring event for entries 1232 // that are exceptions to that recurring event. We need this so 1233 // that we can format the date correctly for the "originalInstanceTime" 1234 // column when we make a change to the recurrence exception and 1235 // send it to the server. 1236 db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;"); 1237 1238 // Iterate through the Events table and for each recurrence 1239 // exception, fill in the correct value for "originalAllDay", 1240 // if possible. The only times where this might not be possible 1241 // are (1) the original recurring event no longer exists, or 1242 // (2) the original recurring event does not yet have a _sync_id 1243 // because it was created on the phone and hasn't been synced to the 1244 // server yet. In both cases the originalAllDay field will be set 1245 // to null. In the first case we don't care because the recurrence 1246 // exception will not be displayed and we won't be able to make 1247 // any changes to it (and even if we did, the server should ignore 1248 // them, right?). In the second case, the calendar client already 1249 // disallows making changes to an instance of a recurring event 1250 // until the recurring event has been synced to the server so the 1251 // second case should never occur. 1252 1253 // "cursor" iterates over all the recurrences exceptions. 1254 Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events" 1255 + " WHERE originalEvent IS NOT NULL", null /* selection args */); 1256 if (cursor != null) { 1257 try { 1258 while (cursor.moveToNext()) { 1259 long id = cursor.getLong(0); 1260 String originalEvent = cursor.getString(1); 1261 1262 // Find the original recurring event (if it exists) 1263 Cursor recur = db.rawQuery("SELECT allDay FROM Events" 1264 + " WHERE _sync_id=?", new String[] {originalEvent}); 1265 if (recur == null) { 1266 continue; 1267 } 1268 1269 try { 1270 // Fill in the "originalAllDay" field of the 1271 // recurrence exception with the "allDay" value 1272 // from the recurring event. 1273 if (recur.moveToNext()) { 1274 int allDay = recur.getInt(0); 1275 db.execSQL("UPDATE Events SET originalAllDay=" + allDay 1276 + " WHERE _id="+id); 1277 } 1278 } finally { 1279 recur.close(); 1280 } 1281 } 1282 } finally { 1283 cursor.close(); 1284 } 1285 } 1286 } 1287 1288 private void upgradeToVersion51(SQLiteDatabase db) { 1289 Log.w(TAG, "Upgrading DeletedEvents table"); 1290 1291 // We don't have enough information to fill in the correct 1292 // value of the calendar_id for old rows in the DeletedEvents 1293 // table, but rows in that table are transient so it is unlikely 1294 // that there are any rows. Plus, the calendar_id is used only 1295 // when deleting a calendar, which is a rare event. All new rows 1296 // will have the correct calendar_id. 1297 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;"); 1298 1299 // Trigger to remove a calendar's events when we delete the calendar 1300 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 1301 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 1302 "BEGIN " + 1303 "DELETE FROM Events WHERE calendar_id = old._id;" + 1304 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" + 1305 "END"); 1306 db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted"); 1307 } 1308 1309 private void dropTables(SQLiteDatabase db) { 1310 db.execSQL("DROP TABLE IF EXISTS Calendars;"); 1311 db.execSQL("DROP TABLE IF EXISTS Events;"); 1312 db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;"); 1313 db.execSQL("DROP TABLE IF EXISTS Instances;"); 1314 db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;"); 1315 db.execSQL("DROP TABLE IF EXISTS CalendarCache;"); 1316 db.execSQL("DROP TABLE IF EXISTS Attendees;"); 1317 db.execSQL("DROP TABLE IF EXISTS Reminders;"); 1318 db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;"); 1319 db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;"); 1320 } 1321 1322 @Override 1323 public synchronized SQLiteDatabase getWritableDatabase() { 1324 SQLiteDatabase db = super.getWritableDatabase(); 1325 return db; 1326 } 1327 1328 public SyncStateContentProviderHelper getSyncState() { 1329 return mSyncState; 1330 } 1331 1332 /** 1333 * Schedule a calendar sync for the account. 1334 * @param account the account for which to schedule a sync 1335 * @param uploadChangesOnly if set, specify that the sync should only send 1336 * up local changes. This is typically used for a local sync, a user override of 1337 * too many deletions, or a sync after a calendar is unselected. 1338 * @param url the url feed for the calendar to sync (may be null, in which case a poll of 1339 * all feeds is done.) 1340 */ 1341 void scheduleSync(Account account, boolean uploadChangesOnly, String url) { 1342 Bundle extras = new Bundle(); 1343 if (uploadChangesOnly) { 1344 extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly); 1345 } 1346 if (url != null) { 1347 extras.putString("feed", url); 1348 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 1349 } 1350 ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras); 1351 } 1352 1353 public interface Views { 1354 public static final String EVENTS = "view_events"; 1355 } 1356 1357 public interface Tables { 1358 public static final String EVENTS = "Events"; 1359 public static final String CALENDARS = "Calendars"; 1360 } 1361 1362 private static void createEventsView(SQLiteDatabase db) { 1363 db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";"); 1364 String eventsSelect = "SELECT " 1365 + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + "," 1366 + Calendar.Events.HTML_URI + "," 1367 + Calendar.Events.TITLE + "," 1368 + Calendar.Events.DESCRIPTION + "," 1369 + Calendar.Events.EVENT_LOCATION + "," 1370 + Calendar.Events.STATUS + "," 1371 + Calendar.Events.SELF_ATTENDEE_STATUS + "," 1372 + Calendar.Events.COMMENTS_URI + "," 1373 + Calendar.Events.DTSTART + "," 1374 + Calendar.Events.DTEND + "," 1375 + Calendar.Events.DURATION + "," 1376 + Calendar.Events.EVENT_TIMEZONE + "," 1377 + Calendar.Events.ALL_DAY + "," 1378 + Calendar.Events.VISIBILITY + "," 1379 + Calendar.Events.TIMEZONE + "," 1380 + Calendar.Events.SELECTED + "," 1381 + Calendar.Events.ACCESS_LEVEL + "," 1382 + Calendar.Events.TRANSPARENCY + "," 1383 + Calendar.Events.COLOR + "," 1384 + Calendar.Events.HAS_ALARM + "," 1385 + Calendar.Events.HAS_EXTENDED_PROPERTIES + "," 1386 + Calendar.Events.RRULE + "," 1387 + Calendar.Events.RDATE + "," 1388 + Calendar.Events.EXRULE + "," 1389 + Calendar.Events.EXDATE + "," 1390 + Calendar.Events.ORIGINAL_EVENT + "," 1391 + Calendar.Events.ORIGINAL_INSTANCE_TIME + "," 1392 + Calendar.Events.ORIGINAL_ALL_DAY + "," 1393 + Calendar.Events.LAST_DATE + "," 1394 + Calendar.Events.HAS_ATTENDEE_DATA + "," 1395 + Calendar.Events.CALENDAR_ID + "," 1396 + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + "," 1397 + Calendar.Events.GUESTS_CAN_MODIFY + "," 1398 + Calendar.Events.GUESTS_CAN_SEE_GUESTS + "," 1399 + Calendar.Events.ORGANIZER + "," 1400 + Calendar.Events.DELETED + "," 1401 + Tables.EVENTS + "." + Calendar.Events._SYNC_ID 1402 + " AS " + Calendar.Events._SYNC_ID + "," 1403 + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION 1404 + " AS " + Calendar.Events._SYNC_VERSION + "," 1405 + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY 1406 + " AS " + Calendar.Events._SYNC_DIRTY + "," 1407 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT 1408 + " AS " + Calendar.Events._SYNC_ACCOUNT + "," 1409 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE 1410 + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + "," 1411 + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME 1412 + " AS " + Calendar.Events._SYNC_TIME + "," 1413 + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA 1414 + " AS " + Calendar.Events._SYNC_DATA + "," 1415 + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK 1416 + " AS " + Calendar.Events._SYNC_MARK + "," 1417 + Calendar.Calendars.URL + "," 1418 + Calendar.Calendars.OWNER_ACCOUNT + "," 1419 + Calendar.Calendars.SYNC_EVENTS 1420 + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS 1421 + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID 1422 + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID 1423 + ")"; 1424 1425 db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect); 1426 } 1427 1428 /** 1429 * Extracts the calendar email from a calendar feed url. 1430 * @param feed the calendar feed url 1431 * @return the calendar email that is in the feed url or null if it can't 1432 * find the email address. 1433 * TODO: this is duplicated in CalendarSyncAdapter; move to a library 1434 */ 1435 public static String calendarEmailAddressFromFeedUrl(String feed) { 1436 // Example feed url: 1437 // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees 1438 String[] pathComponents = feed.split("/"); 1439 if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) { 1440 try { 1441 return URLDecoder.decode(pathComponents[5], "UTF-8"); 1442 } catch (UnsupportedEncodingException e) { 1443 Log.e(TAG, "unable to url decode the email address in calendar " + feed); 1444 return null; 1445 } 1446 } 1447 1448 Log.e(TAG, "unable to find the email address in calendar " + feed); 1449 return null; 1450 } 1451 } 1452