CalendarDatabaseHelper.java revision 258fc0a0054d6aba8556921c270379cea6bf3de1
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.common.content.SyncStateContentProviderHelper; 22 23import android.accounts.Account; 24import android.content.ContentResolver; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.res.Resources; 28import android.database.Cursor; 29import android.database.DatabaseUtils; 30import android.database.sqlite.SQLiteDatabase; 31import android.database.sqlite.SQLiteException; 32import android.database.sqlite.SQLiteOpenHelper; 33import android.os.Bundle; 34import android.provider.Calendar; 35import android.provider.ContactsContract; 36import android.provider.SyncStateContract; 37import android.text.TextUtils; 38import android.text.format.Time; 39import android.util.Log; 40 41import java.io.UnsupportedEncodingException; 42import java.net.URLDecoder; 43import java.util.TimeZone; 44 45/** 46 * Database helper for calendar. Designed as a singleton to make sure that all 47 * {@link android.content.ContentProvider} users get the same reference. 48 */ 49/* package */ class CalendarDatabaseHelper extends SQLiteOpenHelper { 50 51 private static final String TAG = "CalendarDatabaseHelper"; 52 53 private static final boolean LOGD = false; 54 55 private static final String DATABASE_NAME = "calendar.db"; 56 57 private static final int DAY_IN_SECONDS = 24 * 60 * 60; 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 = 203; 62 63 private static final int PRE_FROYO_SYNC_STATE_VERSION = 3; 64 65 public interface Tables { 66 public static final String CALENDARS = "Calendars"; 67 public static final String EVENTS = "Events"; 68 public static final String EVENTS_RAW_TIMES = "EventsRawTimes"; 69 public static final String INSTANCES = "Instances"; 70 public static final String ATTENDEES = "Attendees"; 71 public static final String REMINDERS = "Reminders"; 72 public static final String CALENDAR_ALERTS = "CalendarAlerts"; 73 public static final String EXTENDED_PROPERTIES = "ExtendedProperties"; 74 public static final String CALENDAR_META_DATA = "CalendarMetaData"; 75 public static final String CALENDAR_CACHE = "CalendarCache"; 76 public static final String SYNC_STATE = "_sync_state"; 77 public static final String SYNC_STATE_META = "_sync_state_metadata"; 78 } 79 80 public interface Views { 81 public static final String EVENTS = "view_events"; 82 } 83 84 // Copied from SyncStateContentProviderHelper. Don't really want to make them public there. 85 private static final String SYNC_STATE_META_VERSION_COLUMN = "version"; 86 87 private static final String AFTER_EVENT_INSERT_SQL = 88 "UPDATE " + Tables.EVENTS + 89 " SET " + Calendar.Events._SYNC_ACCOUNT + "=" + 90 " (SELECT " + Calendar.Calendars._SYNC_ACCOUNT + " FROM " + Tables.CALENDARS + 91 " WHERE " + Tables.CALENDARS + "." + Calendar.Calendars._ID + 92 "=new." + Calendar.Events.CALENDAR_ID + ")," + 93 Calendar.SyncColumns._SYNC_ACCOUNT_TYPE + "=" + 94 " (SELECT " + Calendar.Calendars._SYNC_ACCOUNT_TYPE + " FROM " + Tables.CALENDARS + 95 " WHERE " + Tables.CALENDARS + "." + Calendar.Calendars._ID + 96 "=new." + Calendar.Events.CALENDAR_ID + ") " + 97 "WHERE " + Tables.EVENTS + "." + Calendar.Events._ID + 98 "=new." + Calendar.Events._ID + ";"; 99 100 // This needs to be done when all the tables are already created 101 private static final String EVENTS_CLEANUP_TRIGGER_SQL = 102 "DELETE FROM " + Tables.INSTANCES + 103 " WHERE "+ Calendar.Instances.EVENT_ID + "=" + 104 "old." + Calendar.Events._ID + ";" + 105 "DELETE FROM " + Tables.EVENTS_RAW_TIMES + 106 " WHERE " + Calendar.EventsRawTimes.EVENT_ID + "=" + 107 "old." + Calendar.Events._ID + ";" + 108 "DELETE FROM " + Tables.ATTENDEES + 109 " WHERE " + Calendar.Attendees.EVENT_ID + "=" + 110 "old." + Calendar.Events._ID + ";" + 111 "DELETE FROM " + Tables.REMINDERS + 112 " WHERE " + Calendar.Reminders.EVENT_ID + "=" + 113 "old." + Calendar.Events._ID + ";" + 114 "DELETE FROM " + Tables.CALENDAR_ALERTS + 115 " WHERE " + Calendar.CalendarAlerts.EVENT_ID + "=" + 116 "old." + Calendar.Events._ID + ";" + 117 "DELETE FROM " + Tables.EXTENDED_PROPERTIES + 118 " WHERE " + Calendar.ExtendedProperties.EVENT_ID + "=" + 119 "old." + Calendar.Events._ID + ";"; 120 121 private static final String CALENDAR_CLEANUP_TRIGGER_SQL = "DELETE FROM " + Tables.EVENTS + 122 " WHERE " + Calendar.Events.CALENDAR_ID + "=" + 123 "old." + Calendar.Events._ID + ";"; 124 125 private static final String SELECT_CALENDAR_CACHE_SQL = 126 "SELECT " + Calendar.CalendarCache.VALUE + 127 " FROM " + Tables.CALENDAR_CACHE + 128 " WHERE " + Calendar.CalendarCache.KEY + "=?"; 129 130 private static final String SCHEMA_HTTPS = "https://"; 131 private static final String SCHEMA_HTTP = "http://"; 132 133 private final Context mContext; 134 private final SyncStateContentProviderHelper mSyncState; 135 136 private static CalendarDatabaseHelper sSingleton = null; 137 138 private DatabaseUtils.InsertHelper mCalendarsInserter; 139 private DatabaseUtils.InsertHelper mEventsInserter; 140 private DatabaseUtils.InsertHelper mEventsRawTimesInserter; 141 private DatabaseUtils.InsertHelper mInstancesInserter; 142 private DatabaseUtils.InsertHelper mAttendeesInserter; 143 private DatabaseUtils.InsertHelper mRemindersInserter; 144 private DatabaseUtils.InsertHelper mCalendarAlertsInserter; 145 private DatabaseUtils.InsertHelper mExtendedPropertiesInserter; 146 147 public long calendarsInsert(ContentValues values) { 148 return mCalendarsInserter.insert(values); 149 } 150 151 public long eventsInsert(ContentValues values) { 152 return mEventsInserter.insert(values); 153 } 154 155 public long eventsRawTimesInsert(ContentValues values) { 156 return mEventsRawTimesInserter.insert(values); 157 } 158 159 public long eventsRawTimesReplace(ContentValues values) { 160 return mEventsRawTimesInserter.replace(values); 161 } 162 163 public long instancesInsert(ContentValues values) { 164 return mInstancesInserter.insert(values); 165 } 166 167 public long instancesReplace(ContentValues values) { 168 return mInstancesInserter.replace(values); 169 } 170 171 public long attendeesInsert(ContentValues values) { 172 return mAttendeesInserter.insert(values); 173 } 174 175 public long remindersInsert(ContentValues values) { 176 return mRemindersInserter.insert(values); 177 } 178 179 public long calendarAlertsInsert(ContentValues values) { 180 return mCalendarAlertsInserter.insert(values); 181 } 182 183 public long extendedPropertiesInsert(ContentValues values) { 184 return mExtendedPropertiesInserter.insert(values); 185 } 186 187 public static synchronized CalendarDatabaseHelper getInstance(Context context) { 188 if (sSingleton == null) { 189 sSingleton = new CalendarDatabaseHelper(context); 190 } 191 return sSingleton; 192 } 193 194 /** 195 * Private constructor, callers except unit tests should obtain an instance through 196 * {@link #getInstance(android.content.Context)} instead. 197 */ 198 /* package */ CalendarDatabaseHelper(Context context) { 199 super(context, DATABASE_NAME, null, DATABASE_VERSION); 200 if (LOGD) Log.d(TAG, "Creating OpenHelper"); 201 Resources resources = context.getResources(); 202 203 mContext = context; 204 mSyncState = new SyncStateContentProviderHelper(); 205 } 206 207 @Override 208 public void onOpen(SQLiteDatabase db) { 209 mSyncState.onDatabaseOpened(db); 210 211 mCalendarsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDARS); 212 mEventsInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS); 213 mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS_RAW_TIMES); 214 mInstancesInserter = new DatabaseUtils.InsertHelper(db, Tables.INSTANCES); 215 mAttendeesInserter = new DatabaseUtils.InsertHelper(db, Tables.ATTENDEES); 216 mRemindersInserter = new DatabaseUtils.InsertHelper(db, Tables.REMINDERS); 217 mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDAR_ALERTS); 218 mExtendedPropertiesInserter = 219 new DatabaseUtils.InsertHelper(db, Tables.EXTENDED_PROPERTIES); 220 } 221 222 /* 223 * Upgrade sync state table if necessary. Note that the data bundle 224 * in the table is not upgraded. 225 * 226 * The sync state used to be stored with version 3, but now uses the 227 * same sync state code as contacts, which is version 1. This code 228 * upgrades from 3 to 1 if necessary. (Yes, the numbers are unfortunately 229 * backwards.) 230 * 231 * This code is only called when upgrading from an old calendar version, 232 * so there is no problem if sync state version 3 gets used again in the 233 * future. 234 */ 235 private void upgradeSyncState(SQLiteDatabase db) { 236 long version = DatabaseUtils.longForQuery(db, 237 "SELECT " + SYNC_STATE_META_VERSION_COLUMN 238 + " FROM " + Tables.SYNC_STATE_META, 239 null); 240 if (version == PRE_FROYO_SYNC_STATE_VERSION) { 241 Log.i(TAG, "Upgrading calendar sync state table"); 242 db.execSQL("CREATE TEMPORARY TABLE state_backup(_sync_account TEXT, " 243 + "_sync_account_type TEXT, data TEXT);"); 244 db.execSQL("INSERT INTO state_backup SELECT _sync_account, _sync_account_type, data" 245 + " FROM " 246 + Tables.SYNC_STATE 247 + " WHERE _sync_account is not NULL and _sync_account_type is not NULL;"); 248 db.execSQL("DROP TABLE " + Tables.SYNC_STATE + ";"); 249 mSyncState.onDatabaseOpened(db); 250 db.execSQL("INSERT INTO " + Tables.SYNC_STATE + "(" 251 + SyncStateContract.Columns.ACCOUNT_NAME + "," 252 + SyncStateContract.Columns.ACCOUNT_TYPE + "," 253 + SyncStateContract.Columns.DATA 254 + ") SELECT _sync_account, _sync_account_type, data from state_backup;"); 255 db.execSQL("DROP TABLE state_backup;"); 256 } else { 257 // Wrong version to upgrade. 258 // Don't need to do anything more here because mSyncState.onDatabaseOpened() will blow 259 // away and recreate the database (which will result in a resync). 260 Log.w(TAG, "upgradeSyncState: current version is " + version + ", skipping upgrade."); 261 } 262 } 263 264 @Override 265 public void onCreate(SQLiteDatabase db) { 266 bootstrapDB(db); 267 } 268 269 private void bootstrapDB(SQLiteDatabase db) { 270 Log.i(TAG, "Bootstrapping database"); 271 272 mSyncState.createDatabase(db); 273 274 createCalendarsTable(db); 275 276 // TODO: do we need both dtend and duration? 277 db.execSQL("CREATE TABLE " + Tables.EVENTS + " (" + 278 Calendar.Events._ID + " INTEGER PRIMARY KEY," + 279 Calendar.Events._SYNC_ACCOUNT + " TEXT," + 280 Calendar.Events._SYNC_ACCOUNT_TYPE + " TEXT," + 281 Calendar.Events._SYNC_ID + " TEXT," + 282 Calendar.Events._SYNC_VERSION + " TEXT," + 283 // sync time in UTC 284 Calendar.Events._SYNC_TIME + " TEXT," + 285 Calendar.Events._SYNC_DATA + " INTEGER," + 286 Calendar.Events._SYNC_DIRTY + " INTEGER," + 287 // sync mark to filter out new rows 288 Calendar.Events._SYNC_MARK + " INTEGER," + 289 Calendar.Events.CALENDAR_ID + " INTEGER NOT NULL," + 290 Calendar.Events.HTML_URI + " TEXT," + 291 Calendar.Events.TITLE + " TEXT," + 292 Calendar.Events.EVENT_LOCATION + " TEXT," + 293 Calendar.Events.DESCRIPTION + " TEXT," + 294 Calendar.Events.STATUS + " INTEGER," + 295 Calendar.Events.SELF_ATTENDEE_STATUS + " INTEGER NOT NULL DEFAULT 0," + 296 Calendar.Events.COMMENTS_URI + " TEXT," + 297 // dtstart in millis since epoch 298 Calendar.Events.DTSTART + " INTEGER," + 299 // dtend in millis since epoch 300 Calendar.Events.DTEND + " INTEGER," + 301 // timezone for event 302 Calendar.Events.EVENT_TIMEZONE + " TEXT," + 303 Calendar.Events.DURATION + " TEXT," + 304 Calendar.Events.ALL_DAY + " INTEGER NOT NULL DEFAULT 0," + 305 Calendar.Events.VISIBILITY + " INTEGER NOT NULL DEFAULT 0," + 306 Calendar.Events.TRANSPARENCY + " INTEGER NOT NULL DEFAULT 0," + 307 Calendar.Events.HAS_ALARM + " INTEGER NOT NULL DEFAULT 0," + 308 Calendar.Events.HAS_EXTENDED_PROPERTIES + " INTEGER NOT NULL DEFAULT 0," + 309 Calendar.Events.RRULE + " TEXT," + 310 Calendar.Events.RDATE + " TEXT," + 311 Calendar.Events.EXRULE + " TEXT," + 312 Calendar.Events.EXDATE + " TEXT," + 313 // originalEvent is the _sync_id of recurring event 314 Calendar.Events.ORIGINAL_EVENT + " TEXT," + 315 // originalInstanceTime is in millis since epoch 316 Calendar.Events.ORIGINAL_INSTANCE_TIME + " INTEGER," + 317 Calendar.Events.ORIGINAL_ALL_DAY + " INTEGER," + 318 // lastDate is in millis since epoch 319 Calendar.Events.LAST_DATE + " INTEGER," + 320 Calendar.Events.HAS_ATTENDEE_DATA + " INTEGER NOT NULL DEFAULT 0," + 321 Calendar.Events.GUESTS_CAN_MODIFY + " INTEGER NOT NULL DEFAULT 0," + 322 Calendar.Events.GUESTS_CAN_INVITE_OTHERS + " INTEGER NOT NULL DEFAULT 1," + 323 Calendar.Events.GUESTS_CAN_SEE_GUESTS + " INTEGER NOT NULL DEFAULT 1," + 324 Calendar.Events.ORGANIZER + " STRING," + 325 Calendar.Events.DELETED + " INTEGER NOT NULL DEFAULT 0," + 326 // dstart2 is in millis since epoch, allDay events are in local timezone 327 Calendar.Events.DTSTART2 + " INTEGER," + 328 // dtend2 is in millis since epoch, allDay events are in local timezone 329 Calendar.Events.DTEND2 + " INTEGER," + 330 // timezone for event with allDay events are in local timezone 331 Calendar.Events.EVENT_TIMEZONE2 + " TEXT," + 332 // syncAdapterData is available for use by sync adapters 333 Calendar.Events.SYNC_ADAPTER_DATA + " TEXT" + 334 ");"); 335 336 // Trigger to set event's sync_account 337 db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON " + Tables.EVENTS + " " + 338 "BEGIN " + 339 AFTER_EVENT_INSERT_SQL + 340 "END"); 341 342 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON " + Tables.EVENTS + " (" + 343 Calendar.Events._SYNC_ACCOUNT_TYPE + ", " + 344 Calendar.Events._SYNC_ACCOUNT + ", " + 345 Calendar.Events._SYNC_ID + 346 ");"); 347 348 db.execSQL("CREATE INDEX eventsCalendarIdIndex ON " + Tables.EVENTS + " (" + 349 Calendar.Events.CALENDAR_ID + 350 ");"); 351 352 db.execSQL("CREATE TABLE " + Tables.EVENTS_RAW_TIMES + " (" + 353 Calendar.EventsRawTimes._ID + " INTEGER PRIMARY KEY," + 354 Calendar.EventsRawTimes.EVENT_ID + " INTEGER NOT NULL," + 355 Calendar.EventsRawTimes.DTSTART_2445 + " TEXT," + 356 Calendar.EventsRawTimes.DTEND_2445 + " TEXT," + 357 Calendar.EventsRawTimes.ORIGINAL_INSTANCE_TIME_2445 + " TEXT," + 358 Calendar.EventsRawTimes.LAST_DATE_2445 + " TEXT," + 359 "UNIQUE (" + Calendar.EventsRawTimes.EVENT_ID + ")" + 360 ");"); 361 362 db.execSQL("CREATE TABLE " + Tables.INSTANCES + " (" + 363 Calendar.Instances._ID + " INTEGER PRIMARY KEY," + 364 Calendar.Instances.EVENT_ID + " INTEGER," + 365 Calendar.Instances.BEGIN + " INTEGER," + // UTC millis 366 Calendar.Instances.END + " INTEGER," + // UTC millis 367 Calendar.Instances.START_DAY + " INTEGER," + // Julian start day 368 Calendar.Instances.END_DAY + " INTEGER," + // Julian end day 369 Calendar.Instances.START_MINUTE + " INTEGER," + // minutes from midnight 370 Calendar.Instances.END_MINUTE + " INTEGER," + // minutes from midnight 371 "UNIQUE (" + 372 Calendar.Instances.EVENT_ID + ", " + 373 Calendar.Instances.BEGIN + ", " + 374 Calendar.Instances.END + ")" + 375 ");"); 376 377 db.execSQL("CREATE INDEX instancesStartDayIndex ON " + Tables.INSTANCES + " (" + 378 Calendar.Instances.START_DAY + 379 ");"); 380 381 createCalendarMetaDataTable(db); 382 383 createCalendarCacheTable(db, null); 384 385 db.execSQL("CREATE TABLE " + Tables.ATTENDEES + " (" + 386 Calendar.Attendees._ID + " INTEGER PRIMARY KEY," + 387 Calendar.Attendees.EVENT_ID + " INTEGER," + 388 Calendar.Attendees.ATTENDEE_NAME + " TEXT," + 389 Calendar.Attendees.ATTENDEE_EMAIL + " TEXT," + 390 Calendar.Attendees.ATTENDEE_STATUS + " INTEGER," + 391 Calendar.Attendees.ATTENDEE_RELATIONSHIP + " INTEGER," + 392 Calendar.Attendees.ATTENDEE_TYPE + " INTEGER" + 393 ");"); 394 395 db.execSQL("CREATE INDEX attendeesEventIdIndex ON " + Tables.ATTENDEES + " (" + 396 Calendar.Attendees.EVENT_ID + 397 ");"); 398 399 db.execSQL("CREATE TABLE " + Tables.REMINDERS + " (" + 400 Calendar.Reminders._ID + " INTEGER PRIMARY KEY," + 401 Calendar.Reminders.EVENT_ID + " INTEGER," + 402 Calendar.Reminders.MINUTES + " INTEGER," + 403 Calendar.Reminders.METHOD + " INTEGER NOT NULL" + 404 " DEFAULT " + Calendar.Reminders.METHOD_DEFAULT + 405 ");"); 406 407 db.execSQL("CREATE INDEX remindersEventIdIndex ON " + Tables.REMINDERS + " (" + 408 Calendar.Reminders.EVENT_ID + 409 ");"); 410 411 // This table stores the Calendar notifications that have gone off. 412 db.execSQL("CREATE TABLE " + Tables.CALENDAR_ALERTS + " (" + 413 Calendar.CalendarAlerts._ID + " INTEGER PRIMARY KEY," + 414 Calendar.CalendarAlerts.EVENT_ID + " INTEGER," + 415 Calendar.CalendarAlerts.BEGIN + " INTEGER NOT NULL," + // UTC millis 416 Calendar.CalendarAlerts.END + " INTEGER NOT NULL," + // UTC millis 417 Calendar.CalendarAlerts.ALARM_TIME + " INTEGER NOT NULL," + // UTC millis 418 Calendar.CalendarAlerts.CREATION_TIME + " INTEGER NOT NULL," + // UTC millis 419 Calendar.CalendarAlerts.RECEIVED_TIME + " INTEGER NOT NULL," + // UTC millis 420 Calendar.CalendarAlerts.NOTIFY_TIME + " INTEGER NOT NULL," + // UTC millis 421 Calendar.CalendarAlerts.STATE + " INTEGER NOT NULL," + 422 Calendar.CalendarAlerts.MINUTES + " INTEGER," + 423 "UNIQUE (" + 424 Calendar.CalendarAlerts.ALARM_TIME + ", " + 425 Calendar.CalendarAlerts.BEGIN + ", " + 426 Calendar.CalendarAlerts.EVENT_ID + ")" + 427 ");"); 428 429 db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON " + Tables.CALENDAR_ALERTS + " (" + 430 Calendar.CalendarAlerts.EVENT_ID + 431 ");"); 432 433 db.execSQL("CREATE TABLE " + Tables.EXTENDED_PROPERTIES + " (" + 434 Calendar.ExtendedProperties._ID + " INTEGER PRIMARY KEY," + 435 Calendar.ExtendedProperties.EVENT_ID + " INTEGER," + 436 Calendar.ExtendedProperties.NAME + " TEXT," + 437 Calendar.ExtendedProperties.VALUE + " TEXT" + 438 ");"); 439 440 db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON " + Tables.EXTENDED_PROPERTIES 441 + " (" + 442 Calendar.ExtendedProperties.EVENT_ID + 443 ");"); 444 445 createEventsView(db); 446 447 // Trigger to remove data tied to an event when we delete that event. 448 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " + 449 "BEGIN " + 450 EVENTS_CLEANUP_TRIGGER_SQL + 451 "END"); 452 453 ContentResolver.requestSync(null /* all accounts */, 454 ContactsContract.AUTHORITY, new Bundle()); 455 } 456 457 private void createCalendarsTable(SQLiteDatabase db) { 458 db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" + 459 Calendar.Calendars._ID + " INTEGER PRIMARY KEY," + 460 Calendar.Calendars._SYNC_ACCOUNT + " TEXT," + 461 Calendar.Calendars._SYNC_ACCOUNT_TYPE + " TEXT," + 462 Calendar.Calendars._SYNC_ID + " TEXT," + 463 Calendar.Calendars._SYNC_VERSION + " TEXT," + 464 Calendar.Calendars._SYNC_TIME + " TEXT," + // UTC 465 Calendar.Calendars._SYNC_DATA + " INTEGER," + 466 Calendar.Calendars._SYNC_DIRTY + " INTEGER," + 467 Calendar.Calendars._SYNC_MARK + " INTEGER," + // Used to filter out new rows 468 Calendar.Calendars.NAME + " TEXT," + 469 Calendar.Calendars.DISPLAY_NAME + " TEXT," + 470 Calendar.Calendars.COLOR + " INTEGER," + 471 Calendar.Calendars.ACCESS_LEVEL + " INTEGER," + 472 Calendar.Calendars.SELECTED + " INTEGER NOT NULL DEFAULT 1," + 473 Calendar.Calendars.SYNC_EVENTS + " INTEGER NOT NULL DEFAULT 0," + 474 Calendar.Calendars.LOCATION + " TEXT," + 475 Calendar.Calendars.TIMEZONE + " TEXT," + 476 Calendar.Calendars.OWNER_ACCOUNT + " TEXT, " + 477 Calendar.Calendars.ORGANIZER_CAN_RESPOND + " INTEGER NOT NULL DEFAULT 1," + 478 Calendar.Calendars.DELETED + " INTEGER NOT NULL DEFAULT 0," + 479 Calendar.Calendars.SYNC1 + " TEXT," + 480 Calendar.Calendars.SYNC2 + " TEXT," + 481 Calendar.Calendars.SYNC3 + " TEXT," + 482 Calendar.Calendars.SYNC4 + " TEXT," + 483 Calendar.Calendars.SYNC5 + " TEXT" + 484 ");"); 485 486 // Trigger to remove a calendar's events when we delete the calendar 487 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " + 488 "BEGIN " + 489 CALENDAR_CLEANUP_TRIGGER_SQL + 490 "END"); 491 } 492 493 private void createCalendarMetaDataTable(SQLiteDatabase db) { 494 db.execSQL("CREATE TABLE " + Tables.CALENDAR_META_DATA + " (" + 495 Calendar.CalendarMetaData._ID + " INTEGER PRIMARY KEY," + 496 Calendar.CalendarMetaData.LOCAL_TIMEZONE + " TEXT," + 497 Calendar.CalendarMetaData.MIN_INSTANCE + " INTEGER," + // UTC millis 498 Calendar.CalendarMetaData.MAX_INSTANCE + " INTEGER" + // UTC millis 499 ");"); 500 } 501 502 private void createCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) { 503 // This is a hack because versioning skipped version number 61 of schema 504 // TODO after version 70 this can be removed 505 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";"); 506 507 // IF NOT EXISTS should be normal pattern for table creation 508 db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.CALENDAR_CACHE + " (" + 509 CalendarCache.COLUMN_NAME_ID + " INTEGER PRIMARY KEY," + 510 CalendarCache.COLUMN_NAME_KEY + " TEXT NOT NULL," + 511 CalendarCache.COLUMN_NAME_VALUE + " TEXT" + 512 ");"); 513 514 initCalendarCacheTable(db, oldTimezoneDbVersion); 515 updateCalendarCacheTableTo203(db); 516 } 517 518 private void initCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) { 519 String timezoneDbVersion = (oldTimezoneDbVersion != null) ? 520 oldTimezoneDbVersion : CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION; 521 522 // Set the default timezone database version 523 db.execSQL("INSERT OR REPLACE INTO " + Tables.CALENDAR_CACHE + 524 " (" + CalendarCache.COLUMN_NAME_ID + ", " + 525 CalendarCache.COLUMN_NAME_KEY + ", " + 526 CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" + 527 CalendarCache.KEY_TIMEZONE_DATABASE_VERSION.hashCode() + "," + 528 "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "'," + 529 "'" + timezoneDbVersion + "'" + 530 ");"); 531 } 532 533 private void updateCalendarCacheTableTo203(SQLiteDatabase db) { 534 // Define the default timezone type for Instances timezone management 535 db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE + 536 " (" + CalendarCache.COLUMN_NAME_ID + ", " + 537 CalendarCache.COLUMN_NAME_KEY + ", " + 538 CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" + 539 CalendarCache.KEY_TIMEZONE_TYPE.hashCode() + "," + 540 "'" + CalendarCache.KEY_TIMEZONE_TYPE + "'," + 541 "'" + CalendarCache.TIMEZONE_TYPE_AUTO + "'" + 542 ");"); 543 544 String defaultTimezone = TimeZone.getDefault().getID(); 545 546 // Define the default timezone for Instances 547 db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE + 548 " (" + CalendarCache.COLUMN_NAME_ID + ", " + 549 CalendarCache.COLUMN_NAME_KEY + ", " + 550 CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" + 551 CalendarCache.KEY_TIMEZONE_INSTANCES.hashCode() + "," + 552 "'" + CalendarCache.KEY_TIMEZONE_INSTANCES + "'," + 553 "'" + defaultTimezone + "'" + 554 ");"); 555 556 // Define the default previous timezone for Instances 557 db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE + 558 " (" + CalendarCache.COLUMN_NAME_ID + ", " + 559 CalendarCache.COLUMN_NAME_KEY + ", " + 560 CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" + 561 CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS.hashCode() + "," + 562 "'" + CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + "'," + 563 "'" + defaultTimezone + "'" + 564 ");"); 565 } 566 567 @Override 568 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 569 Log.i(TAG, "Upgrading DB from version " + oldVersion 570 + " to " + newVersion); 571 if (oldVersion < 49) { 572 dropTables(db); 573 mSyncState.createDatabase(db); 574 return; // this was lossy 575 } 576 577 // From schema versions 59 to version 66, the CalendarMetaData table definition had lost 578 // the primary key leading to having the CalendarMetaData with multiple rows instead of 579 // only one. The Instance table was then corrupted (during Instance expansion we are using 580 // the localTimezone, minInstance and maxInstance from CalendarMetaData table. 581 // This boolean helps us tracking the need to recreate the CalendarMetaData table and 582 // clear the Instance table (and thus force an Instance expansion). 583 boolean recreateMetaDataAndInstances = (oldVersion >= 59 && oldVersion <= 66); 584 585 try { 586 if (oldVersion < 51) { 587 upgradeToVersion51(db); // From 50 or 51 588 oldVersion = 51; 589 } 590 if (oldVersion == 51) { 591 upgradeToVersion52(db); 592 oldVersion += 1; 593 } 594 if (oldVersion == 52) { 595 upgradeToVersion53(db); 596 oldVersion += 1; 597 } 598 if (oldVersion == 53) { 599 upgradeToVersion54(db); 600 oldVersion += 1; 601 } 602 if (oldVersion == 54) { 603 upgradeToVersion55(db); 604 oldVersion += 1; 605 } 606 if (oldVersion == 55 || oldVersion == 56) { 607 // Both require resync, so just schedule it once 608 upgradeResync(db); 609 } 610 if (oldVersion == 55) { 611 upgradeToVersion56(db); 612 oldVersion += 1; 613 } 614 if (oldVersion == 56) { 615 upgradeToVersion57(db); 616 oldVersion += 1; 617 } 618 if (oldVersion == 57) { 619 // Changes are undone upgrading to 60, so don't do anything. 620 oldVersion += 1; 621 } 622 if (oldVersion == 58) { 623 upgradeToVersion59(db); 624 oldVersion += 1; 625 } 626 if (oldVersion == 59) { 627 upgradeToVersion60(db); 628 oldVersion += 1; 629 } 630 if (oldVersion == 60) { 631 upgradeToVersion61(db); 632 oldVersion += 1; 633 } 634 if (oldVersion == 61) { 635 upgradeToVersion62(db); 636 oldVersion += 1; 637 } 638 if (oldVersion == 62) { 639 upgradeToVersion63(db); 640 oldVersion += 1; 641 } 642 if (oldVersion == 63) { 643 upgradeToVersion64(db); 644 oldVersion += 1; 645 } 646 if (oldVersion == 64) { 647 upgradeToVersion65(db); 648 oldVersion += 1; 649 } 650 if (oldVersion == 65) { 651 upgradeToVersion66(db); 652 oldVersion += 1; 653 } 654 if (oldVersion == 66) { 655 // Changes are done thru recreateMetaDataAndInstances() method 656 oldVersion += 1; 657 } 658 if (recreateMetaDataAndInstances) { 659 recreateMetaDataAndInstances(db); 660 } 661 if (oldVersion == 67 || oldVersion == 68) { 662 upgradeToVersion69(db); 663 oldVersion = 69; 664 } 665 // 69. 70 are for Froyo/old Gingerbread only and 100s are for Gingerbread only 666 // 70 and 71 have been for Honeycomb but no more used 667 // 72 and 73 and 74 were for Honeycomb only but are considered as obsolete for enabling 668 // room for Froyo version numbers 669 if(oldVersion == 69) { 670 upgradeToVersion200(db); 671 oldVersion = 200; 672 } 673 if (oldVersion == 70) { 674 upgradeToVersion200(db); 675 oldVersion = 200; 676 } 677 if (oldVersion == 100) { 678 upgradeToVersion200(db); 679 oldVersion = 200; 680 } 681 boolean need203Update = true; 682 if (oldVersion == 101) { 683 oldVersion = 200; 684 // Gingerbread version 101 is similar to Honeycomb version 203 685 need203Update = false; 686 } 687 if (oldVersion == 200) { 688 upgradeToVersion201(db); 689 oldVersion += 1; 690 } 691 if (oldVersion == 201) { 692 upgradeToVersion202(db); 693 oldVersion += 1; 694 } 695 if (oldVersion == 202 && need203Update) { 696 upgradeToVersion203(db); 697 oldVersion += 1; 698 } 699 if (oldVersion != DATABASE_VERSION) { 700 Log.e(TAG, "Need to recreate Calendar schema because of " 701 + "unknown Calendar database version: " + oldVersion); 702 dropTables(db); 703 bootstrapDB(db); 704 oldVersion = DATABASE_VERSION; 705 } 706 } catch (SQLiteException e) { 707 Log.e(TAG, "onUpgrade: SQLiteException, recreating db. " + e); 708 dropTables(db); 709 bootstrapDB(db); 710 return; // this was lossy 711 } 712 } 713 714 /** 715 * If the user_version of the database if between 59 and 66 (those versions has been deployed 716 * with no primary key for the CalendarMetaData table) 717 */ 718 private void recreateMetaDataAndInstances(SQLiteDatabase db) { 719 // Recreate the CalendarMetaData table with correct primary key 720 db.execSQL("DROP TABLE " + Tables.CALENDAR_META_DATA + ";"); 721 createCalendarMetaDataTable(db); 722 723 // Also clean the Instance table as this table may be corrupted 724 db.execSQL("DELETE FROM " + Tables.INSTANCES + ";"); 725 } 726 727 private static boolean fixAllDayTime(Time time, String timezone, Long timeInMillis) { 728 time.set(timeInMillis); 729 if(time.hour != 0 || time.minute != 0 || time.second != 0) { 730 time.hour = 0; 731 time.minute = 0; 732 time.second = 0; 733 return true; 734 } 735 return false; 736 } 737 738 @VisibleForTesting 739 void upgradeToVersion203(SQLiteDatabase db) { 740 // Same as Gingerbread version 100 741 Cursor cursor = db.rawQuery(SELECT_CALENDAR_CACHE_SQL, 742 new String[] {"timezoneDatabaseVersion"}); 743 744 String oldTimezoneDbVersion = null; 745 if (cursor != null && cursor.moveToNext()) { 746 try { 747 oldTimezoneDbVersion = cursor.getString(0); 748 } finally { 749 cursor.close(); 750 } 751 // Also clean the CalendarCache table 752 db.execSQL("DELETE FROM " + Tables.CALENDAR_CACHE + ";"); 753 } 754 initCalendarCacheTable(db, oldTimezoneDbVersion); 755 756 // Same as Gingerbread version 101 757 updateCalendarCacheTableTo203(db); 758 } 759 760 @VisibleForTesting 761 void upgradeToVersion202(SQLiteDatabase db) { 762 // We will drop the "hidden" column from the calendar schema and add the "sync5" column 763 db.execSQL("ALTER TABLE " + Tables.CALENDARS +" RENAME TO " + 764 Tables.CALENDARS + "_Backup;"); 765 766 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 767 createCalendarsTable(db); 768 769 // Populate the new Calendars table and put into the "sync5" column the value of the 770 // old "hidden" column 771 db.execSQL("INSERT INTO " + Tables.CALENDARS + " (" + 772 Calendar.Calendars._ID + ", " + 773 Calendar.Calendars._SYNC_ACCOUNT + ", " + 774 Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " + 775 Calendar.Calendars._SYNC_ID + ", " + 776 Calendar.Calendars._SYNC_VERSION + ", " + 777 Calendar.Calendars._SYNC_TIME + ", " + 778 Calendar.Calendars._SYNC_DATA + ", " + 779 Calendar.Calendars._SYNC_DIRTY + ", " + 780 Calendar.Calendars._SYNC_MARK + ", " + 781 Calendar.Calendars.NAME + ", " + 782 Calendar.Calendars.DISPLAY_NAME + ", " + 783 Calendar.Calendars.COLOR + ", " + 784 Calendar.Calendars.ACCESS_LEVEL + ", " + 785 Calendar.Calendars.SELECTED + ", " + 786 Calendar.Calendars.SYNC_EVENTS + ", " + 787 Calendar.Calendars.LOCATION + ", " + 788 Calendar.Calendars.TIMEZONE + ", " + 789 Calendar.Calendars.OWNER_ACCOUNT + ", " + 790 Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " + 791 Calendar.Calendars.DELETED + ", " + 792 Calendar.Calendars.SYNC1 + ", " + 793 Calendar.Calendars.SYNC2 + ", " + 794 Calendar.Calendars.SYNC3 + ", " + 795 Calendar.Calendars.SYNC4 + ", " + 796 Calendar.Calendars.SYNC5 + ") " + 797 "SELECT " + 798 Calendar.Calendars._ID + ", " + 799 Calendar.Calendars._SYNC_ACCOUNT + ", " + 800 Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " + 801 Calendar.Calendars._SYNC_ID + ", " + 802 Calendar.Calendars._SYNC_VERSION + ", " + 803 Calendar.Calendars._SYNC_TIME + ", " + 804 Calendar.Calendars._SYNC_DATA + ", " + 805 Calendar.Calendars._SYNC_DIRTY + ", " + 806 Calendar.Calendars._SYNC_MARK + ", " + 807 Calendar.Calendars.NAME + ", " + 808 Calendar.Calendars.DISPLAY_NAME + ", " + 809 Calendar.Calendars.COLOR + ", " + 810 Calendar.Calendars.ACCESS_LEVEL + ", " + 811 Calendar.Calendars.SELECTED + ", " + 812 Calendar.Calendars.SYNC_EVENTS + ", " + 813 Calendar.Calendars.LOCATION + ", " + 814 Calendar.Calendars.TIMEZONE + ", " + 815 Calendar.Calendars.OWNER_ACCOUNT + ", " + 816 Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " + 817 Calendar.Calendars.DELETED + ", " + 818 Calendar.Calendars.SYNC1 + ", " + 819 Calendar.Calendars.SYNC2 + ", " + 820 Calendar.Calendars.SYNC3 + ", " + 821 Calendar.Calendars.SYNC4 + " " + 822 "hidden" + " " + 823 "FROM " + Tables.CALENDARS + "_Backup" + ";" 824 ); 825 826 // Drop the backup table 827 db.execSQL("DROP TABLE " + Tables.CALENDARS + "_Backup;"); 828 829 // Recreate the Events Views as column "hidden" has been deleted 830 createEventsView(db); 831 } 832 833 @VisibleForTesting 834 void upgradeToVersion201(SQLiteDatabase db) { 835 db.execSQL("ALTER TABLE " + Tables.CALENDARS + 836 " ADD COLUMN " + Calendar.Calendars.SYNC4 + " TEXT;"); 837 } 838 839 @VisibleForTesting 840 void upgradeToVersion200(SQLiteDatabase db) { 841 // we cannot use here a Calendar.Calendars,URL constant for "url" as we are trying to make 842 // it disappear so we are keeping the hardcoded name "url" in all the SQLs 843 db.execSQL("ALTER TABLE " + Tables.CALENDARS +" RENAME TO " + 844 Tables.CALENDARS + "_Backup;"); 845 846 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 847 createCalendarsTable(db); 848 849 // Populate the new Calendars table except the SYNC2 / SYNC3 columns 850 db.execSQL("INSERT INTO " + Tables.CALENDARS + " (" + 851 Calendar.Calendars._ID + ", " + 852 Calendar.Calendars._SYNC_ACCOUNT + ", " + 853 Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " + 854 Calendar.Calendars._SYNC_ID + ", " + 855 Calendar.Calendars._SYNC_VERSION + ", " + 856 Calendar.Calendars._SYNC_TIME + ", " + 857 Calendar.Calendars._SYNC_DATA + ", " + 858 Calendar.Calendars._SYNC_DIRTY + ", " + 859 Calendar.Calendars._SYNC_MARK + ", " + 860 Calendar.Calendars.NAME + ", " + 861 Calendar.Calendars.DISPLAY_NAME + ", " + 862 Calendar.Calendars.COLOR + ", " + 863 Calendar.Calendars.ACCESS_LEVEL + ", " + 864 Calendar.Calendars.SELECTED + ", " + 865 Calendar.Calendars.SYNC_EVENTS + ", " + 866 Calendar.Calendars.LOCATION + ", " + 867 Calendar.Calendars.TIMEZONE + ", " + 868 Calendar.Calendars.OWNER_ACCOUNT + ", " + 869 Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " + 870 Calendar.Calendars.DELETED + ", " + 871 Calendar.Calendars.SYNC1 + ") " + 872 "SELECT " + 873 Calendar.Calendars._ID + ", " + 874 Calendar.Calendars._SYNC_ACCOUNT + ", " + 875 Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " + 876 Calendar.Calendars._SYNC_ID + ", " + 877 Calendar.Calendars._SYNC_VERSION + ", " + 878 Calendar.Calendars._SYNC_TIME + ", " + 879 Calendar.Calendars._SYNC_DATA + ", " + 880 Calendar.Calendars._SYNC_DIRTY + ", " + 881 Calendar.Calendars._SYNC_MARK + ", " + 882 Calendar.Calendars.NAME + ", " + 883 Calendar.Calendars.DISPLAY_NAME + ", " + 884 Calendar.Calendars.COLOR + ", " + 885 Calendar.Calendars.ACCESS_LEVEL + ", " + 886 Calendar.Calendars.SELECTED + ", " + 887 Calendar.Calendars.SYNC_EVENTS + ", " + 888 Calendar.Calendars.LOCATION + ", " + 889 Calendar.Calendars.TIMEZONE + ", " + 890 Calendar.Calendars.OWNER_ACCOUNT + ", " + 891 Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " + 892 "0" + ", " + 893 "url" + " " + 894 "FROM " + Tables.CALENDARS + "_Backup" + ";" 895 ); 896 897 // Populate SYNC2 and SYNC3 columns - SYNC1 represent the old "url" column 898 // We will need to iterate over all the "com.google" type of calendars 899 String selectSql = "SELECT " + Calendar.Calendars._ID + ", " + "url" + 900 " FROM " + Tables.CALENDARS + "_Backup" + 901 " WHERE " + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "='com.google'" + 902 " AND url IS NOT NULL;"; 903 904 String updateSql = "UPDATE " + Tables.CALENDARS + " SET " + 905 Calendar.Calendars.SYNC2 + "=?, " + // edit Url 906 Calendar.Calendars.SYNC3 + "=? " + // self Url 907 "WHERE " + Calendar.Calendars._ID + "=?;"; 908 909 Cursor cursor = db.rawQuery(selectSql, null /* selection args */); 910 if (cursor != null && cursor.getCount() > 0) { 911 try { 912 Object[] bindArgs = new Object[3]; 913 914 while (cursor.moveToNext()) { 915 Long id = cursor.getLong(0); 916 String url = cursor.getString(1); 917 String selfUrl = getSelfUrlFromEventsUrl(url); 918 String editUrl = getEditUrlFromEventsUrl(url); 919 920 bindArgs[0] = editUrl; 921 bindArgs[1] = selfUrl; 922 bindArgs[2] = id; 923 924 db.execSQL(updateSql, bindArgs); 925 } 926 } finally { 927 cursor.close(); 928 } 929 } 930 931 // Drop the backup table 932 db.execSQL("DROP TABLE " + Tables.CALENDARS + "_Backup;"); 933 934 // Recreate the Events Views as column "deleted" is now ambiguous 935 // ("deleted" is now defined in both Calendars and Events tables) 936 createEventsView(db); 937 } 938 939 @VisibleForTesting 940 static void upgradeToVersion69(SQLiteDatabase db) { 941 // Clean up allDay events which could be in an invalid state from an earlier version 942 // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This 943 // will go through the allDay events and make sure they have proper values and are in the 944 // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that 945 // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2 946 // and dtend2 are at midnight in their timezone. 947 final String sql = "SELECT " + Calendar.Events._ID + ", " + 948 Calendar.Events.DTSTART + ", " + 949 Calendar.Events.DTEND + ", " + 950 Calendar.Events.DURATION + ", " + 951 Calendar.Events.DTSTART2 + ", " + 952 Calendar.Events.DTEND2 + ", " + 953 Calendar.Events.EVENT_TIMEZONE + ", " + 954 Calendar.Events.EVENT_TIMEZONE2 + ", " + 955 Calendar.Events.RRULE + " " + 956 "FROM " + Tables.EVENTS + " " + 957 "WHERE " + Calendar.Events.ALL_DAY + "=?"; 958 Cursor cursor = db.rawQuery(sql, new String[] {"1"}); 959 if (cursor != null) { 960 try { 961 String timezone; 962 String timezone2; 963 String duration; 964 Long dtstart; 965 Long dtstart2; 966 Long dtend; 967 Long dtend2; 968 Time time = new Time(); 969 Long id; 970 // some things need to be in utc so we call this frequently, cache to make faster 971 final String utc = Time.TIMEZONE_UTC; 972 while (cursor.moveToNext()) { 973 String rrule = cursor.getString(8); 974 id = cursor.getLong(0); 975 dtstart = cursor.getLong(1); 976 dtstart2 = null; 977 timezone = cursor.getString(6); 978 timezone2 = cursor.getString(7); 979 duration = cursor.getString(3); 980 981 if (TextUtils.isEmpty(rrule)) { 982 // For non-recurring events dtstart and dtend should both have values 983 // and duration should be null. 984 dtend = cursor.getLong(2); 985 dtend2 = null; 986 // Since we made all three of these at the same time if timezone2 exists 987 // so should dtstart2 and dtend2. 988 if(!TextUtils.isEmpty(timezone2)) { 989 dtstart2 = cursor.getLong(4); 990 dtend2 = cursor.getLong(5); 991 } 992 993 boolean update = false; 994 if (!TextUtils.equals(timezone, utc)) { 995 update = true; 996 timezone = utc; 997 } 998 999 time.clear(timezone); 1000 update |= fixAllDayTime(time, timezone, dtstart); 1001 dtstart = time.normalize(false); 1002 1003 time.clear(timezone); 1004 update |= fixAllDayTime(time, timezone, dtend); 1005 dtend = time.normalize(false); 1006 1007 if (dtstart2 != null) { 1008 time.clear(timezone2); 1009 update |= fixAllDayTime(time, timezone2, dtstart2); 1010 dtstart2 = time.normalize(false); 1011 } 1012 1013 if (dtend2 != null) { 1014 time.clear(timezone2); 1015 update |= fixAllDayTime(time, timezone2, dtend2); 1016 dtend2 = time.normalize(false); 1017 } 1018 1019 if (!TextUtils.isEmpty(duration)) { 1020 update = true; 1021 } 1022 1023 if (update) { 1024 // enforce duration being null 1025 db.execSQL("UPDATE " + Tables.EVENTS + " SET " + 1026 Calendar.Events.DTSTART + "=?, " + 1027 Calendar.Events.DTEND + "=?, " + 1028 Calendar.Events.DTSTART2 + "=?, " + 1029 Calendar.Events.DTEND2 + "=?, " + 1030 Calendar.Events.DURATION + "=?, " + 1031 Calendar.Events.EVENT_TIMEZONE + "=?, " + 1032 Calendar.Events.EVENT_TIMEZONE2 + "=? " + 1033 "WHERE " + Calendar.Events._ID + "=?", 1034 new Object[] { 1035 dtstart, 1036 dtend, 1037 dtstart2, 1038 dtend2, 1039 null, 1040 timezone, 1041 timezone2, 1042 id} 1043 ); 1044 } 1045 1046 } else { 1047 // For recurring events only dtstart and duration should be used. 1048 // We ignore dtend since it will be overwritten if the event changes to a 1049 // non-recurring event and won't be used otherwise. 1050 if(!TextUtils.isEmpty(timezone2)) { 1051 dtstart2 = cursor.getLong(4); 1052 } 1053 1054 boolean update = false; 1055 if (!TextUtils.equals(timezone, utc)) { 1056 update = true; 1057 timezone = utc; 1058 } 1059 1060 time.clear(timezone); 1061 update |= fixAllDayTime(time, timezone, dtstart); 1062 dtstart = time.normalize(false); 1063 1064 if (dtstart2 != null) { 1065 time.clear(timezone2); 1066 update |= fixAllDayTime(time, timezone2, dtstart2); 1067 dtstart2 = time.normalize(false); 1068 } 1069 1070 if (TextUtils.isEmpty(duration)) { 1071 // If duration was missing assume a 1 day duration 1072 duration = "P1D"; 1073 update = true; 1074 } else { 1075 int len = duration.length(); 1076 // TODO fix durations in other formats as well 1077 if (duration.charAt(0) == 'P' && 1078 duration.charAt(len - 1) == 'S') { 1079 int seconds = Integer.parseInt(duration.substring(1, len - 1)); 1080 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS; 1081 duration = "P" + days + "D"; 1082 update = true; 1083 } 1084 } 1085 1086 if (update) { 1087 // If there were other problems also enforce dtend being null 1088 db.execSQL("UPDATE " + Tables.EVENTS + " SET " + 1089 Calendar.Events.DTSTART + "=?, " + 1090 Calendar.Events.DTEND + "=?, " + 1091 Calendar.Events.DTSTART2 + "=?, " + 1092 Calendar.Events.DTEND2 + "=?, " + 1093 Calendar.Events.DURATION + "=?," + 1094 Calendar.Events.EVENT_TIMEZONE + "=?, " + 1095 Calendar.Events.EVENT_TIMEZONE2 + "=? " + 1096 "WHERE " + Calendar.Events._ID + "=?", 1097 new Object[] { 1098 dtstart, 1099 null, 1100 dtstart2, 1101 null, 1102 duration, 1103 timezone, 1104 timezone2, 1105 id} 1106 ); 1107 } 1108 } 1109 } 1110 } finally { 1111 cursor.close(); 1112 } 1113 } 1114 } 1115 1116 private void upgradeToVersion66(SQLiteDatabase db) { 1117 // Add a column to indicate whether the event organizer can respond to his own events 1118 // The UI should not show attendee status for events in calendars with this column = 0 1119 db.execSQL("ALTER TABLE " + Tables.CALENDARS + 1120 " ADD COLUMN " + Calendar.Calendars.ORGANIZER_CAN_RESPOND + 1121 " INTEGER NOT NULL DEFAULT 1;"); 1122 } 1123 1124 private void upgradeToVersion65(SQLiteDatabase db) { 1125 // we need to recreate the Events view 1126 createEventsView(db); 1127 } 1128 1129 private void upgradeToVersion64(SQLiteDatabase db) { 1130 // Add a column that may be used by sync adapters 1131 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1132 " ADD COLUMN " + Calendar.Events.SYNC_ADAPTER_DATA + " TEXT;"); 1133 } 1134 1135 private void upgradeToVersion63(SQLiteDatabase db) { 1136 // we need to recreate the Events view 1137 createEventsView(db); 1138 } 1139 1140 private void upgradeToVersion62(SQLiteDatabase db) { 1141 // New columns are to transition to having allDay events in the local timezone 1142 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1143 " ADD COLUMN " + Calendar.Events.DTSTART2 + " INTEGER;"); 1144 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1145 " ADD COLUMN " + Calendar.Events.DTEND2 + " INTEGER;"); 1146 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1147 " ADD COLUMN " + Calendar.Events.EVENT_TIMEZONE2 + " TEXT;"); 1148 1149 String[] allDayBit = new String[] {"0"}; 1150 // Copy over all the data that isn't an all day event. 1151 db.execSQL("UPDATE " + Tables.EVENTS + " SET " + 1152 Calendar.Events.DTSTART2 + "=" + Calendar.Events.DTSTART + "," + 1153 Calendar.Events.DTEND2 + "=" + Calendar.Events.DTEND + "," + 1154 Calendar.Events.EVENT_TIMEZONE2 + "=" + Calendar.Events.EVENT_TIMEZONE + " " + 1155 "WHERE " + Calendar.Events.ALL_DAY + "=?;", 1156 allDayBit /* selection args */); 1157 1158 // "cursor" iterates over all the calendars 1159 allDayBit[0] = "1"; 1160 Cursor cursor = db.rawQuery("SELECT " + Tables.EVENTS + "." + Calendar.Events._ID + "," + 1161 Calendar.Events.DTSTART + "," + 1162 Calendar.Events.DTEND + "," + 1163 Calendar.Events.EVENT_TIMEZONE + "," + 1164 Calendar.Calendars.TIMEZONE + " " + 1165 "FROM " + Tables.EVENTS + " INNER JOIN " + Tables.CALENDARS + " " + 1166 "WHERE " + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID + "=" + 1167 Tables.CALENDARS + "." + Calendar.Calendars._ID + 1168 " AND " 1169 + Calendar.Events.ALL_DAY + "=?", 1170 allDayBit /* selection args */); 1171 1172 Time oldTime = new Time(); 1173 Time newTime = new Time(); 1174 // Update the allday events in the new columns 1175 if (cursor != null) { 1176 try { 1177 String[] newData = new String[4]; 1178 cursor.moveToPosition(-1); 1179 while (cursor.moveToNext()) { 1180 long id = cursor.getLong(0); // Order from query above 1181 long dtstart = cursor.getLong(1); 1182 long dtend = cursor.getLong(2); 1183 String eTz = cursor.getString(3); // current event timezone 1184 String tz = cursor.getString(4); // Calendar timezone 1185 //If there's no timezone for some reason use UTC by default. 1186 if(eTz == null) { 1187 eTz = Time.TIMEZONE_UTC; 1188 } 1189 1190 // Convert start time for all day events into the timezone of their calendar 1191 oldTime.clear(eTz); 1192 oldTime.set(dtstart); 1193 newTime.clear(tz); 1194 newTime.set(oldTime.monthDay, oldTime.month, oldTime.year); 1195 newTime.normalize(false); 1196 dtstart = newTime.toMillis(false /*ignoreDst*/); 1197 1198 // Convert end time for all day events into the timezone of their calendar 1199 oldTime.clear(eTz); 1200 oldTime.set(dtend); 1201 newTime.clear(tz); 1202 newTime.set(oldTime.monthDay, oldTime.month, oldTime.year); 1203 newTime.normalize(false); 1204 dtend = newTime.toMillis(false /*ignoreDst*/); 1205 1206 newData[0] = String.valueOf(dtstart); 1207 newData[1] = String.valueOf(dtend); 1208 newData[2] = tz; 1209 newData[3] = String.valueOf(id); 1210 db.execSQL("UPDATE " + Tables.EVENTS + " SET " + 1211 Calendar.Events.DTSTART2 + "=?, " + 1212 Calendar.Events.DTEND2 + "=?, " + 1213 Calendar.Events.EVENT_TIMEZONE2 + "=? " + 1214 "WHERE " + Calendar.Events._ID + "=?", 1215 newData); 1216 } 1217 } finally { 1218 cursor.close(); 1219 } 1220 } 1221 } 1222 1223 private void upgradeToVersion61(SQLiteDatabase db) { 1224 db.execSQL("DROP TABLE IF EXISTS CalendarCache;"); 1225 1226 // IF NOT EXISTS should be normal pattern for table creation 1227 db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.CALENDAR_CACHE + " (" + 1228 CalendarCache.COLUMN_NAME_ID + " INTEGER PRIMARY KEY," + 1229 CalendarCache.COLUMN_NAME_KEY + " TEXT NOT NULL," + 1230 CalendarCache.COLUMN_NAME_VALUE + " TEXT" + 1231 ");"); 1232 1233 db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE + " (" + 1234 CalendarCache.COLUMN_NAME_KEY + ", " + 1235 CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" + 1236 "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "'," + 1237 "'" + CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION + "'" + 1238 ");"); 1239 } 1240 1241 private void upgradeToVersion60(SQLiteDatabase db) { 1242 // Switch to CalendarProvider2 1243 upgradeSyncState(db); 1244 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 1245 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " + 1246 "BEGIN " + 1247 CALENDAR_CLEANUP_TRIGGER_SQL + 1248 "END"); 1249 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1250 " ADD COLUMN " + Calendar.Events.DELETED + " INTEGER NOT NULL DEFAULT 0;"); 1251 db.execSQL("DROP TRIGGER IF EXISTS events_insert"); 1252 // Trigger to set event's sync_account 1253 db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON " + Tables.EVENTS + " " + 1254 "BEGIN " + 1255 AFTER_EVENT_INSERT_SQL + 1256 "END"); 1257 db.execSQL("DROP TABLE IF EXISTS DeletedEvents;"); 1258 db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete"); 1259 // Trigger to remove data tied to an event when we delete that event. 1260 db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " + 1261 "BEGIN " + 1262 EVENTS_CLEANUP_TRIGGER_SQL + 1263 "END"); 1264 db.execSQL("DROP TRIGGER IF EXISTS attendees_update"); 1265 db.execSQL("DROP TRIGGER IF EXISTS attendees_insert"); 1266 db.execSQL("DROP TRIGGER IF EXISTS attendees_delete"); 1267 db.execSQL("DROP TRIGGER IF EXISTS reminders_update"); 1268 db.execSQL("DROP TRIGGER IF EXISTS reminders_insert"); 1269 db.execSQL("DROP TRIGGER IF EXISTS reminders_delete"); 1270 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update"); 1271 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert"); 1272 db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete"); 1273 1274 createEventsView(db); 1275 } 1276 1277 private void upgradeToVersion59(SQLiteDatabase db) { 1278 db.execSQL("DROP TABLE IF EXISTS BusyBits;"); 1279 db.execSQL("CREATE TEMPORARY TABLE " + Tables.CALENDAR_META_DATA + "_Backup" + "(" + 1280 Calendar.CalendarMetaData._ID + "," + 1281 Calendar.CalendarMetaData.LOCAL_TIMEZONE + "," + 1282 Calendar.CalendarMetaData.MIN_INSTANCE + "," + 1283 Calendar.CalendarMetaData.MAX_INSTANCE + 1284 ");"); 1285 db.execSQL("INSERT INTO " + Tables.CALENDAR_META_DATA + "_Backup " + 1286 "SELECT " + 1287 Calendar.CalendarMetaData._ID + "," + 1288 Calendar.CalendarMetaData.LOCAL_TIMEZONE + "," + 1289 Calendar.CalendarMetaData.MIN_INSTANCE + "," + 1290 Calendar.CalendarMetaData.MAX_INSTANCE + 1291 " FROM " + Tables.CALENDAR_META_DATA + ";"); 1292 db.execSQL("DROP TABLE " + Tables.CALENDAR_META_DATA + ";"); 1293 createCalendarMetaDataTable(db); 1294 db.execSQL("INSERT INTO " + Tables.CALENDAR_META_DATA + " " + 1295 "SELECT " + 1296 Calendar.CalendarMetaData._ID + "," + 1297 Calendar.CalendarMetaData.LOCAL_TIMEZONE + "," + 1298 Calendar.CalendarMetaData.MIN_INSTANCE + "," + 1299 Calendar.CalendarMetaData.MAX_INSTANCE + 1300 " FROM " + Tables.CALENDAR_META_DATA + "_Backup;"); 1301 db.execSQL("DROP TABLE " + Tables.CALENDAR_META_DATA + "_Backup;"); 1302 } 1303 1304 private void upgradeToVersion57(SQLiteDatabase db) { 1305 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1306 " ADD COLUMN " + Calendar.Events.GUESTS_CAN_MODIFY + 1307 " INTEGER NOT NULL DEFAULT 0;"); 1308 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1309 " ADD COLUMN " + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + 1310 " INTEGER NOT NULL DEFAULT 1;"); 1311 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1312 " ADD COLUMN " + Calendar.Events.GUESTS_CAN_SEE_GUESTS + 1313 " INTEGER NOT NULL DEFAULT 1;"); 1314 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1315 " ADD COLUMN " + Calendar.Events.ORGANIZER + 1316 " STRING;"); 1317 db.execSQL("UPDATE " + Tables.EVENTS + " SET " + Calendar.Events.ORGANIZER + "=" + 1318 "(SELECT " + Calendar.Attendees.ATTENDEE_EMAIL + 1319 " FROM " + Tables.ATTENDEES + "" + 1320 " WHERE " + 1321 Tables.ATTENDEES + "." + Calendar.Attendees.EVENT_ID + "=" + 1322 Tables.EVENTS + "." + Calendar.Events._ID + 1323 " AND " + 1324 Tables.ATTENDEES + "." + Calendar.Attendees.ATTENDEE_RELATIONSHIP + "=2);"); 1325 } 1326 1327 private void upgradeToVersion56(SQLiteDatabase db) { 1328 db.execSQL("ALTER TABLE " + Tables.CALENDARS + 1329 " ADD COLUMN " + Calendar.Calendars.OWNER_ACCOUNT + " TEXT;"); 1330 db.execSQL("ALTER TABLE " + Tables.EVENTS + 1331 " ADD COLUMN " + Calendar.Events.HAS_ATTENDEE_DATA + " INTEGER;"); 1332 1333 // Clear _sync_dirty to avoid a client-to-server sync that could blow away 1334 // server attendees. 1335 // Clear _sync_version to pull down the server's event (with attendees) 1336 // Change the URLs from full-selfattendance to full 1337 db.execSQL("UPDATE " + Tables.EVENTS 1338 + " SET " + Calendar.Events._SYNC_DIRTY + "=0, " 1339 + Calendar.Events._SYNC_VERSION + "=NULL, " 1340 + Calendar.Events._SYNC_ID + "=" 1341 + "REPLACE(" + Calendar.Events._SYNC_ID + ", " + 1342 "'/private/full-selfattendance', '/private/full')," 1343 + Calendar.Events.COMMENTS_URI + "=" 1344 + "REPLACE(" + Calendar.Events.COMMENTS_URI + ", " + 1345 "'/private/full-selfattendance', '/private/full');"); 1346 1347 db.execSQL("UPDATE " + Tables.CALENDARS 1348 + " SET " + "url=" 1349 + "REPLACE(" + "url, " + "'/private/full-selfattendance', '/private/full');"); 1350 1351 // "cursor" iterates over all the calendars 1352 Cursor cursor = db.rawQuery("SELECT " + Calendar.Calendars._ID + ", " + 1353 Calendar.Calendars.URL + " FROM " + Tables.CALENDARS, 1354 null /* selection args */); 1355 // Add the owner column. 1356 if (cursor != null) { 1357 try { 1358 final String updateSql = "UPDATE " + Tables.CALENDARS + 1359 " SET " + Calendar.Calendars.OWNER_ACCOUNT + "=?" + 1360 " WHERE " + Calendar.Calendars._ID + "=?"; 1361 while (cursor.moveToNext()) { 1362 Long id = cursor.getLong(0); 1363 String url = cursor.getString(1); 1364 String owner = calendarEmailAddressFromFeedUrl(url); 1365 db.execSQL(updateSql, new Object[] {owner, id}); 1366 } 1367 } finally { 1368 cursor.close(); 1369 } 1370 } 1371 } 1372 1373 private void upgradeResync(SQLiteDatabase db) { 1374 // Delete sync state, so all records will be re-synced. 1375 db.execSQL("DELETE FROM " + Tables.SYNC_STATE + ";"); 1376 1377 // "cursor" iterates over all the calendars 1378 Cursor cursor = db.rawQuery("SELECT " + Calendar.Calendars._SYNC_ACCOUNT + "," + 1379 Calendar.Calendars._SYNC_ACCOUNT_TYPE + ",url FROM " + Tables.CALENDARS, 1380 null /* selection args */); 1381 if (cursor != null) { 1382 try { 1383 while (cursor.moveToNext()) { 1384 String accountName = cursor.getString(0); 1385 String accountType = cursor.getString(1); 1386 final Account account = new Account(accountName, accountType); 1387 String calendarUrl = cursor.getString(2); 1388 scheduleSync(account, false /* two-way sync */, calendarUrl); 1389 } 1390 } finally { 1391 cursor.close(); 1392 } 1393 } 1394 } 1395 1396 private void upgradeToVersion55(SQLiteDatabase db) { 1397 db.execSQL("ALTER TABLE " + Tables.CALENDARS + " ADD COLUMN " + 1398 Calendar.Calendars._SYNC_ACCOUNT_TYPE + " TEXT;"); 1399 db.execSQL("ALTER TABLE " + Tables.EVENTS + " ADD COLUMN " + 1400 Calendar.Events._SYNC_ACCOUNT_TYPE + " TEXT;"); 1401 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;"); 1402 db.execSQL("UPDATE " + Tables.CALENDARS 1403 + " SET " + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "='com.google'" 1404 + " WHERE " + Calendar.Calendars._SYNC_ACCOUNT + " IS NOT NULL"); 1405 db.execSQL("UPDATE " + Tables.EVENTS 1406 + " SET " + Calendar.Events._SYNC_ACCOUNT_TYPE + "='com.google'" 1407 + " WHERE " + Calendar.Events._SYNC_ACCOUNT + " IS NOT NULL"); 1408 db.execSQL("UPDATE DeletedEvents" 1409 + " SET _sync_account_type='com.google'" 1410 + " WHERE _sync_account IS NOT NULL"); 1411 Log.w(TAG, "re-creating eventSyncAccountAndIdIndex"); 1412 db.execSQL("DROP INDEX eventSyncAccountAndIdIndex"); 1413 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON " + Tables.EVENTS + " (" 1414 + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " 1415 + Calendar.Events._SYNC_ACCOUNT + ", " 1416 + Calendar.Events._SYNC_ID + ");"); 1417 } 1418 1419 private void upgradeToVersion54(SQLiteDatabase db) { 1420 Log.w(TAG, "adding eventSyncAccountAndIdIndex"); 1421 db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events (" 1422 + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");"); 1423 } 1424 1425 private void upgradeToVersion53(SQLiteDatabase db) { 1426 Log.w(TAG, "Upgrading CalendarAlerts table"); 1427 db.execSQL("ALTER TABLE " + Tables.CALENDAR_ALERTS + " ADD COLUMN " + 1428 Calendar.CalendarAlerts.CREATION_TIME + " INTEGER DEFAULT 0;"); 1429 db.execSQL("ALTER TABLE " + Tables.CALENDAR_ALERTS + " ADD COLUMN " + 1430 Calendar.CalendarAlerts.RECEIVED_TIME + " INTEGER DEFAULT 0;"); 1431 db.execSQL("ALTER TABLE " + Tables.CALENDAR_ALERTS + " ADD COLUMN " + 1432 Calendar.CalendarAlerts.NOTIFY_TIME + " INTEGER DEFAULT 0;"); 1433 } 1434 1435 private void upgradeToVersion52(SQLiteDatabase db) { 1436 // We added "originalAllDay" to the Events table to keep track of 1437 // the allDay status of the original recurring event for entries 1438 // that are exceptions to that recurring event. We need this so 1439 // that we can format the date correctly for the "originalInstanceTime" 1440 // column when we make a change to the recurrence exception and 1441 // send it to the server. 1442 db.execSQL("ALTER TABLE " + Tables.EVENTS + " ADD COLUMN " + 1443 Calendar.Events.ORIGINAL_ALL_DAY + " INTEGER;"); 1444 1445 // Iterate through the Events table and for each recurrence 1446 // exception, fill in the correct value for "originalAllDay", 1447 // if possible. The only times where this might not be possible 1448 // are (1) the original recurring event no longer exists, or 1449 // (2) the original recurring event does not yet have a _sync_id 1450 // because it was created on the phone and hasn't been synced to the 1451 // server yet. In both cases the originalAllDay field will be set 1452 // to null. In the first case we don't care because the recurrence 1453 // exception will not be displayed and we won't be able to make 1454 // any changes to it (and even if we did, the server should ignore 1455 // them, right?). In the second case, the calendar client already 1456 // disallows making changes to an instance of a recurring event 1457 // until the recurring event has been synced to the server so the 1458 // second case should never occur. 1459 1460 // "cursor" iterates over all the recurrences exceptions. 1461 Cursor cursor = db.rawQuery("SELECT " + Calendar.Events._ID + "," + 1462 Calendar.Events.ORIGINAL_EVENT + 1463 " FROM " + Tables.EVENTS + 1464 " WHERE " + Calendar.Events.ORIGINAL_EVENT + " IS NOT NULL", 1465 null /* selection args */); 1466 if (cursor != null) { 1467 try { 1468 while (cursor.moveToNext()) { 1469 long id = cursor.getLong(0); 1470 String originalEvent = cursor.getString(1); 1471 1472 // Find the original recurring event (if it exists) 1473 Cursor recur = db.rawQuery("SELECT " + Calendar.Events.ALL_DAY + 1474 " FROM " + Tables.EVENTS + 1475 " WHERE " + Calendar.Events._SYNC_ID + "=?", 1476 new String[] {originalEvent}); 1477 if (recur == null) { 1478 continue; 1479 } 1480 1481 try { 1482 // Fill in the "originalAllDay" field of the 1483 // recurrence exception with the "allDay" value 1484 // from the recurring event. 1485 if (recur.moveToNext()) { 1486 int allDay = recur.getInt(0); 1487 db.execSQL("UPDATE " + Tables.EVENTS + 1488 " SET " + Calendar.Events.ORIGINAL_ALL_DAY + "=" + allDay + 1489 " WHERE " + Calendar.Events._ID + "="+id); 1490 } 1491 } finally { 1492 recur.close(); 1493 } 1494 } 1495 } finally { 1496 cursor.close(); 1497 } 1498 } 1499 } 1500 1501 private void upgradeToVersion51(SQLiteDatabase db) { 1502 Log.w(TAG, "Upgrading DeletedEvents table"); 1503 1504 // We don't have enough information to fill in the correct 1505 // value of the calendar_id for old rows in the DeletedEvents 1506 // table, but rows in that table are transient so it is unlikely 1507 // that there are any rows. Plus, the calendar_id is used only 1508 // when deleting a calendar, which is a rare event. All new rows 1509 // will have the correct calendar_id. 1510 db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;"); 1511 1512 // Trigger to remove a calendar's events when we delete the calendar 1513 db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup"); 1514 db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " + 1515 "BEGIN " + 1516 "DELETE FROM " + Tables.EVENTS + " WHERE " + Calendar.Events.CALENDAR_ID + "=" + 1517 "old." + Calendar.Events._ID + ";" + 1518 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" + 1519 "END"); 1520 db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted"); 1521 } 1522 1523 private void dropTables(SQLiteDatabase db) { 1524 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDARS + ";"); 1525 db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS + ";"); 1526 db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS_RAW_TIMES + ";"); 1527 db.execSQL("DROP TABLE IF EXISTS " + Tables.INSTANCES + ";"); 1528 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_META_DATA + ";"); 1529 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";"); 1530 db.execSQL("DROP TABLE IF EXISTS " + Tables.ATTENDEES + ";"); 1531 db.execSQL("DROP TABLE IF EXISTS " + Tables.REMINDERS + ";"); 1532 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_ALERTS + ";"); 1533 db.execSQL("DROP TABLE IF EXISTS " + Tables.EXTENDED_PROPERTIES + ";"); 1534 } 1535 1536 @Override 1537 public synchronized SQLiteDatabase getWritableDatabase() { 1538 SQLiteDatabase db = super.getWritableDatabase(); 1539 return db; 1540 } 1541 1542 public SyncStateContentProviderHelper getSyncState() { 1543 return mSyncState; 1544 } 1545 1546 /** 1547 * Schedule a calendar sync for the account. 1548 * @param account the account for which to schedule a sync 1549 * @param uploadChangesOnly if set, specify that the sync should only send 1550 * up local changes. This is typically used for a local sync, a user override of 1551 * too many deletions, or a sync after a calendar is unselected. 1552 * @param url the url feed for the calendar to sync (may be null, in which case a poll of 1553 * all feeds is done.) 1554 */ 1555 void scheduleSync(Account account, boolean uploadChangesOnly, String url) { 1556 Bundle extras = new Bundle(); 1557 if (uploadChangesOnly) { 1558 extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly); 1559 } 1560 if (url != null) { 1561 extras.putString("feed", url); 1562 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 1563 } 1564 ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras); 1565 } 1566 1567 private static void createEventsView(SQLiteDatabase db) { 1568 db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";"); 1569 String eventsSelect = "SELECT " 1570 + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + "," 1571 + Calendar.Events.HTML_URI + "," 1572 + Calendar.Events.TITLE + "," 1573 + Calendar.Events.DESCRIPTION + "," 1574 + Calendar.Events.EVENT_LOCATION + "," 1575 + Calendar.Events.STATUS + "," 1576 + Calendar.Events.SELF_ATTENDEE_STATUS + "," 1577 + Calendar.Events.COMMENTS_URI + "," 1578 + Calendar.Events.DTSTART + "," 1579 + Calendar.Events.DTEND + "," 1580 + Calendar.Events.DURATION + "," 1581 + Calendar.Events.EVENT_TIMEZONE + "," 1582 + Calendar.Events.ALL_DAY + "," 1583 + Calendar.Events.VISIBILITY + "," 1584 + Calendar.Calendars.TIMEZONE + "," 1585 + Calendar.Calendars.SELECTED + "," 1586 + Calendar.Calendars.ACCESS_LEVEL + "," 1587 + Calendar.Events.TRANSPARENCY + "," 1588 + Calendar.Calendars.COLOR + "," 1589 + Calendar.Events.HAS_ALARM + "," 1590 + Calendar.Events.HAS_EXTENDED_PROPERTIES + "," 1591 + Calendar.Events.RRULE + "," 1592 + Calendar.Events.RDATE + "," 1593 + Calendar.Events.EXRULE + "," 1594 + Calendar.Events.EXDATE + "," 1595 + Calendar.Events.ORIGINAL_EVENT + "," 1596 + Calendar.Events.ORIGINAL_INSTANCE_TIME + "," 1597 + Calendar.Events.ORIGINAL_ALL_DAY + "," 1598 + Calendar.Events.LAST_DATE + "," 1599 + Calendar.Events.HAS_ATTENDEE_DATA + "," 1600 + Calendar.Events.CALENDAR_ID + "," 1601 + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + "," 1602 + Calendar.Events.GUESTS_CAN_MODIFY + "," 1603 + Calendar.Events.GUESTS_CAN_SEE_GUESTS + "," 1604 + Calendar.Events.ORGANIZER + "," 1605 + Tables.EVENTS + "." + Calendar.Events.DELETED 1606 + " AS " + Calendar.Events.DELETED + "," 1607 + Tables.EVENTS + "." + Calendar.Events._SYNC_ID 1608 + " AS " + Calendar.Events._SYNC_ID + "," 1609 + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION 1610 + " AS " + Calendar.Events._SYNC_VERSION + "," 1611 + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY 1612 + " AS " + Calendar.Events._SYNC_DIRTY + "," 1613 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT 1614 + " AS " + Calendar.Events._SYNC_ACCOUNT + "," 1615 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE 1616 + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + "," 1617 + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME 1618 + " AS " + Calendar.Events._SYNC_TIME + "," 1619 + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA 1620 + " AS " + Calendar.Events._SYNC_DATA + "," 1621 + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK 1622 + " AS " + Calendar.Events._SYNC_MARK + "," 1623 + Calendar.Calendars.SYNC1 + "," 1624 + Calendar.Calendars.OWNER_ACCOUNT + "," 1625 + Calendar.Calendars.SYNC_EVENTS 1626 + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS 1627 + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID 1628 + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID 1629 + ")"; 1630 1631 db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect); 1632 } 1633 1634 /** 1635 * Extracts the calendar email from a calendar feed url. 1636 * @param feed the calendar feed url 1637 * @return the calendar email that is in the feed url or null if it can't 1638 * find the email address. 1639 * TODO: this is duplicated in CalendarSyncAdapter; move to a library 1640 */ 1641 public static String calendarEmailAddressFromFeedUrl(String feed) { 1642 // Example feed url: 1643 // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees 1644 String[] pathComponents = feed.split("/"); 1645 if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) { 1646 try { 1647 return URLDecoder.decode(pathComponents[5], "UTF-8"); 1648 } catch (UnsupportedEncodingException e) { 1649 Log.e(TAG, "unable to url decode the email address in calendar " + feed); 1650 return null; 1651 } 1652 } 1653 1654 Log.e(TAG, "unable to find the email address in calendar " + feed); 1655 return null; 1656 } 1657 1658 /** 1659 * Get a "allcalendars" url from a "private/full" or "private/free-busy" url 1660 * @param url 1661 * @return the rewritten Url 1662 * 1663 * For example: 1664 * 1665 * http://www.google.com/calendar/feeds/joe%40joe.com/private/full 1666 * http://www.google.com/calendar/feeds/joe%40joe.com/private/free-busy 1667 * 1668 * will be rewriten into: 1669 * 1670 * http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com 1671 * http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com 1672 */ 1673 @VisibleForTesting 1674 private static String getAllCalendarsUrlFromEventsUrl(String url) { 1675 if (url == null) { 1676 if (Log.isLoggable(TAG, Log.DEBUG)) { 1677 Log.d(TAG, "Cannot get AllCalendars url from a NULL url"); 1678 } 1679 return null; 1680 } 1681 if (url.contains("/private/full")) { 1682 return url.replace("/private/full", ""). 1683 replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full"); 1684 } 1685 if (url.contains("/private/free-busy")) { 1686 return url.replace("/private/free-busy", ""). 1687 replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full"); 1688 } 1689 // Just log as we dont recognize the provided Url 1690 if (Log.isLoggable(TAG, Log.DEBUG)) { 1691 Log.d(TAG, "Cannot get AllCalendars url from the following url: " + url); 1692 } 1693 return null; 1694 } 1695 1696 /** 1697 * Get "selfUrl" from "events url" 1698 * @param url the Events url (either "private/full" or "private/free-busy" 1699 * @return the corresponding allcalendar url 1700 */ 1701 private static String getSelfUrlFromEventsUrl(String url) { 1702 return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url)); 1703 } 1704 1705 /** 1706 * Get "editUrl" from "events url" 1707 * @param url the Events url (either "private/full" or "private/free-busy" 1708 * @return the corresponding allcalendar url 1709 */ 1710 private static String getEditUrlFromEventsUrl(String url) { 1711 return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url)); 1712 } 1713 1714 /** 1715 * Rewrite the url from "http" to "https" scheme 1716 * @param url the url to rewrite 1717 * @return the rewritten URL 1718 */ 1719 private static String rewriteUrlFromHttpToHttps(String url) { 1720 if (url == null) { 1721 if (Log.isLoggable(TAG, Log.DEBUG)) { 1722 Log.d(TAG, "Cannot rewrite a NULL url"); 1723 } 1724 return null; 1725 } 1726 if (url.startsWith(SCHEMA_HTTPS)) { 1727 return url; 1728 } 1729 if (!url.startsWith(SCHEMA_HTTP)) { 1730 throw new IllegalArgumentException("invalid url parameter, unknown scheme: " + url); 1731 } 1732 return SCHEMA_HTTPS + url.substring(SCHEMA_HTTP.length()); 1733 } 1734} 1735