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