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