1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.providers.calendar; 18 19 import com.google.common.annotations.VisibleForTesting; 20 21 import com.android.internal.content.SyncStateContentProviderHelper; 22 23 import android.accounts.Account; 24 import android.content.ContentResolver; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.database.DatabaseUtils; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteException; 32 import android.database.sqlite.SQLiteOpenHelper; 33 import android.os.Bundle; 34 import android.provider.Calendar; 35 import android.provider.ContactsContract; 36 import android.provider.SyncStateContract; 37 import android.text.TextUtils; 38 import android.text.format.Time; 39 import android.util.Log; 40 41 import java.io.UnsupportedEncodingException; 42 import java.net.URLDecoder; 43 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 = 69; 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 || oldVersion == 68) { 532 upgradeToVersion69(db); 533 oldVersion = 69; 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 time.set(timeInMillis); 558 if(time.hour != 0 || time.minute != 0 || time.second != 0) { 559 time.hour = 0; 560 time.minute = 0; 561 time.second = 0; 562 return true; 563 } 564 return false; 565 } 566 567 @VisibleForTesting 568 static void upgradeToVersion69(SQLiteDatabase db) { 569 // Clean up allDay events which could be in an invalid state from an earlier version 570 // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This 571 // will go through the allDay events and make sure they have proper values and are in the 572 // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that 573 // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2 574 // and dtend2 are at midnight in their timezone. 575 Cursor cursor = db.rawQuery("SELECT _id, dtstart, dtend, duration, dtstart2, dtend2, " + 576 "eventTimezone, eventTimezone2, rrule FROM Events WHERE allDay=?", 577 new String[] {"1"}); 578 if (cursor != null) { 579 try { 580 String timezone; 581 String timezone2; 582 String duration; 583 Long dtstart; 584 Long dtstart2; 585 Long dtend; 586 Long dtend2; 587 Time time = new Time(); 588 Long id; 589 // some things need to be in utc so we call this frequently, cache to make faster 590 final String utc = Time.TIMEZONE_UTC; 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 if (!TextUtils.equals(timezone, utc)) { 614 update = true; 615 timezone = utc; 616 } 617 618 time.clear(timezone); 619 update |= fixAllDayTime(time, timezone, dtstart); 620 dtstart = time.normalize(false); 621 622 time.clear(timezone); 623 update |= fixAllDayTime(time, timezone, dtend); 624 dtend = time.normalize(false); 625 626 if (dtstart2 != null) { 627 time.clear(timezone2); 628 update |= fixAllDayTime(time, timezone2, dtstart2); 629 dtstart2 = time.normalize(false); 630 } 631 632 if (dtend2 != null) { 633 time.clear(timezone2); 634 update |= fixAllDayTime(time, timezone2, dtend2); 635 dtend2 = time.normalize(false); 636 } 637 638 if (!TextUtils.isEmpty(duration)) { 639 update = true; 640 } 641 642 if (update) { 643 // enforce duration being null 644 db.execSQL("UPDATE Events " + 645 "SET dtstart=?, dtend=?, dtstart2=?, dtend2=?, duration=?, " + 646 "eventTimezone=?, eventTimezone2=? WHERE _id=?", 647 new Object[] {dtstart, dtend, dtstart2, dtend2, null, timezone, 648 timezone2, id}); 649 } 650 651 } else { 652 // For recurring events only dtstart and duration should be used. 653 // We ignore dtend since it will be overwritten if the event changes to a 654 // non-recurring event and won't be used otherwise. 655 if(!TextUtils.isEmpty(timezone2)) { 656 dtstart2 = cursor.getLong(4); 657 } 658 659 boolean update = false; 660 if (!TextUtils.equals(timezone, utc)) { 661 update = true; 662 timezone = utc; 663 } 664 665 time.clear(timezone); 666 update |= fixAllDayTime(time, timezone, dtstart); 667 dtstart = time.normalize(false); 668 669 if (dtstart2 != null) { 670 time.clear(timezone2); 671 update |= fixAllDayTime(time, timezone2, dtstart2); 672 dtstart2 = time.normalize(false); 673 } 674 675 if (TextUtils.isEmpty(duration)) { 676 // If duration was missing assume a 1 day duration 677 duration = "P1D"; 678 update = true; 679 } else { 680 int len = duration.length(); 681 // TODO fix durations in other formats as well 682 if (duration.charAt(0) == 'P' && 683 duration.charAt(len - 1) == 'S') { 684 int seconds = Integer.parseInt(duration.substring(1, len - 1)); 685 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS; 686 duration = "P" + days + "D"; 687 update = true; 688 } 689 } 690 691 if (update) { 692 // If there were other problems also enforce dtend being null 693 db.execSQL("UPDATE Events " + 694 "SET dtstart=?,dtend=?,dtstart2=?,dtend2=?,duration=?," + 695 "eventTimezone=?, eventTimezone2=? WHERE _id=?", 696 new Object[] {dtstart, null, dtstart2, null, duration, 697 timezone, timezone2, id}); 698 } 699 } 700 } 701 } finally { 702 cursor.close(); 703 } 704 } 705 } 706 707 private void upgradeToVersion66(SQLiteDatabase db) { 708 // Add a column to indicate whether the event organizer can respond to his own events 709 // The UI should not show attendee status for events in calendars with this column = 0 710 db.execSQL("ALTER TABLE " + 711 "Calendars ADD COLUMN organizerCanRespond INTEGER NOT NULL DEFAULT 1;"); 712 } 713 714 private void upgradeToVersion65(SQLiteDatabase db) { 715 // we need to recreate the Events view 716 createEventsView(db); 717 } 718 719 private void upgradeToVersion64(SQLiteDatabase db) { 720 // Add a column that may be used by sync adapters 721 db.execSQL("ALTER TABLE Events ADD COLUMN syncAdapterData TEXT;"); 722 } 723 724 private void upgradeToVersion63(SQLiteDatabase db) { 725 // we need to recreate the Events view 726 createEventsView(db); 727 } 728 729 private void upgradeToVersion62(SQLiteDatabase db) { 730 // New columns are to transition to having allDay events in the local timezone 731 db.execSQL("ALTER TABLE Events ADD COLUMN dtstart2 INTEGER;"); 732 db.execSQL("ALTER TABLE Events ADD COLUMN dtend2 INTEGER;"); 733 db.execSQL("ALTER TABLE Events ADD COLUMN eventTimezone2 TEXT;"); 734 735 String[] allDayBit = new String[] {"0"}; 736 // Copy over all the data that isn't an all day event. 737 db.execSQL("UPDATE Events " + 738 "SET dtstart2=dtstart,dtend2=dtend,eventTimezone2=eventTimezone " + 739 "WHERE allDay=?;", 740 allDayBit /* selection args */); 741 742 // "cursor" iterates over all the calendars 743 allDayBit[0] = "1"; 744 Cursor cursor = db.rawQuery("SELECT Events._id,dtstart,dtend,eventTimezone,timezone " + 745 "FROM Events INNER JOIN Calendars " + 746 "WHERE Events.calendar_id=Calendars._id AND allDay=?", 747 allDayBit /* selection args */); 748 749 Time oldTime = new Time(); 750 Time newTime = new Time(); 751 // Update the allday events in the new columns 752 if (cursor != null) { 753 try { 754 String[] newData = new String[4]; 755 cursor.moveToPosition(-1); 756 while (cursor.moveToNext()) { 757 long id = cursor.getLong(0); // Order from query above 758 long dtstart = cursor.getLong(1); 759 long dtend = cursor.getLong(2); 760 String eTz = cursor.getString(3); // current event timezone 761 String tz = cursor.getString(4); // Calendar timezone 762 //If there's no timezone for some reason use UTC by default. 763 if(eTz == null) { 764 eTz = Time.TIMEZONE_UTC; 765 } 766 767 // Convert start time for all day events into the timezone of their calendar 768 oldTime.clear(eTz); 769 oldTime.set(dtstart); 770 newTime.clear(tz); 771 newTime.set(oldTime.monthDay, oldTime.month, oldTime.year); 772 newTime.normalize(false); 773 dtstart = newTime.toMillis(false /*ignoreDst*/); 774 775 // Convert end time for all day events into the timezone of their calendar 776 oldTime.clear(eTz); 777 oldTime.set(dtend); 778 newTime.clear(tz); 779 newTime.set(oldTime.monthDay, oldTime.month, oldTime.year); 780 newTime.normalize(false); 781 dtend = newTime.toMillis(false /*ignoreDst*/); 782 783 newData[0] = String.valueOf(dtstart); 784 newData[1] = String.valueOf(dtend); 785 newData[2] = tz; 786 newData[3] = String.valueOf(id); 787 db.execSQL("UPDATE Events " + 788 "SET dtstart2=?,dtend2=?,eventTimezone2=? " + 789 "WHERE _id=?", 790 newData); 791 } 792 } finally { 793 cursor.close(); 794 } 795 } 796 } 797 798 private void upgradeToVersion61(SQLiteDatabase db) { 799 createCalendarCacheTable(db); 800 } 801 802 private void upgradeToVersion60(SQLiteDatabase db) { 803 // Switch to CalendarProvider2 804 upgradeSyncState(db); 805 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 806 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 807 "BEGIN " + 808 "DELETE FROM Events WHERE calendar_id = old._id;" + 809 "END"); 810 db.execSQL("ALTER TABLE Events ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;"); 811 db.execSQL("DROP TRIGGER IF EXISTS events_insert"); 812 db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " + 813 "BEGIN " + 814 "UPDATE Events SET _sync_account=" + 815 "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," + 816 "_sync_account_type=" + 817 "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " + 818 "WHERE Events._id=new._id;" + 819 "END"); 820 db.execSQL("DROP TABLE IF EXISTS DeletedEvents;"); 821 db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete"); 822 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " + 823 "BEGIN " + 824 "DELETE FROM Instances WHERE event_id = old._id;" + 825 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" + 826 "DELETE FROM Attendees WHERE event_id = old._id;" + 827 "DELETE FROM Reminders WHERE event_id = old._id;" + 828 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" + 829 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" + 830 "END"); 831 db.execSQL("DROP TRIGGER IF EXISTS attendees_update"); 832 db.execSQL("DROP TRIGGER IF EXISTS attendees_insert"); 833 db.execSQL("DROP TRIGGER IF EXISTS attendees_delete"); 834 db.execSQL("DROP TRIGGER IF EXISTS reminders_update"); 835 db.execSQL("DROP TRIGGER IF EXISTS reminders_insert"); 836 db.execSQL("DROP TRIGGER IF EXISTS reminders_delete"); 837 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update"); 838 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert"); 839 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete"); 840 841 createEventsView(db); 842 } 843 844 private void upgradeToVersion59(SQLiteDatabase db) { 845 db.execSQL("DROP TABLE IF EXISTS BusyBits;"); 846 db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup" + 847 "(_id,localTimezone,minInstance,maxInstance);"); 848 db.execSQL("INSERT INTO CalendarMetaData_Backup " + 849 "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData;"); 850 db.execSQL("DROP TABLE CalendarMetaData;"); 851 createCalendarMetaDataTable(db); 852 db.execSQL("INSERT INTO CalendarMetaData " + 853 "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData_Backup;"); 854 db.execSQL("DROP TABLE CalendarMetaData_Backup;"); 855 } 856 857 private void upgradeToVersion57(SQLiteDatabase db) { 858 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify" 859 + " INTEGER NOT NULL DEFAULT 0;"); 860 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers" 861 + " INTEGER NOT NULL DEFAULT 1;"); 862 db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests" 863 + " INTEGER NOT NULL DEFAULT 1;"); 864 db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;"); 865 db.execSQL("UPDATE Events SET organizer=" 866 + "(SELECT attendeeEmail FROM Attendees WHERE " 867 + "Attendees.event_id = Events._id" 868 + " AND Attendees.attendeeRelationship=2);"); 869 } 870 871 private void upgradeToVersion56(SQLiteDatabase db) { 872 db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;"); 873 db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;"); 874 // Clear _sync_dirty to avoid a client-to-server sync that could blow away 875 // server attendees. 876 // Clear _sync_version to pull down the server's event (with attendees) 877 // Change the URLs from full-selfattendance to full 878 db.execSQL("UPDATE Events" 879 + " SET _sync_dirty=0," 880 + " _sync_version=NULL," 881 + " _sync_id=" 882 + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full')," 883 + " commentsUri =" 884 + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');"); 885 db.execSQL("UPDATE Calendars" 886 + " SET url=" 887 + "REPLACE(url, '/private/full-selfattendance', '/private/full');"); 888 889 // "cursor" iterates over all the calendars 890 Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars", 891 null /* selection args */); 892 // Add the owner column. 893 if (cursor != null) { 894 try { 895 while (cursor.moveToNext()) { 896 Long id = cursor.getLong(0); 897 String url = cursor.getString(1); 898 String owner = calendarEmailAddressFromFeedUrl(url); 899 db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?", 900 new Object[] {owner, id}); 901 } 902 } finally { 903 cursor.close(); 904 } 905 } 906 } 907 908 private void upgradeResync(SQLiteDatabase db) { 909 // Delete sync state, so all records will be re-synced. 910 db.execSQL("DELETE FROM _sync_state;"); 911 912 // "cursor" iterates over all the calendars 913 Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url " 914 + "FROM Calendars", 915 null /* selection args */); 916 if (cursor != null) { 917 try { 918 while (cursor.moveToNext()) { 919 String accountName = cursor.getString(0); 920 String accountType = cursor.getString(1); 921 final Account account = new Account(accountName, accountType); 922 String calendarUrl = cursor.getString(2); 923 scheduleSync(account, false /* two-way sync */, calendarUrl); 924 } 925 } finally { 926 cursor.close(); 927 } 928 } 929 } 930 931 private void upgradeToVersion55(SQLiteDatabase db) { 932 db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;"); 933 db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;"); 934 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;"); 935 db.execSQL("UPDATE Calendars" 936 + " SET _sync_account_type='com.google'" 937 + " WHERE _sync_account IS NOT NULL"); 938 db.execSQL("UPDATE Events" 939 + " SET _sync_account_type='com.google'" 940 + " WHERE _sync_account IS NOT NULL"); 941 db.execSQL("UPDATE DeletedEvents" 942 + " SET _sync_account_type='com.google'" 943 + " WHERE _sync_account IS NOT NULL"); 944 Log.w(TAG, "re-creating eventSyncAccountAndIdIndex"); 945 db.execSQL("DROP INDEX eventSyncAccountAndIdIndex"); 946 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 947 + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " 948 + Calendar.Events._SYNC_ACCOUNT + ", " 949 + Calendar.Events._SYNC_ID + ");"); 950 } 951 952 private void upgradeToVersion54(SQLiteDatabase db) { 953 Log.w(TAG, "adding eventSyncAccountAndIdIndex"); 954 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 955 + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");"); 956 } 957 958 private void upgradeToVersion53(SQLiteDatabase db) { 959 Log.w(TAG, "Upgrading CalendarAlerts table"); 960 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;"); 961 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;"); 962 db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;"); 963 } 964 965 private void upgradeToVersion52(SQLiteDatabase db) { 966 // We added "originalAllDay" to the Events table to keep track of 967 // the allDay status of the original recurring event for entries 968 // that are exceptions to that recurring event. We need this so 969 // that we can format the date correctly for the "originalInstanceTime" 970 // column when we make a change to the recurrence exception and 971 // send it to the server. 972 db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;"); 973 974 // Iterate through the Events table and for each recurrence 975 // exception, fill in the correct value for "originalAllDay", 976 // if possible. The only times where this might not be possible 977 // are (1) the original recurring event no longer exists, or 978 // (2) the original recurring event does not yet have a _sync_id 979 // because it was created on the phone and hasn't been synced to the 980 // server yet. In both cases the originalAllDay field will be set 981 // to null. In the first case we don't care because the recurrence 982 // exception will not be displayed and we won't be able to make 983 // any changes to it (and even if we did, the server should ignore 984 // them, right?). In the second case, the calendar client already 985 // disallows making changes to an instance of a recurring event 986 // until the recurring event has been synced to the server so the 987 // second case should never occur. 988 989 // "cursor" iterates over all the recurrences exceptions. 990 Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events" 991 + " WHERE originalEvent IS NOT NULL", null /* selection args */); 992 if (cursor != null) { 993 try { 994 while (cursor.moveToNext()) { 995 long id = cursor.getLong(0); 996 String originalEvent = cursor.getString(1); 997 998 // Find the original recurring event (if it exists) 999 Cursor recur = db.rawQuery("SELECT allDay FROM Events" 1000 + " WHERE _sync_id=?", new String[] {originalEvent}); 1001 if (recur == null) { 1002 continue; 1003 } 1004 1005 try { 1006 // Fill in the "originalAllDay" field of the 1007 // recurrence exception with the "allDay" value 1008 // from the recurring event. 1009 if (recur.moveToNext()) { 1010 int allDay = recur.getInt(0); 1011 db.execSQL("UPDATE Events SET originalAllDay=" + allDay 1012 + " WHERE _id="+id); 1013 } 1014 } finally { 1015 recur.close(); 1016 } 1017 } 1018 } finally { 1019 cursor.close(); 1020 } 1021 } 1022 } 1023 1024 private void upgradeToVersion51(SQLiteDatabase db) { 1025 Log.w(TAG, "Upgrading DeletedEvents table"); 1026 1027 // We don't have enough information to fill in the correct 1028 // value of the calendar_id for old rows in the DeletedEvents 1029 // table, but rows in that table are transient so it is unlikely 1030 // that there are any rows. Plus, the calendar_id is used only 1031 // when deleting a calendar, which is a rare event. All new rows 1032 // will have the correct calendar_id. 1033 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;"); 1034 1035 // Trigger to remove a calendar's events when we delete the calendar 1036 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 1037 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + 1038 "BEGIN " + 1039 "DELETE FROM Events WHERE calendar_id = old._id;" + 1040 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" + 1041 "END"); 1042 db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted"); 1043 } 1044 1045 private void dropTables(SQLiteDatabase db) { 1046 db.execSQL("DROP TABLE IF EXISTS Calendars;"); 1047 db.execSQL("DROP TABLE IF EXISTS Events;"); 1048 db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;"); 1049 db.execSQL("DROP TABLE IF EXISTS Instances;"); 1050 db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;"); 1051 db.execSQL("DROP TABLE IF EXISTS CalendarCache;"); 1052 db.execSQL("DROP TABLE IF EXISTS Attendees;"); 1053 db.execSQL("DROP TABLE IF EXISTS Reminders;"); 1054 db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;"); 1055 db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;"); 1056 } 1057 1058 @Override 1059 public synchronized SQLiteDatabase getWritableDatabase() { 1060 SQLiteDatabase db = super.getWritableDatabase(); 1061 return db; 1062 } 1063 1064 public SyncStateContentProviderHelper getSyncState() { 1065 return mSyncState; 1066 } 1067 1068 /** 1069 * Schedule a calendar sync for the account. 1070 * @param account the account for which to schedule a sync 1071 * @param uploadChangesOnly if set, specify that the sync should only send 1072 * up local changes. This is typically used for a local sync, a user override of 1073 * too many deletions, or a sync after a calendar is unselected. 1074 * @param url the url feed for the calendar to sync (may be null, in which case a poll of 1075 * all feeds is done.) 1076 */ 1077 void scheduleSync(Account account, boolean uploadChangesOnly, String url) { 1078 Bundle extras = new Bundle(); 1079 if (uploadChangesOnly) { 1080 extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly); 1081 } 1082 if (url != null) { 1083 extras.putString("feed", url); 1084 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 1085 } 1086 ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras); 1087 } 1088 1089 public interface Views { 1090 public static final String EVENTS = "view_events"; 1091 } 1092 1093 public interface Tables { 1094 public static final String EVENTS = "Events"; 1095 public static final String CALENDARS = "Calendars"; 1096 } 1097 1098 private static void createEventsView(SQLiteDatabase db) { 1099 db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";"); 1100 String eventsSelect = "SELECT " 1101 + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + "," 1102 + Calendar.Events.HTML_URI + "," 1103 + Calendar.Events.TITLE + "," 1104 + Calendar.Events.DESCRIPTION + "," 1105 + Calendar.Events.EVENT_LOCATION + "," 1106 + Calendar.Events.STATUS + "," 1107 + Calendar.Events.SELF_ATTENDEE_STATUS + "," 1108 + Calendar.Events.COMMENTS_URI + "," 1109 + Calendar.Events.DTSTART + "," 1110 + Calendar.Events.DTEND + "," 1111 + Calendar.Events.DURATION + "," 1112 + Calendar.Events.EVENT_TIMEZONE + "," 1113 + Calendar.Events.ALL_DAY + "," 1114 + Calendar.Events.VISIBILITY + "," 1115 + Calendar.Events.TIMEZONE + "," 1116 + Calendar.Events.SELECTED + "," 1117 + Calendar.Events.ACCESS_LEVEL + "," 1118 + Calendar.Events.TRANSPARENCY + "," 1119 + Calendar.Events.COLOR + "," 1120 + Calendar.Events.HAS_ALARM + "," 1121 + Calendar.Events.HAS_EXTENDED_PROPERTIES + "," 1122 + Calendar.Events.RRULE + "," 1123 + Calendar.Events.RDATE + "," 1124 + Calendar.Events.EXRULE + "," 1125 + Calendar.Events.EXDATE + "," 1126 + Calendar.Events.ORIGINAL_EVENT + "," 1127 + Calendar.Events.ORIGINAL_INSTANCE_TIME + "," 1128 + Calendar.Events.ORIGINAL_ALL_DAY + "," 1129 + Calendar.Events.LAST_DATE + "," 1130 + Calendar.Events.HAS_ATTENDEE_DATA + "," 1131 + Calendar.Events.CALENDAR_ID + "," 1132 + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + "," 1133 + Calendar.Events.GUESTS_CAN_MODIFY + "," 1134 + Calendar.Events.GUESTS_CAN_SEE_GUESTS + "," 1135 + Calendar.Events.ORGANIZER + "," 1136 + Calendar.Events.DELETED + "," 1137 + Tables.EVENTS + "." + Calendar.Events._SYNC_ID 1138 + " AS " + Calendar.Events._SYNC_ID + "," 1139 + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION 1140 + " AS " + Calendar.Events._SYNC_VERSION + "," 1141 + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY 1142 + " AS " + Calendar.Events._SYNC_DIRTY + "," 1143 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT 1144 + " AS " + Calendar.Events._SYNC_ACCOUNT + "," 1145 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE 1146 + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + "," 1147 + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME 1148 + " AS " + Calendar.Events._SYNC_TIME + "," 1149 + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA 1150 + " AS " + Calendar.Events._SYNC_DATA + "," 1151 + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK 1152 + " AS " + Calendar.Events._SYNC_MARK + "," 1153 + Calendar.Calendars.URL + "," 1154 + Calendar.Calendars.OWNER_ACCOUNT + "," 1155 + Calendar.Calendars.SYNC_EVENTS 1156 + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS 1157 + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID 1158 + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID 1159 + ")"; 1160 1161 db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect); 1162 } 1163 1164 /** 1165 * Extracts the calendar email from a calendar feed url. 1166 * @param feed the calendar feed url 1167 * @return the calendar email that is in the feed url or null if it can't 1168 * find the email address. 1169 * TODO: this is duplicated in CalendarSyncAdapter; move to a library 1170 */ 1171 public static String calendarEmailAddressFromFeedUrl(String feed) { 1172 // Example feed url: 1173 // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees 1174 String[] pathComponents = feed.split("/"); 1175 if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) { 1176 try { 1177 return URLDecoder.decode(pathComponents[5], "UTF-8"); 1178 } catch (UnsupportedEncodingException e) { 1179 Log.e(TAG, "unable to url decode the email address in calendar " + feed); 1180 return null; 1181 } 1182 } 1183 1184 Log.e(TAG, "unable to find the email address in calendar " + feed); 1185 return null; 1186 } 1187 } 1188