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