CalendarDatabaseHelper.java revision bcba82631ab0ee16efe58f0e0b0b9c18d93a6fd2
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.android.common.content.SyncStateContentProviderHelper;
20import com.google.common.annotations.VisibleForTesting;
21
22import android.accounts.Account;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.database.Cursor;
27import android.database.DatabaseUtils;
28import android.database.sqlite.SQLiteDatabase;
29import android.database.sqlite.SQLiteException;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.os.Bundle;
32import android.provider.Calendar;
33import android.provider.Calendar.Attendees;
34import android.provider.Calendar.Events;
35import android.provider.Calendar.Reminders;
36import android.provider.ContactsContract;
37import android.provider.SyncStateContract;
38import android.text.TextUtils;
39import android.text.format.Time;
40import android.util.Log;
41
42import java.io.UnsupportedEncodingException;
43import java.net.URLDecoder;
44import java.util.TimeZone;
45
46/**
47 * Database helper for calendar. Designed as a singleton to make sure that all
48 * {@link android.content.ContentProvider} users get the same reference.
49 */
50/* package */ class CalendarDatabaseHelper extends SQLiteOpenHelper {
51
52    private static final String TAG = "CalendarDatabaseHelper";
53
54    private static final boolean LOGD = false;
55
56    private static final String DATABASE_NAME = "calendar.db";
57
58    private static final int DAY_IN_SECONDS = 24 * 60 * 60;
59
60    // Note: if you update the version number, you must also update the code
61    // in upgradeDatabase() to modify the database (gracefully, if possible).
62    // Versions under 100 cover through Froyo, 1xx version are for Gingerbread,
63    // 2xx for Honeycomb, and 3xx for ICS. For future versions bump this to the
64    // next hundred at each major release.
65    static final int DATABASE_VERSION = 304;
66
67    private static final int PRE_FROYO_SYNC_STATE_VERSION = 3;
68
69    // columns used to duplicate an event row
70    private static final String LAST_SYNCED_EVENT_COLUMNS = TextUtils.join(",", new String[] {
71            Events._SYNC_ID,
72            Events.CALENDAR_ID,
73            Events.TITLE,
74            Events.EVENT_LOCATION,
75            Events.DESCRIPTION,
76            Events.STATUS,
77            Events.SELF_ATTENDEE_STATUS,
78            Events.DTSTART,
79            Events.DTEND,
80            Events.EVENT_TIMEZONE,
81            Events.EVENT_END_TIMEZONE,
82            Events.DURATION,
83            Events.ALL_DAY,
84            Events.ACCESS_LEVEL,
85            Events.AVAILABILITY,
86            Events.HAS_ALARM,
87            Events.HAS_EXTENDED_PROPERTIES,
88            Events.RRULE,
89            Events.RDATE,
90            Events.EXRULE,
91            Events.EXDATE,
92            Events.ORIGINAL_SYNC_ID,
93            Events.ORIGINAL_INSTANCE_TIME,
94            Events.ORIGINAL_ALL_DAY,
95            Events.LAST_DATE,
96            Events.HAS_ATTENDEE_DATA,
97            Events.GUESTS_CAN_MODIFY,
98            Events.GUESTS_CAN_INVITE_OTHERS,
99            Events.GUESTS_CAN_SEE_GUESTS,
100            Events.ORGANIZER,
101    });
102
103    // columns used to duplicate a reminder row
104    private static final String LAST_SYNCED_REMINDER_COLUMNS = TextUtils.join(",", new String[] {
105            Calendar.Reminders.MINUTES,
106            Calendar.Reminders.METHOD,
107    });
108
109    // columns used to duplicate an attendee row
110    private static final String LAST_SYNCED_ATTENDEE_COLUMNS = TextUtils.join(",", new String[] {
111            Calendar.Attendees.ATTENDEE_NAME,
112            Calendar.Attendees.ATTENDEE_EMAIL,
113            Calendar.Attendees.ATTENDEE_STATUS,
114            Calendar.Attendees.ATTENDEE_RELATIONSHIP,
115            Calendar.Attendees.ATTENDEE_TYPE,
116    });
117
118    // columns used to duplicate an extended property row
119    private static final String LAST_SYNCED_EXTENDED_PROPERTY_COLUMNS
120            = TextUtils.join(",", new String[] {
121            Calendar.ExtendedProperties.NAME,
122            Calendar.ExtendedProperties.VALUE,
123    });
124
125    public interface Tables {
126        public static final String CALENDARS = "Calendars";
127        public static final String EVENTS = "Events";
128        public static final String EVENTS_RAW_TIMES = "EventsRawTimes";
129        public static final String INSTANCES = "Instances";
130        public static final String ATTENDEES = "Attendees";
131        public static final String REMINDERS = "Reminders";
132        public static final String CALENDAR_ALERTS = "CalendarAlerts";
133        public static final String EXTENDED_PROPERTIES = "ExtendedProperties";
134        public static final String CALENDAR_META_DATA = "CalendarMetaData";
135        public static final String CALENDAR_CACHE = "CalendarCache";
136        public static final String SYNC_STATE = "_sync_state";
137        public static final String SYNC_STATE_META = "_sync_state_metadata";
138    }
139
140    public interface Views {
141        public static final String EVENTS = "view_events";
142    }
143
144    // Copied from SyncStateContentProviderHelper.  Don't really want to make them public there.
145    private static final String SYNC_STATE_META_VERSION_COLUMN = "version";
146
147    // This needs to be done when all the tables are already created
148    private static final String EVENTS_CLEANUP_TRIGGER_SQL =
149            "DELETE FROM " + Tables.INSTANCES +
150                " WHERE "+ Calendar.Instances.EVENT_ID + "=" +
151                    "old." + Calendar.Events._ID + ";" +
152            "DELETE FROM " + Tables.EVENTS_RAW_TIMES +
153                " WHERE " + Calendar.EventsRawTimes.EVENT_ID + "=" +
154                    "old." + Calendar.Events._ID + ";" +
155            "DELETE FROM " + Tables.ATTENDEES +
156                " WHERE " + Calendar.Attendees.EVENT_ID + "=" +
157                    "old." + Calendar.Events._ID + ";" +
158            "DELETE FROM " + Tables.REMINDERS +
159                " WHERE " + Calendar.Reminders.EVENT_ID + "=" +
160                    "old." + Calendar.Events._ID + ";" +
161            "DELETE FROM " + Tables.CALENDAR_ALERTS +
162                " WHERE " + Calendar.CalendarAlerts.EVENT_ID + "=" +
163                    "old." + Calendar.Events._ID + ";" +
164            "DELETE FROM " + Tables.EXTENDED_PROPERTIES +
165                " WHERE " + Calendar.ExtendedProperties.EVENT_ID + "=" +
166                    "old." + Calendar.Events._ID + ";";
167
168    // This ensures any exceptions based on an event get their original_sync_id
169    // column set when an the _sync_id is set.
170    private static final String EVENTS_ORIGINAL_SYNC_TRIGGER_SQL =
171            "UPDATE " + Tables.EVENTS +
172                " SET " + Events.ORIGINAL_SYNC_ID + "=new." + Events._SYNC_ID +
173                " WHERE " + Events.ORIGINAL_ID + "=old." + Events._ID + ";";
174
175    private static final String SYNC_ID_UPDATE_TRIGGER_NAME = "original_sync_update";
176    private static final String CREATE_SYNC_ID_UPDATE_TRIGGER =
177            "CREATE TRIGGER " + SYNC_ID_UPDATE_TRIGGER_NAME + " UPDATE OF " + Events._SYNC_ID +
178            " ON " + Tables.EVENTS +
179            " BEGIN " +
180                EVENTS_ORIGINAL_SYNC_TRIGGER_SQL +
181            " END";
182
183    private static final String CALENDAR_CLEANUP_TRIGGER_SQL = "DELETE FROM " + Tables.EVENTS +
184            " WHERE " + Calendar.Events.CALENDAR_ID + "=" +
185                "old." + Calendar.Events._ID + ";";
186
187    private static final String SCHEMA_HTTPS = "https://";
188    private static final String SCHEMA_HTTP = "http://";
189
190    private final SyncStateContentProviderHelper mSyncState;
191
192    private static CalendarDatabaseHelper sSingleton = null;
193
194    private DatabaseUtils.InsertHelper mCalendarsInserter;
195    private DatabaseUtils.InsertHelper mEventsInserter;
196    private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
197    private DatabaseUtils.InsertHelper mInstancesInserter;
198    private DatabaseUtils.InsertHelper mAttendeesInserter;
199    private DatabaseUtils.InsertHelper mRemindersInserter;
200    private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
201    private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
202
203    public long calendarsInsert(ContentValues values) {
204        return mCalendarsInserter.insert(values);
205    }
206
207    public long eventsInsert(ContentValues values) {
208        return mEventsInserter.insert(values);
209    }
210
211    public long eventsRawTimesInsert(ContentValues values) {
212        return mEventsRawTimesInserter.insert(values);
213    }
214
215    public long eventsRawTimesReplace(ContentValues values) {
216        return mEventsRawTimesInserter.replace(values);
217    }
218
219    public long instancesInsert(ContentValues values) {
220        return mInstancesInserter.insert(values);
221    }
222
223    public long instancesReplace(ContentValues values) {
224        return mInstancesInserter.replace(values);
225    }
226
227    public long attendeesInsert(ContentValues values) {
228        return mAttendeesInserter.insert(values);
229    }
230
231    public long remindersInsert(ContentValues values) {
232        return mRemindersInserter.insert(values);
233    }
234
235    public long calendarAlertsInsert(ContentValues values) {
236        return mCalendarAlertsInserter.insert(values);
237    }
238
239    public long extendedPropertiesInsert(ContentValues values) {
240        return mExtendedPropertiesInserter.insert(values);
241    }
242
243    public static synchronized CalendarDatabaseHelper getInstance(Context context) {
244        if (sSingleton == null) {
245            sSingleton = new CalendarDatabaseHelper(context);
246        }
247        return sSingleton;
248    }
249
250    /**
251     * Private constructor, callers except unit tests should obtain an instance through
252     * {@link #getInstance(android.content.Context)} instead.
253     */
254    /* package */ CalendarDatabaseHelper(Context context) {
255        super(context, DATABASE_NAME, null, DATABASE_VERSION);
256        if (LOGD) Log.d(TAG, "Creating OpenHelper");
257
258        mSyncState = new SyncStateContentProviderHelper();
259    }
260
261    @Override
262    public void onOpen(SQLiteDatabase db) {
263        mSyncState.onDatabaseOpened(db);
264
265        mCalendarsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDARS);
266        mEventsInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS);
267        mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS_RAW_TIMES);
268        mInstancesInserter = new DatabaseUtils.InsertHelper(db, Tables.INSTANCES);
269        mAttendeesInserter = new DatabaseUtils.InsertHelper(db, Tables.ATTENDEES);
270        mRemindersInserter = new DatabaseUtils.InsertHelper(db, Tables.REMINDERS);
271        mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDAR_ALERTS);
272        mExtendedPropertiesInserter =
273                new DatabaseUtils.InsertHelper(db, Tables.EXTENDED_PROPERTIES);
274    }
275
276    /*
277     * Upgrade sync state table if necessary.  Note that the data bundle
278     * in the table is not upgraded.
279     *
280     * The sync state used to be stored with version 3, but now uses the
281     * same sync state code as contacts, which is version 1.  This code
282     * upgrades from 3 to 1 if necessary.  (Yes, the numbers are unfortunately
283     * backwards.)
284     *
285     * This code is only called when upgrading from an old calendar version,
286     * so there is no problem if sync state version 3 gets used again in the
287     * future.
288     */
289    private void upgradeSyncState(SQLiteDatabase db) {
290        long version = DatabaseUtils.longForQuery(db,
291                 "SELECT " + SYNC_STATE_META_VERSION_COLUMN
292                 + " FROM " + Tables.SYNC_STATE_META,
293                 null);
294        if (version == PRE_FROYO_SYNC_STATE_VERSION) {
295            Log.i(TAG, "Upgrading calendar sync state table");
296            db.execSQL("CREATE TEMPORARY TABLE state_backup(_sync_account TEXT, "
297                    + "_sync_account_type TEXT, data TEXT);");
298            db.execSQL("INSERT INTO state_backup SELECT _sync_account, _sync_account_type, data"
299                    + " FROM "
300                    + Tables.SYNC_STATE
301                    + " WHERE _sync_account is not NULL and _sync_account_type is not NULL;");
302            db.execSQL("DROP TABLE " + Tables.SYNC_STATE + ";");
303            mSyncState.onDatabaseOpened(db);
304            db.execSQL("INSERT INTO " + Tables.SYNC_STATE + "("
305                    + SyncStateContract.Columns.ACCOUNT_NAME + ","
306                    + SyncStateContract.Columns.ACCOUNT_TYPE + ","
307                    + SyncStateContract.Columns.DATA
308                    + ") SELECT _sync_account, _sync_account_type, data from state_backup;");
309            db.execSQL("DROP TABLE state_backup;");
310        } else {
311            // Wrong version to upgrade.
312            // Don't need to do anything more here because mSyncState.onDatabaseOpened() will blow
313            // away and recreate  the database (which will result in a resync).
314            Log.w(TAG, "upgradeSyncState: current version is " + version + ", skipping upgrade.");
315        }
316    }
317
318    @Override
319    public void onCreate(SQLiteDatabase db) {
320        bootstrapDB(db);
321    }
322
323    private void bootstrapDB(SQLiteDatabase db) {
324        Log.i(TAG, "Bootstrapping database");
325
326        mSyncState.createDatabase(db);
327
328        createCalendarsTable(db);
329
330        createEventsTable(db);
331
332        db.execSQL("CREATE TABLE " + Tables.EVENTS_RAW_TIMES + " (" +
333                Calendar.EventsRawTimes._ID + " INTEGER PRIMARY KEY," +
334                Calendar.EventsRawTimes.EVENT_ID + " INTEGER NOT NULL," +
335                Calendar.EventsRawTimes.DTSTART_2445 + " TEXT," +
336                Calendar.EventsRawTimes.DTEND_2445 + " TEXT," +
337                Calendar.EventsRawTimes.ORIGINAL_INSTANCE_TIME_2445 + " TEXT," +
338                Calendar.EventsRawTimes.LAST_DATE_2445 + " TEXT," +
339                "UNIQUE (" + Calendar.EventsRawTimes.EVENT_ID + ")" +
340                ");");
341
342        db.execSQL("CREATE TABLE " + Tables.INSTANCES + " (" +
343                Calendar.Instances._ID + " INTEGER PRIMARY KEY," +
344                Calendar.Instances.EVENT_ID + " INTEGER," +
345                Calendar.Instances.BEGIN + " INTEGER," +         // UTC millis
346                Calendar.Instances.END + " INTEGER," +           // UTC millis
347                Calendar.Instances.START_DAY + " INTEGER," +      // Julian start day
348                Calendar.Instances.END_DAY + " INTEGER," +        // Julian end day
349                Calendar.Instances.START_MINUTE + " INTEGER," +   // minutes from midnight
350                Calendar.Instances.END_MINUTE + " INTEGER," +     // minutes from midnight
351                "UNIQUE (" +
352                    Calendar.Instances.EVENT_ID + ", " +
353                    Calendar.Instances.BEGIN + ", " +
354                    Calendar.Instances.END + ")" +
355                ");");
356
357        db.execSQL("CREATE INDEX instancesStartDayIndex ON " + Tables.INSTANCES + " (" +
358                Calendar.Instances.START_DAY +
359                ");");
360
361        createCalendarMetaDataTable(db);
362
363        createCalendarCacheTable(db, null);
364
365        db.execSQL("CREATE TABLE " + Tables.ATTENDEES + " (" +
366                Calendar.Attendees._ID + " INTEGER PRIMARY KEY," +
367                Calendar.Attendees.EVENT_ID + " INTEGER," +
368                Calendar.Attendees.ATTENDEE_NAME + " TEXT," +
369                Calendar.Attendees.ATTENDEE_EMAIL + " TEXT," +
370                Calendar.Attendees.ATTENDEE_STATUS + " INTEGER," +
371                Calendar.Attendees.ATTENDEE_RELATIONSHIP + " INTEGER," +
372                Calendar.Attendees.ATTENDEE_TYPE + " INTEGER" +
373                ");");
374
375        db.execSQL("CREATE INDEX attendeesEventIdIndex ON " + Tables.ATTENDEES + " (" +
376                Calendar.Attendees.EVENT_ID +
377                ");");
378
379        db.execSQL("CREATE TABLE " + Tables.REMINDERS + " (" +
380                Calendar.Reminders._ID + " INTEGER PRIMARY KEY," +
381                Calendar.Reminders.EVENT_ID + " INTEGER," +
382                Calendar.Reminders.MINUTES + " INTEGER," +
383                Calendar.Reminders.METHOD + " INTEGER NOT NULL" +
384                " DEFAULT " + Calendar.Reminders.METHOD_DEFAULT +
385                ");");
386
387        db.execSQL("CREATE INDEX remindersEventIdIndex ON " + Tables.REMINDERS + " (" +
388                Calendar.Reminders.EVENT_ID +
389                ");");
390
391         // This table stores the Calendar notifications that have gone off.
392        db.execSQL("CREATE TABLE " + Tables.CALENDAR_ALERTS + " (" +
393                Calendar.CalendarAlerts._ID + " INTEGER PRIMARY KEY," +
394                Calendar.CalendarAlerts.EVENT_ID + " INTEGER," +
395                Calendar.CalendarAlerts.BEGIN + " INTEGER NOT NULL," +          // UTC millis
396                Calendar.CalendarAlerts.END + " INTEGER NOT NULL," +            // UTC millis
397                Calendar.CalendarAlerts.ALARM_TIME + " INTEGER NOT NULL," +     // UTC millis
398                // UTC millis
399                Calendar.CalendarAlerts.CREATION_TIME + " INTEGER NOT NULL DEFAULT 0," +
400                // UTC millis
401                Calendar.CalendarAlerts.RECEIVED_TIME + " INTEGER NOT NULL DEFAULT 0," +
402                // UTC millis
403                Calendar.CalendarAlerts.NOTIFY_TIME + " INTEGER NOT NULL DEFAULT 0," +
404                Calendar.CalendarAlerts.STATE + " INTEGER NOT NULL," +
405                Calendar.CalendarAlerts.MINUTES + " INTEGER," +
406                "UNIQUE (" +
407                    Calendar.CalendarAlerts.ALARM_TIME + ", " +
408                    Calendar.CalendarAlerts.BEGIN + ", " +
409                    Calendar.CalendarAlerts.EVENT_ID + ")" +
410                ");");
411
412        db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON " + Tables.CALENDAR_ALERTS + " (" +
413                Calendar.CalendarAlerts.EVENT_ID +
414                ");");
415
416        db.execSQL("CREATE TABLE " + Tables.EXTENDED_PROPERTIES + " (" +
417                Calendar.ExtendedProperties._ID + " INTEGER PRIMARY KEY," +
418                Calendar.ExtendedProperties.EVENT_ID + " INTEGER," +
419                Calendar.ExtendedProperties.NAME + " TEXT," +
420                Calendar.ExtendedProperties.VALUE + " TEXT" +
421                ");");
422
423        db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON " + Tables.EXTENDED_PROPERTIES
424                + " (" +
425                Calendar.ExtendedProperties.EVENT_ID +
426                ");");
427
428        createEventsView(db);
429
430        // Trigger to remove data tied to an event when we delete that event.
431        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
432                "BEGIN " +
433                EVENTS_CLEANUP_TRIGGER_SQL +
434                "END");
435
436        // Trigger to update exceptions when an original event updates its
437        // _sync_id
438        db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
439
440        ContentResolver.requestSync(null /* all accounts */,
441                ContactsContract.AUTHORITY, new Bundle());
442    }
443
444    private void createEventsTable(SQLiteDatabase db) {
445        // IMPORTANT: when adding new columns, be sure to update ALLOWED_IN_EXCEPTION and
446        // DONT_CLONE_INTO_EXCEPTION in CalendarProvider2.
447        //
448        // TODO: do we need both dtend and duration?
449        db.execSQL("CREATE TABLE " + Tables.EVENTS + " (" +
450                Calendar.Events._ID + " INTEGER PRIMARY KEY," +
451                Calendar.Events._SYNC_ID + " TEXT," +
452                Calendar.Events._SYNC_VERSION + " TEXT," +
453                // sync time in UTC
454                Calendar.Events._SYNC_TIME + " TEXT,"  +
455                Calendar.Events._SYNC_DATA + " INTEGER," +
456                Calendar.Events.SYNC_DATA7 + " TEXT," +
457                Calendar.Events.DIRTY + " INTEGER," +
458                Calendar.Events.LAST_SYNCED + " INTEGER DEFAULT 0," +
459                // sync mark to filter out new rows
460                Calendar.Events._SYNC_MARK + " INTEGER," +
461                Calendar.Events.CALENDAR_ID + " INTEGER NOT NULL," +
462                Calendar.Events.HTML_URI + " TEXT," +
463                Calendar.Events.TITLE + " TEXT," +
464                Calendar.Events.EVENT_LOCATION + " TEXT," +
465                Calendar.Events.DESCRIPTION + " TEXT," +
466                Calendar.Events.STATUS + " INTEGER," +
467                Calendar.Events.SELF_ATTENDEE_STATUS + " INTEGER NOT NULL DEFAULT 0," +
468                Calendar.Events.COMMENTS_URI + " TEXT," +
469                // dtstart in millis since epoch
470                Calendar.Events.DTSTART + " INTEGER," +
471                // dtend in millis since epoch
472                Calendar.Events.DTEND + " INTEGER," +
473                // timezone for event
474                Calendar.Events.EVENT_TIMEZONE + " TEXT," +
475                Calendar.Events.DURATION + " TEXT," +
476                Calendar.Events.ALL_DAY + " INTEGER NOT NULL DEFAULT 0," +
477                Calendar.Events.ACCESS_LEVEL + " INTEGER NOT NULL DEFAULT 0," +
478                Calendar.Events.AVAILABILITY + " INTEGER NOT NULL DEFAULT 0," +
479                Calendar.Events.HAS_ALARM + " INTEGER NOT NULL DEFAULT 0," +
480                Calendar.Events.HAS_EXTENDED_PROPERTIES + " INTEGER NOT NULL DEFAULT 0," +
481                Calendar.Events.RRULE + " TEXT," +
482                Calendar.Events.RDATE + " TEXT," +
483                Calendar.Events.EXRULE + " TEXT," +
484                Calendar.Events.EXDATE + " TEXT," +
485                Calendar.Events.ORIGINAL_ID + " INTEGER," +
486                // ORIGINAL_SYNC_ID is the _sync_id of recurring event
487                Calendar.Events.ORIGINAL_SYNC_ID + " TEXT," +
488                // originalInstanceTime is in millis since epoch
489                Calendar.Events.ORIGINAL_INSTANCE_TIME + " INTEGER," +
490                Calendar.Events.ORIGINAL_ALL_DAY + " INTEGER," +
491                // lastDate is in millis since epoch
492                Calendar.Events.LAST_DATE + " INTEGER," +
493                Calendar.Events.HAS_ATTENDEE_DATA + " INTEGER NOT NULL DEFAULT 0," +
494                Calendar.Events.GUESTS_CAN_MODIFY + " INTEGER NOT NULL DEFAULT 0," +
495                Calendar.Events.GUESTS_CAN_INVITE_OTHERS + " INTEGER NOT NULL DEFAULT 1," +
496                Calendar.Events.GUESTS_CAN_SEE_GUESTS + " INTEGER NOT NULL DEFAULT 1," +
497                Calendar.Events.ORGANIZER + " STRING," +
498                Calendar.Events.DELETED + " INTEGER NOT NULL DEFAULT 0," +
499                // timezone for event with allDay events are in local timezone
500                Calendar.Events.EVENT_END_TIMEZONE + " TEXT," +
501                // SYNC_DATA1 is available for use by sync adapters
502                Calendar.Events.SYNC_DATA1 + " TEXT" + ");");
503
504        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON " + Tables.EVENTS + " ("
505                + Calendar.Events.CALENDAR_ID + ");");
506    }
507
508    // TODO Remove this method after merging all ICS upgrades
509    private void createEventsTable300(SQLiteDatabase db) {
510        db.execSQL("CREATE TABLE " + Tables.EVENTS + " (" +
511                Calendar.Events._ID + " INTEGER PRIMARY KEY," +
512                Calendar.Events._SYNC_ID + " TEXT," +
513                Calendar.Events._SYNC_VERSION + " TEXT," +
514                // sync time in UTC
515                Calendar.Events._SYNC_TIME + " TEXT,"  +
516                Calendar.Events._SYNC_DATA + " INTEGER," +
517                Calendar.Events.DIRTY + " INTEGER," +
518                // sync mark to filter out new rows
519                Calendar.Events._SYNC_MARK + " INTEGER," +
520                Calendar.Events.CALENDAR_ID + " INTEGER NOT NULL," +
521                Calendar.Events.HTML_URI + " TEXT," +
522                Calendar.Events.TITLE + " TEXT," +
523                Calendar.Events.EVENT_LOCATION + " TEXT," +
524                Calendar.Events.DESCRIPTION + " TEXT," +
525                Calendar.Events.STATUS + " INTEGER," +
526                Calendar.Events.SELF_ATTENDEE_STATUS + " INTEGER NOT NULL DEFAULT 0," +
527                Calendar.Events.COMMENTS_URI + " TEXT," +
528                // dtstart in millis since epoch
529                Calendar.Events.DTSTART + " INTEGER," +
530                // dtend in millis since epoch
531                Calendar.Events.DTEND + " INTEGER," +
532                // timezone for event
533                Calendar.Events.EVENT_TIMEZONE + " TEXT," +
534                Calendar.Events.DURATION + " TEXT," +
535                Calendar.Events.ALL_DAY + " INTEGER NOT NULL DEFAULT 0," +
536                Calendar.Events.ACCESS_LEVEL + " INTEGER NOT NULL DEFAULT 0," +
537                Calendar.Events.AVAILABILITY + " INTEGER NOT NULL DEFAULT 0," +
538                Calendar.Events.HAS_ALARM + " INTEGER NOT NULL DEFAULT 0," +
539                Calendar.Events.HAS_EXTENDED_PROPERTIES + " INTEGER NOT NULL DEFAULT 0," +
540                Calendar.Events.RRULE + " TEXT," +
541                Calendar.Events.RDATE + " TEXT," +
542                Calendar.Events.EXRULE + " TEXT," +
543                Calendar.Events.EXDATE + " TEXT," +
544                // originalEvent is the _sync_id of recurring event
545                Calendar.Events.ORIGINAL_SYNC_ID + " TEXT," +
546                // originalInstanceTime is in millis since epoch
547                Calendar.Events.ORIGINAL_INSTANCE_TIME + " INTEGER," +
548                Calendar.Events.ORIGINAL_ALL_DAY + " INTEGER," +
549                // lastDate is in millis since epoch
550                Calendar.Events.LAST_DATE + " INTEGER," +
551                Calendar.Events.HAS_ATTENDEE_DATA + " INTEGER NOT NULL DEFAULT 0," +
552                Calendar.Events.GUESTS_CAN_MODIFY + " INTEGER NOT NULL DEFAULT 0," +
553                Calendar.Events.GUESTS_CAN_INVITE_OTHERS + " INTEGER NOT NULL DEFAULT 1," +
554                Calendar.Events.GUESTS_CAN_SEE_GUESTS + " INTEGER NOT NULL DEFAULT 1," +
555                Calendar.Events.ORGANIZER + " STRING," +
556                Calendar.Events.DELETED + " INTEGER NOT NULL DEFAULT 0," +
557                // timezone for event with allDay events are in local timezone
558                Calendar.Events.EVENT_END_TIMEZONE + " TEXT," +
559                // syncAdapterData is available for use by sync adapters
560                Calendar.Events.SYNC_DATA1 + " TEXT" + ");");
561
562        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON " + Tables.EVENTS + " ("
563                + Calendar.Events.CALENDAR_ID + ");");
564    }
565
566    private void createCalendarsTable303(SQLiteDatabase db) {
567        db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
568                "_id INTEGER PRIMARY KEY," +
569                "account_name TEXT," +
570                "account_type TEXT," +
571                "_sync_id TEXT," +
572                "_sync_version TEXT," +
573                "_sync_time TEXT," +  // UTC
574                "dirty INTEGER," +
575                "name TEXT," +
576                "displayName TEXT," +
577                "calendar_color INTEGER," +
578                "access_level INTEGER," +
579                "visible INTEGER NOT NULL DEFAULT 1," +
580                "sync_events INTEGER NOT NULL DEFAULT 0," +
581                "calendar_location TEXT," +
582                "calendar_timezone TEXT," +
583                "ownerAccount TEXT, " +
584                "canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
585                "canModifyTimeZone INTEGER DEFAULT 1," +
586                "maxReminders INTEGER DEFAULT 5," +
587                "allowedReminders TEXT DEFAULT '0,1,2,3'," +
588                "deleted INTEGER NOT NULL DEFAULT 0," +
589                "cal_sync1 TEXT," +
590                "cal_sync2 TEXT," +
591                "cal_sync3 TEXT," +
592                "cal_sync4 TEXT," +
593                "cal_sync5 TEXT," +
594                "cal_sync6 TEXT" +
595                ");");
596
597        // Trigger to remove a calendar's events when we delete the calendar
598        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
599                "BEGIN " +
600                CALENDAR_CLEANUP_TRIGGER_SQL +
601                "END");
602    }
603
604    private void createCalendarsTable(SQLiteDatabase db) {
605        db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
606                Calendar.Calendars._ID + " INTEGER PRIMARY KEY," +
607                Calendar.Calendars.ACCOUNT_NAME + " TEXT," +
608                Calendar.Calendars.ACCOUNT_TYPE + " TEXT," +
609                Calendar.Calendars._SYNC_ID + " TEXT," +
610                Calendar.Calendars._SYNC_VERSION + " TEXT," +
611                Calendar.Calendars._SYNC_TIME + " TEXT," +  // UTC
612                Calendar.Calendars.DIRTY + " INTEGER," +
613                Calendar.Calendars.NAME + " TEXT," +
614                Calendar.Calendars.DISPLAY_NAME + " TEXT," +
615                Calendar.Calendars.CALENDAR_COLOR + " INTEGER," +
616                Calendar.Calendars.ACCESS_LEVEL + " INTEGER," +
617                Calendar.Calendars.VISIBLE + " INTEGER NOT NULL DEFAULT 1," +
618                Calendar.Calendars.SYNC_EVENTS + " INTEGER NOT NULL DEFAULT 0," +
619                Calendar.Calendars.CALENDAR_LOCATION + " TEXT," +
620                Calendar.Calendars.CALENDAR_TIMEZONE + " TEXT," +
621                Calendar.Calendars.OWNER_ACCOUNT + " TEXT, " +
622                Calendar.Calendars.CAN_ORGANIZER_RESPOND + " INTEGER NOT NULL DEFAULT 1," +
623                Calendar.Calendars.CAN_MODIFY_TIME_ZONE + " INTEGER DEFAULT 1," +
624                Calendar.Calendars.CAN_PARTIALLY_UPDATE + " INTEGER DEFAULT 0," +
625                Calendar.Calendars.MAX_REMINDERS + " INTEGER DEFAULT 5," +
626                Calendar.Calendars.ALLOWED_REMINDERS + " TEXT DEFAULT '0,1,2,3'," +
627                Calendar.Calendars.DELETED + " INTEGER NOT NULL DEFAULT 0," +
628                Calendar.Calendars.CAL_SYNC1 + " TEXT," +
629                Calendar.Calendars.CAL_SYNC2 + " TEXT," +
630                Calendar.Calendars.CAL_SYNC3 + " TEXT," +
631                Calendar.Calendars.CAL_SYNC4 + " TEXT," +
632                Calendar.Calendars.CAL_SYNC5 + " TEXT," +
633                Calendar.Calendars.CAL_SYNC6 + " TEXT" +
634                ");");
635
636        // Trigger to remove a calendar's events when we delete the calendar
637        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
638                "BEGIN " +
639                CALENDAR_CLEANUP_TRIGGER_SQL +
640                "END");
641    }
642
643    private void createCalendarsTable300(SQLiteDatabase db) {
644        db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
645                "_id INTEGER PRIMARY KEY," +
646                "account_name TEXT," +
647                "account_type TEXT," +
648                "_sync_id TEXT," +
649                "_sync_version TEXT," +
650                "_sync_time TEXT," +  // UTC
651                "dirty INTEGER," +
652                "name TEXT," +
653                "displayName TEXT," +
654                "calendar_color INTEGER," +
655                "access_level INTEGER," +
656                "visible INTEGER NOT NULL DEFAULT 1," +
657                "sync_events INTEGER NOT NULL DEFAULT 0," +
658                "calendar_location TEXT," +
659                "calendar_timezone TEXT," +
660                "ownerAccount TEXT, " +
661                "canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
662                "canModifyTimeZone INTEGER DEFAULT 1," +
663                "maxReminders INTEGER DEFAULT 5," +
664                "allowedReminders TEXT DEFAULT '0,1,2'," +
665                "deleted INTEGER NOT NULL DEFAULT 0," +
666                "sync1 TEXT," +
667                "sync2 TEXT," +
668                "sync3 TEXT," +
669                "sync4 TEXT," +
670                "sync5 TEXT," +
671                "sync6 TEXT" +
672                ");");
673
674        // Trigger to remove a calendar's events when we delete the calendar
675        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
676                "BEGIN " +
677                CALENDAR_CLEANUP_TRIGGER_SQL +
678                "END");
679    }
680
681    private void createCalendarsTable205(SQLiteDatabase db) {
682        db.execSQL("CREATE TABLE Calendars (" +
683                "_id INTEGER PRIMARY KEY," +
684                "_sync_account TEXT," +
685                "_sync_account_type TEXT," +
686                "_sync_id TEXT," +
687                "_sync_version TEXT," +
688                "_sync_time TEXT," +  // UTC
689                "_sync_dirty INTEGER," +
690                "name TEXT," +
691                "displayName TEXT," +
692                "color INTEGER," +
693                "access_level INTEGER," +
694                "visible INTEGER NOT NULL DEFAULT 1," +
695                "sync_events INTEGER NOT NULL DEFAULT 0," +
696                "location TEXT," +
697                "timezone TEXT," +
698                "ownerAccount TEXT, " +
699                "canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
700                "canModifyTimeZone INTEGER DEFAULT 1, " +
701                "maxReminders INTEGER DEFAULT 5," +
702                "deleted INTEGER NOT NULL DEFAULT 0," +
703                "sync1 TEXT," +
704                "sync2 TEXT," +
705                "sync3 TEXT," +
706                "sync4 TEXT," +
707                "sync5 TEXT," +
708                "sync6 TEXT" +
709                ");");
710
711        createCalendarsCleanup200(db);
712    }
713
714    private void createCalendarsTable202(SQLiteDatabase db) {
715        db.execSQL("CREATE TABLE Calendars (" +
716                "_id INTEGER PRIMARY KEY," +
717                "_sync_account TEXT," +
718                "_sync_account_type TEXT," +
719                "_sync_id TEXT," +
720                "_sync_version TEXT," +
721                "_sync_time TEXT," +  // UTC
722                "_sync_local_id INTEGER," +
723                "_sync_dirty INTEGER," +
724                "_sync_mark INTEGER," + // Used to filter out new rows
725                "name TEXT," +
726                "displayName TEXT," +
727                "color INTEGER," +
728                "access_level INTEGER," +
729                "selected INTEGER NOT NULL DEFAULT 1," +
730                "sync_events INTEGER NOT NULL DEFAULT 0," +
731                "location TEXT," +
732                "timezone TEXT," +
733                "ownerAccount TEXT, " +
734                "organizerCanRespond INTEGER NOT NULL DEFAULT 1," +
735                "deleted INTEGER NOT NULL DEFAULT 0," +
736                "sync1 TEXT," +
737                "sync2 TEXT," +
738                "sync3 TEXT," +
739                "sync4 TEXT," +
740                "sync5 TEXT" +
741                ");");
742
743        createCalendarsCleanup200(db);
744    }
745
746    private void createCalendarsTable200(SQLiteDatabase db) {
747        db.execSQL("CREATE TABLE Calendars (" +
748                "_id INTEGER PRIMARY KEY," +
749                "_sync_account TEXT," +
750                "_sync_account_type TEXT," +
751                "_sync_id TEXT," +
752                "_sync_version TEXT," +
753                "_sync_time TEXT," +  // UTC
754                "_sync_local_id INTEGER," +
755                "_sync_dirty INTEGER," +
756                "_sync_mark INTEGER," + // Used to filter out new rows
757                "name TEXT," +
758                "displayName TEXT," +
759                "hidden INTEGER NOT NULL DEFAULT 0," +
760                "color INTEGER," +
761                "access_level INTEGER," +
762                "selected INTEGER NOT NULL DEFAULT 1," +
763                "sync_events INTEGER NOT NULL DEFAULT 0," +
764                "location TEXT," +
765                "timezone TEXT," +
766                "ownerAccount TEXT, " +
767                "organizerCanRespond INTEGER NOT NULL DEFAULT 1," +
768                "deleted INTEGER NOT NULL DEFAULT 0," +
769                "sync1 TEXT," +
770                "sync2 TEXT," +
771                "sync3 TEXT" +
772                ");");
773
774        createCalendarsCleanup200(db);
775    }
776
777    /** Trigger to remove a calendar's events when we delete the calendar */
778    private void createCalendarsCleanup200(SQLiteDatabase db) {
779        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
780                "BEGIN " +
781                "DELETE FROM Events WHERE calendar_id=old._id;" +
782                "END");
783    }
784
785    private void createCalendarMetaDataTable(SQLiteDatabase db) {
786        db.execSQL("CREATE TABLE " + Tables.CALENDAR_META_DATA + " (" +
787                Calendar.CalendarMetaData._ID + " INTEGER PRIMARY KEY," +
788                Calendar.CalendarMetaData.LOCAL_TIMEZONE + " TEXT," +
789                Calendar.CalendarMetaData.MIN_INSTANCE + " INTEGER," +      // UTC millis
790                Calendar.CalendarMetaData.MAX_INSTANCE + " INTEGER" +       // UTC millis
791                ");");
792    }
793
794    private void createCalendarMetaDataTable59(SQLiteDatabase db) {
795        db.execSQL("CREATE TABLE CalendarMetaData (" +
796                "_id INTEGER PRIMARY KEY," +
797                "localTimezone TEXT," +
798                "minInstance INTEGER," +      // UTC millis
799                "maxInstance INTEGER" +       // UTC millis
800                ");");
801    }
802
803    private void createCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) {
804        // This is a hack because versioning skipped version number 61 of schema
805        // TODO after version 70 this can be removed
806        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";");
807
808        // IF NOT EXISTS should be normal pattern for table creation
809        db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.CALENDAR_CACHE + " (" +
810                CalendarCache.COLUMN_NAME_ID + " INTEGER PRIMARY KEY," +
811                CalendarCache.COLUMN_NAME_KEY + " TEXT NOT NULL," +
812                CalendarCache.COLUMN_NAME_VALUE + " TEXT" +
813                ");");
814
815        initCalendarCacheTable(db, oldTimezoneDbVersion);
816        updateCalendarCacheTable(db);
817    }
818
819    private void initCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) {
820        String timezoneDbVersion = (oldTimezoneDbVersion != null) ?
821                oldTimezoneDbVersion : CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION;
822
823        // Set the default timezone database version
824        db.execSQL("INSERT OR REPLACE INTO " + Tables.CALENDAR_CACHE +
825                " (" + CalendarCache.COLUMN_NAME_ID + ", " +
826                CalendarCache.COLUMN_NAME_KEY + ", " +
827                CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
828                CalendarCache.KEY_TIMEZONE_DATABASE_VERSION.hashCode() + "," +
829                "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "',"  +
830                "'" + timezoneDbVersion + "'" +
831                ");");
832    }
833
834    private void updateCalendarCacheTable(SQLiteDatabase db) {
835        // Define the default timezone type for Instances timezone management
836        db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE +
837                " (" + CalendarCache.COLUMN_NAME_ID + ", " +
838                CalendarCache.COLUMN_NAME_KEY + ", " +
839                CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
840                CalendarCache.KEY_TIMEZONE_TYPE.hashCode() + "," +
841                "'" + CalendarCache.KEY_TIMEZONE_TYPE + "',"  +
842                "'" + CalendarCache.TIMEZONE_TYPE_AUTO + "'" +
843                ");");
844
845        String defaultTimezone = TimeZone.getDefault().getID();
846
847        // Define the default timezone for Instances
848        db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE +
849                " (" + CalendarCache.COLUMN_NAME_ID + ", " +
850                CalendarCache.COLUMN_NAME_KEY + ", " +
851                CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
852                CalendarCache.KEY_TIMEZONE_INSTANCES.hashCode() + "," +
853                "'" + CalendarCache.KEY_TIMEZONE_INSTANCES + "',"  +
854                "'" + defaultTimezone + "'" +
855                ");");
856
857        // Define the default previous timezone for Instances
858        db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE +
859                " (" + CalendarCache.COLUMN_NAME_ID + ", " +
860                CalendarCache.COLUMN_NAME_KEY + ", " +
861                CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
862                CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS.hashCode() + "," +
863                "'" + CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + "',"  +
864                "'" + defaultTimezone + "'" +
865                ");");
866    }
867
868    private void initCalendarCacheTable203(SQLiteDatabase db, String oldTimezoneDbVersion) {
869        String timezoneDbVersion = (oldTimezoneDbVersion != null) ?
870                oldTimezoneDbVersion : "2009s";
871
872        // Set the default timezone database version
873        db.execSQL("INSERT OR REPLACE INTO CalendarCache" +
874                " (_id, " +
875                "key, " +
876                "value) VALUES (" +
877                "timezoneDatabaseVersion".hashCode() + "," +
878                "'timezoneDatabaseVersion',"  +
879                "'" + timezoneDbVersion + "'" +
880                ");");
881    }
882
883    private void updateCalendarCacheTableTo203(SQLiteDatabase db) {
884        // Define the default timezone type for Instances timezone management
885        db.execSQL("INSERT INTO CalendarCache" +
886                " (_id, key, value) VALUES (" +
887                "timezoneType".hashCode() + "," +
888                "'timezoneType',"  +
889                "'auto'" +
890                ");");
891
892        String defaultTimezone = TimeZone.getDefault().getID();
893
894        // Define the default timezone for Instances
895        db.execSQL("INSERT INTO CalendarCache" +
896                " (_id, key, value) VALUES (" +
897                "timezoneInstances".hashCode() + "," +
898                "'timezoneInstances',"  +
899                "'" + defaultTimezone + "'" +
900                ");");
901
902        // Define the default previous timezone for Instances
903        db.execSQL("INSERT INTO CalendarCache" +
904                " (_id, key, value) VALUES (" +
905                "timezoneInstancesPrevious".hashCode() + "," +
906                "'timezoneInstancesPrevious',"  +
907                "'" + defaultTimezone + "'" +
908                ");");
909    }
910
911    @Override
912    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
913        Log.i(TAG, "Upgrading DB from version " + oldVersion
914                + " to " + newVersion);
915        if (oldVersion < 49) {
916            dropTables(db);
917            mSyncState.createDatabase(db);
918            return; // this was lossy
919        }
920
921        // From schema versions 59 to version 66, the CalendarMetaData table definition had lost
922        // the primary key leading to having the CalendarMetaData with multiple rows instead of
923        // only one. The Instance table was then corrupted (during Instance expansion we are using
924        // the localTimezone, minInstance and maxInstance from CalendarMetaData table.
925        // This boolean helps us tracking the need to recreate the CalendarMetaData table and
926        // clear the Instance table (and thus force an Instance expansion).
927        boolean recreateMetaDataAndInstances = (oldVersion >= 59 && oldVersion <= 66);
928        boolean createEventsView = false;
929
930        try {
931            if (oldVersion < 51) {
932                upgradeToVersion51(db); // From 50 or 51
933                oldVersion = 51;
934            }
935            if (oldVersion == 51) {
936                upgradeToVersion52(db);
937                oldVersion += 1;
938            }
939            if (oldVersion == 52) {
940                upgradeToVersion53(db);
941                oldVersion += 1;
942            }
943            if (oldVersion == 53) {
944                upgradeToVersion54(db);
945                oldVersion += 1;
946            }
947            if (oldVersion == 54) {
948                upgradeToVersion55(db);
949                oldVersion += 1;
950            }
951            if (oldVersion == 55 || oldVersion == 56) {
952                // Both require resync, so just schedule it once
953                upgradeResync(db);
954            }
955            if (oldVersion == 55) {
956                upgradeToVersion56(db);
957                oldVersion += 1;
958            }
959            if (oldVersion == 56) {
960                upgradeToVersion57(db);
961                oldVersion += 1;
962            }
963            if (oldVersion == 57) {
964                // Changes are undone upgrading to 60, so don't do anything.
965                oldVersion += 1;
966            }
967            if (oldVersion == 58) {
968                upgradeToVersion59(db);
969                oldVersion += 1;
970            }
971            if (oldVersion == 59) {
972                upgradeToVersion60(db);
973                createEventsView = true;
974                oldVersion += 1;
975            }
976            if (oldVersion == 60) {
977                upgradeToVersion61(db);
978                oldVersion += 1;
979            }
980            if (oldVersion == 61) {
981                upgradeToVersion62(db);
982                oldVersion += 1;
983            }
984            if (oldVersion == 62) {
985                createEventsView = true;
986                oldVersion += 1;
987            }
988            if (oldVersion == 63) {
989                upgradeToVersion64(db);
990                oldVersion += 1;
991            }
992            if (oldVersion == 64) {
993                createEventsView = true;
994                oldVersion += 1;
995            }
996            if (oldVersion == 65) {
997                upgradeToVersion66(db);
998                oldVersion += 1;
999            }
1000            if (oldVersion == 66) {
1001                // Changes are done thru recreateMetaDataAndInstances() method
1002                oldVersion += 1;
1003            }
1004            if (recreateMetaDataAndInstances) {
1005                recreateMetaDataAndInstances67(db);
1006            }
1007            if (oldVersion == 67 || oldVersion == 68) {
1008                upgradeToVersion69(db);
1009                oldVersion = 69;
1010            }
1011            // 69. 70 are for Froyo/old Gingerbread only and 100s are for Gingerbread only
1012            // 70 and 71 have been for Honeycomb but no more used
1013            // 72 and 73 and 74 were for Honeycomb only but are considered as obsolete for enabling
1014            // room for Froyo version numbers
1015            if(oldVersion == 69) {
1016                upgradeToVersion200(db);
1017                createEventsView = true;
1018                oldVersion = 200;
1019            }
1020            if (oldVersion == 70) {
1021                upgradeToVersion200(db);
1022                oldVersion = 200;
1023            }
1024            if (oldVersion == 100) {
1025                upgradeToVersion200(db);
1026                oldVersion = 200;
1027            }
1028            boolean need203Update = true;
1029            if (oldVersion == 101) {
1030                oldVersion = 200;
1031                // Gingerbread version 101 is similar to Honeycomb version 203
1032                need203Update = false;
1033            }
1034            if (oldVersion == 200) {
1035                upgradeToVersion201(db);
1036                oldVersion += 1;
1037            }
1038            if (oldVersion == 201) {
1039                upgradeToVersion202(db);
1040                createEventsView = true;
1041                oldVersion += 1;
1042            }
1043            if (oldVersion == 202 && need203Update) {
1044                upgradeToVersion203(db);
1045                oldVersion += 1;
1046            }
1047            if (oldVersion == 203) {
1048                createEventsView = true;
1049                oldVersion += 1;
1050            }
1051            if (oldVersion == 204) {
1052                // This is an ICS update, all following use 300+ versions.
1053                upgradeToVersion205(db);
1054                createEventsView = true;
1055                oldVersion += 1;
1056            }
1057            if (oldVersion == 205) {
1058                // Move ICS updates to 300 range
1059                upgradeToVersion300(db);
1060                createEventsView = true;
1061                oldVersion = 300;
1062            }
1063            if (oldVersion == 300) {
1064                upgradeToVersion301(db);
1065                createEventsView = true;
1066                oldVersion++;
1067            }
1068            if (oldVersion == 301) {
1069                upgradeToVersion302(db);
1070                oldVersion++;
1071            }
1072            if (oldVersion == 302) {
1073                upgradeToVersion303(db);
1074                oldVersion++;
1075                createEventsView = true;
1076            }
1077            if (oldVersion == 303) {
1078                upgradeToVersion304(db);
1079                oldVersion++;
1080                createEventsView = true;
1081            }
1082            if (createEventsView) {
1083                createEventsView(db);
1084            }
1085            if (oldVersion != DATABASE_VERSION) {
1086                Log.e(TAG, "Need to recreate Calendar schema because of "
1087                        + "unknown Calendar database version: " + oldVersion);
1088                dropTables(db);
1089                bootstrapDB(db);
1090                oldVersion = DATABASE_VERSION;
1091            }
1092        } catch (SQLiteException e) {
1093            Log.e(TAG, "onUpgrade: SQLiteException, recreating db. " + e);
1094            dropTables(db);
1095            bootstrapDB(db);
1096            return; // this was lossy
1097        }
1098
1099        /**
1100         * db versions < 100 correspond to Froyo and earlier. Gingerbread bumped
1101         * the db versioning to 100. Honeycomb bumped it to 200. ICS will begin
1102         * in 300. At each major release we should jump to the next
1103         * centiversion.
1104         */
1105    }
1106
1107    /**
1108     * If the user_version of the database if between 59 and 66 (those versions has been deployed
1109     * with no primary key for the CalendarMetaData table)
1110     */
1111    private void recreateMetaDataAndInstances67(SQLiteDatabase db) {
1112        // Recreate the CalendarMetaData table with correct primary key
1113        db.execSQL("DROP TABLE CalendarMetaData;");
1114        createCalendarMetaDataTable59(db);
1115
1116        // Also clean the Instance table as this table may be corrupted
1117        db.execSQL("DELETE FROM Instances;");
1118    }
1119
1120    private static boolean fixAllDayTime(Time time, String timezone, Long timeInMillis) {
1121        time.set(timeInMillis);
1122        if(time.hour != 0 || time.minute != 0 || time.second != 0) {
1123            time.hour = 0;
1124            time.minute = 0;
1125            time.second = 0;
1126            return true;
1127        }
1128        return false;
1129    }
1130
1131    @VisibleForTesting
1132    void upgradeToVersion304(SQLiteDatabase db) {
1133        /*
1134         * Changes from version 303 to 304:
1135         * - add canPartiallyUpdate to Calendars table
1136         * - add sync_data7 to Calendars to Events table
1137         * - add lastSynced to Calendars to Events table
1138         */
1139        db.execSQL("ALTER TABLE Calendars ADD COLUMN canPartiallyUpdate INTEGER DEFAULT 0;");
1140        db.execSQL("ALTER TABLE Events ADD COLUMN sync_data7 TEXT;");
1141        db.execSQL("ALTER TABLE Events ADD COLUMN lastSynced INTEGER DEFAULT 0;");
1142    }
1143
1144    @VisibleForTesting
1145    void upgradeToVersion303(SQLiteDatabase db) {
1146        /*
1147         * Changes from version 302 to 303:
1148         * - change SYNCx columns to CAL_SYNCx
1149         */
1150
1151        // rename old table, create new table with updated layout
1152        db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
1153        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1154        createCalendarsTable303(db);
1155
1156        // copy fields from old to new
1157        db.execSQL("INSERT INTO Calendars (" +
1158                "_id, " +
1159                "account_name, " +
1160                "account_type, " +
1161                "_sync_id, " +
1162                "_sync_version, " +
1163                "_sync_time, " +
1164                "dirty, " +
1165                "name, " +
1166                "displayName, " +
1167                "calendar_color, " +
1168                "access_level, " +
1169                "visible, " +
1170                "sync_events, " +
1171                "calendar_location, " +
1172                "calendar_timezone, " +
1173                "ownerAccount, " +
1174                "canOrganizerRespond, " +
1175                "canModifyTimeZone, " +
1176                "maxReminders, " +
1177                "allowedReminders, " +
1178                "deleted, " +
1179                "cal_sync1, " +     // rename from sync1
1180                "cal_sync2, " +     // rename from sync2
1181                "cal_sync3, " +     // rename from sync3
1182                "cal_sync4, " +     // rename from sync4
1183                "cal_sync5, " +     // rename from sync5
1184                "cal_sync6) " +     // rename from sync6
1185                "SELECT " +
1186                "_id, " +
1187                "account_name, " +
1188                "account_type, " +
1189                "_sync_id, " +
1190                "_sync_version, " +
1191                "_sync_time, " +
1192                "dirty, " +
1193                "name, " +
1194                "displayName, " +
1195                "calendar_color, " +
1196                "access_level, " +
1197                "visible, " +
1198                "sync_events, " +
1199                "calendar_location, " +
1200                "calendar_timezone, " +
1201                "ownerAccount, " +
1202                "canOrganizerRespond, " +
1203                "canModifyTimeZone, " +
1204                "maxReminders, " +
1205                "allowedReminders," +
1206                "deleted, " +
1207                "sync1, " +
1208                "sync2, " +
1209                "sync3, " +
1210                "sync4," +
1211                "sync5," +
1212                "sync6 " +
1213                "FROM Calendars_Backup;"
1214        );
1215
1216        // drop the old table
1217        db.execSQL("DROP TABLE Calendars_Backup;");
1218    }
1219
1220    @VisibleForTesting
1221    void upgradeToVersion302(SQLiteDatabase db) {
1222        /*
1223         * Changes from version 301 to 302
1224         * - Move Exchange eventEndTimezone values to SYNC_DATA1
1225         */
1226        db.execSQL("UPDATE Events SET sync_data1=eventEndTimezone WHERE calendar_id IN "
1227                + "(SELECT _id FROM Calendars WHERE account_type='com.android.exchange');");
1228
1229        db.execSQL("UPDATE Events SET eventEndTimezone=NULL WHERE calendar_id IN "
1230                + "(SELECT _id FROM Calendars WHERE account_type='com.android.exchange');");
1231    }
1232
1233    @VisibleForTesting
1234    void upgradeToVersion301(SQLiteDatabase db) {
1235        /*
1236         * Changes from version 300 to 301
1237         * - Added original_id column to Events table
1238         * - Added triggers to keep original_id and original_sync_id in sync
1239         */
1240
1241        db.execSQL("DROP TRIGGER IF EXISTS " + SYNC_ID_UPDATE_TRIGGER_NAME + ";");
1242
1243        db.execSQL("ALTER TABLE Events ADD COLUMN original_id INTEGER;");
1244
1245        // Fill in the original_id for all events that have an original_sync_id
1246        db.execSQL("UPDATE Events set original_id=" +
1247                "(SELECT Events2._id FROM Events AS Events2 " +
1248                        "WHERE Events2._sync_id=Events.original_sync_id) " +
1249                "WHERE Events.original_sync_id NOT NULL");
1250        // Trigger to update exceptions when an original event updates its
1251        // _sync_id
1252        db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
1253    }
1254
1255    @VisibleForTesting
1256    void upgradeToVersion300(SQLiteDatabase db) {
1257
1258        /*
1259         * Changes from version 205 to 300:
1260         * - rename _sync_account to account_name in Calendars table
1261         * - remove _sync_account from Events table
1262         * - rename _sync_account_type to account_type in Calendars table
1263         * - remove _sync_account_type from Events table
1264         * - rename _sync_dirty to dirty in Calendars/Events table
1265         * - rename color to calendar_color in Calendars table
1266         * - rename location to calendar_location in Calendars table
1267         * - rename timezone to calendar_timezone in Calendars table
1268         * - add allowedReminders in Calendars table
1269         * - rename visibility to accessLevel in Events table
1270         * - rename transparency to availability in Events table
1271         * - rename originalEvent to original_sync_id in Events table
1272         * - remove dtstart2 and dtend2 from Events table
1273         * - rename syncAdapterData to sync_data1 in Events table
1274         */
1275
1276        // rename old table, create new table with updated layout
1277        db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
1278        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup;");
1279        createCalendarsTable300(db);
1280
1281        // copy fields from old to new
1282        db.execSQL("INSERT INTO Calendars (" +
1283                "_id, " +
1284                "account_name, " +          // rename from _sync_account
1285                "account_type, " +          // rename from _sync_account_type
1286                "_sync_id, " +
1287                "_sync_version, " +
1288                "_sync_time, " +
1289                "dirty, " +                 // rename from _sync_dirty
1290                "name, " +
1291                "displayName, " +
1292                "calendar_color, " +        // rename from color
1293                "access_level, " +
1294                "visible, " +
1295                "sync_events, " +
1296                "calendar_location, " +     // rename from location
1297                "calendar_timezone, " +     // rename from timezone
1298                "ownerAccount, " +
1299                "canOrganizerRespond, " +
1300                "canModifyTimeZone, " +
1301                "maxReminders, " +
1302                "allowedReminders," +
1303                "deleted, " +
1304                "sync1, " +
1305                "sync2, " +
1306                "sync3, " +
1307                "sync4," +
1308                "sync5," +
1309                "sync6) " +
1310
1311                "SELECT " +
1312                "_id, " +
1313                "_sync_account, " +
1314                "_sync_account_type, " +
1315                "_sync_id, " +
1316                "_sync_version, " +
1317                "_sync_time, " +
1318                "_sync_dirty, " +
1319                "name, " +
1320                "displayName, " +
1321                "color, " +
1322                "access_level, " +
1323                "visible, " +
1324                "sync_events, " +
1325                "location, " +
1326                "timezone, " +
1327                "ownerAccount, " +
1328                "canOrganizerRespond, " +
1329                "canModifyTimeZone, " +
1330                "maxReminders, " +
1331                "'0,1,2,3'," +
1332                "deleted, " +
1333                "sync1, " +
1334                "sync2, " +
1335                "sync3, " +
1336                "sync4, " +
1337                "sync5, " +
1338                "sync6 " +
1339                "FROM Calendars_Backup;"
1340        );
1341
1342        // drop the old table
1343        db.execSQL("DROP TABLE Calendars_Backup;");
1344
1345        db.execSQL("ALTER TABLE Events RENAME TO Events_Backup;");
1346        db.execSQL("DROP TRIGGER IF EXISTS events_insert");
1347        db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
1348        db.execSQL("DROP INDEX IF EXISTS eventSyncAccountAndIdIndex");
1349        db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
1350        createEventsTable300(db);
1351
1352        // copy fields from old to new
1353        db.execSQL("INSERT INTO Events (" +
1354                "_id, " +
1355                "_sync_id, " +
1356                "_sync_version, " +
1357                "_sync_time, " +
1358                "_sync_local_id, " +
1359                "dirty, " +                 // renamed from _sync_dirty
1360                "_sync_mark, " +
1361                "calendar_id, " +
1362                "htmlUri, " +
1363                "title, " +
1364                "eventLocation, " +
1365                "description, " +
1366                "eventStatus, " +
1367                "selfAttendeeStatus, " +
1368                "commentsUri, " +
1369                "dtstart, " +
1370                "dtend, " +
1371                "eventTimezone, " +
1372                "eventEndTimezone, " +      // renamed from eventTimezone2
1373                "duration, " +
1374                "allDay, " +
1375                "accessLevel, " +           // renamed from visibility
1376                "availability, " +          // renamed from transparency
1377                "hasAlarm, " +
1378                "hasExtendedProperties, " +
1379                "rrule, " +
1380                "rdate, " +
1381                "exrule, " +
1382                "exdate, " +
1383                "original_sync_id, " +      // renamed from originalEvent
1384                "originalInstanceTime, " +
1385                "originalAllDay, " +
1386                "lastDate, " +
1387                "hasAttendeeData, " +
1388                "guestsCanModify, " +
1389                "guestsCanInviteOthers, " +
1390                "guestsCanSeeGuests, " +
1391                "organizer, " +
1392                "deleted, " +
1393                "sync_data1) " +             // renamed from syncAdapterData
1394
1395                "SELECT " +
1396                "_id, " +
1397                "_sync_id, " +
1398                "_sync_version, " +
1399                "_sync_time, " +
1400                "_sync_local_id, " +
1401                "_sync_dirty, " +
1402                "_sync_mark, " +
1403                "calendar_id, " +
1404                "htmlUri, " +
1405                "title, " +
1406                "eventLocation, " +
1407                "description, " +
1408                "eventStatus, " +
1409                "selfAttendeeStatus, " +
1410                "commentsUri, " +
1411                "dtstart, " +
1412                "dtend, " +
1413                "eventTimezone, " +
1414                "eventTimezone2, " +
1415                "duration, " +
1416                "allDay, " +
1417                "visibility, " +
1418                "transparency, " +
1419                "hasAlarm, " +
1420                "hasExtendedProperties, " +
1421                "rrule, " +
1422                "rdate, " +
1423                "exrule, " +
1424                "exdate, " +
1425                "originalEvent, " +
1426                "originalInstanceTime, " +
1427                "originalAllDay, " +
1428                "lastDate, " +
1429                "hasAttendeeData, " +
1430                "guestsCanModify, " +
1431                "guestsCanInviteOthers, " +
1432                "guestsCanSeeGuests, " +
1433                "organizer, " +
1434                "deleted, " +
1435                "syncAdapterData " +
1436
1437                "FROM Events_Backup;"
1438        );
1439
1440        db.execSQL("DROP TABLE Events_Backup;");
1441
1442        // Trigger to remove data tied to an event when we delete that event.
1443        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
1444                "BEGIN " +
1445                EVENTS_CLEANUP_TRIGGER_SQL +
1446                "END");
1447
1448    }
1449
1450    @VisibleForTesting
1451    void upgradeToVersion205(SQLiteDatabase db) {
1452        /*
1453         * Changes from version 204 to 205:
1454         * - rename+reorder "_sync_mark" to "sync6" (and change type from INTEGER to TEXT)
1455         * - rename "selected" to "visible"
1456         * - rename "organizerCanRespond" to "canOrganizerRespond"
1457         * - add "canModifyTimeZone"
1458         * - add "maxReminders"
1459         * - remove "_sync_local_id" (a/k/a _SYNC_DATA)
1460         */
1461
1462        // rename old table, create new table with updated layout
1463        db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
1464        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1465        createCalendarsTable205(db);
1466
1467        // copy fields from old to new
1468        db.execSQL("INSERT INTO Calendars (" +
1469                "_id, " +
1470                "_sync_account, " +
1471                "_sync_account_type, " +
1472                "_sync_id, " +
1473                "_sync_version, " +
1474                "_sync_time, " +
1475                "_sync_dirty, " +
1476                "name, " +
1477                "displayName, " +
1478                "color, " +
1479                "access_level, " +
1480                "visible, " +                   // rename from "selected"
1481                "sync_events, " +
1482                "location, " +
1483                "timezone, " +
1484                "ownerAccount, " +
1485                "canOrganizerRespond, " +       // rename from "organizerCanRespond"
1486                "canModifyTimeZone, " +
1487                "maxReminders, " +
1488                "deleted, " +
1489                "sync1, " +
1490                "sync2, " +
1491                "sync3, " +
1492                "sync4," +
1493                "sync5," +
1494                "sync6) " +                     // rename/reorder from _sync_mark
1495                "SELECT " +
1496                "_id, " +
1497                "_sync_account, " +
1498                "_sync_account_type, " +
1499                "_sync_id, " +
1500                "_sync_version, " +
1501                "_sync_time, " +
1502                "_sync_dirty, " +
1503                "name, " +
1504                "displayName, " +
1505                "color, " +
1506                "access_level, " +
1507                "selected, " +
1508                "sync_events, " +
1509                "location, " +
1510                "timezone, " +
1511                "ownerAccount, " +
1512                "organizerCanRespond, " +
1513                "1, " +
1514                "5, " +
1515                "deleted, " +
1516                "sync1, " +
1517                "sync2, " +
1518                "sync3, " +
1519                "sync4, " +
1520                "sync5, " +
1521                "_sync_mark " +
1522                "FROM Calendars_Backup;"
1523        );
1524
1525        // set these fields appropriately for Exchange events
1526        db.execSQL("UPDATE Calendars SET canModifyTimeZone=0, maxReminders=1 " +
1527                "WHERE _sync_account_type='com.android.exchange'");
1528
1529        // drop the old table
1530        db.execSQL("DROP TABLE Calendars_Backup;");
1531    }
1532
1533    @VisibleForTesting
1534    void upgradeToVersion203(SQLiteDatabase db) {
1535        // Same as Gingerbread version 100
1536        Cursor cursor = db.rawQuery("SELECT value FROM CalendarCache WHERE key=?",
1537                new String[] {"timezoneDatabaseVersion"});
1538
1539        String oldTimezoneDbVersion = null;
1540        if (cursor != null && cursor.moveToNext()) {
1541            try {
1542                oldTimezoneDbVersion = cursor.getString(0);
1543            } finally {
1544                cursor.close();
1545            }
1546            // Also clean the CalendarCache table
1547            db.execSQL("DELETE FROM CalendarCache;");
1548        }
1549        initCalendarCacheTable203(db, oldTimezoneDbVersion);
1550
1551        // Same as Gingerbread version 101
1552        updateCalendarCacheTableTo203(db);
1553    }
1554
1555    @VisibleForTesting
1556    void upgradeToVersion202(SQLiteDatabase db) {
1557        // We will drop the "hidden" column from the calendar schema and add the "sync5" column
1558        db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
1559
1560        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1561        createCalendarsTable202(db);
1562
1563        // Populate the new Calendars table and put into the "sync5" column the value of the
1564        // old "hidden" column
1565        db.execSQL("INSERT INTO Calendars (" +
1566                "_id, " +
1567                "_sync_account, " +
1568                "_sync_account_type, " +
1569                "_sync_id, " +
1570                "_sync_version, " +
1571                "_sync_time, " +
1572                "_sync_local_id, " +
1573                "_sync_dirty, " +
1574                "_sync_mark, " +
1575                "name, " +
1576                "displayName, " +
1577                "color, " +
1578                "access_level, " +
1579                "selected, " +
1580                "sync_events, " +
1581                "location, " +
1582                "timezone, " +
1583                "ownerAccount, " +
1584                "organizerCanRespond, " +
1585                "deleted, " +
1586                "sync1, " +
1587                "sync2, " +
1588                "sync3, " +
1589                "sync4," +
1590                "sync5) " +
1591                "SELECT " +
1592                "_id, " +
1593                "_sync_account, " +
1594                "_sync_account_type, " +
1595                "_sync_id, " +
1596                "_sync_version, " +
1597                "_sync_time, " +
1598                "_sync_local_id, " +
1599                "_sync_dirty, " +
1600                "_sync_mark, " +
1601                "name, " +
1602                "displayName, " +
1603                "color, " +
1604                "access_level, " +
1605                "selected, " +
1606                "sync_events, " +
1607                "location, " +
1608                "timezone, " +
1609                "ownerAccount, " +
1610                "organizerCanRespond, " +
1611                "deleted, " +
1612                "sync1, " +
1613                "sync2, " +
1614                "sync3, " +
1615                "sync4, " +
1616                "hidden " +
1617                "FROM Calendars_Backup;"
1618        );
1619
1620        // Drop the backup table
1621        db.execSQL("DROP TABLE Calendars_Backup;");
1622    }
1623
1624    @VisibleForTesting
1625    void upgradeToVersion201(SQLiteDatabase db) {
1626        db.execSQL("ALTER TABLE Calendars ADD COLUMN sync4 TEXT;");
1627    }
1628
1629    @VisibleForTesting
1630    void upgradeToVersion200(SQLiteDatabase db) {
1631        // we cannot use here a Calendar.Calendars,URL constant for "url" as we are trying to make
1632        // it disappear so we are keeping the hardcoded name "url" in all the SQLs
1633        db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
1634
1635        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1636        createCalendarsTable200(db);
1637
1638        // Populate the new Calendars table except the SYNC2 / SYNC3 columns
1639        db.execSQL("INSERT INTO Calendars (" +
1640                "_id, " +
1641                "_sync_account, " +
1642                "_sync_account_type, " +
1643                "_sync_id, " +
1644                "_sync_version, " +
1645                "_sync_time, " +
1646                "_sync_local_id, " +
1647                "_sync_dirty, " +
1648                "_sync_mark, " +
1649                "name, " +
1650                "displayName, " +
1651                "color, " +
1652                "access_level, " +
1653                "selected, " +
1654                "sync_events, " +
1655                "location, " +
1656                "timezone, " +
1657                "ownerAccount, " +
1658                "organizerCanRespond, " +
1659                "deleted, " +
1660                "sync1) " +
1661                "SELECT " +
1662                "_id, " +
1663                "_sync_account, " +
1664                "_sync_account_type, " +
1665                "_sync_id, " +
1666                "_sync_version, " +
1667                "_sync_time, " +
1668                "_sync_local_id, " +
1669                "_sync_dirty, " +
1670                "_sync_mark, " +
1671                "name, " +
1672                "displayName, " +
1673                "color, " +
1674                "access_level, " +
1675                "selected, " +
1676                "sync_events, " +
1677                "location, " +
1678                "timezone, " +
1679                "ownerAccount, " +
1680                "organizerCanRespond, " +
1681                "0, " +
1682                "url " +
1683                "FROM Calendars_Backup;"
1684        );
1685
1686        // Populate SYNC2 and SYNC3 columns - SYNC1 represent the old "url" column
1687        // We will need to iterate over all the "com.google" type of calendars
1688        String selectSql = "SELECT _id, url" +
1689                " FROM Calendars_Backup" +
1690                " WHERE _sync_account_type='com.google'" +
1691                " AND url IS NOT NULL;";
1692
1693        String updateSql = "UPDATE Calendars SET " +
1694                "sync2=?, " + // edit Url
1695                "sync3=? " + // self Url
1696                "WHERE _id=?;";
1697
1698        Cursor cursor = db.rawQuery(selectSql, null /* selection args */);
1699        if (cursor != null && cursor.getCount() > 0) {
1700            try {
1701                Object[] bindArgs = new Object[3];
1702
1703                while (cursor.moveToNext()) {
1704                    Long id = cursor.getLong(0);
1705                    String url = cursor.getString(1);
1706                    String selfUrl = getSelfUrlFromEventsUrl(url);
1707                    String editUrl = getEditUrlFromEventsUrl(url);
1708
1709                    bindArgs[0] = editUrl;
1710                    bindArgs[1] = selfUrl;
1711                    bindArgs[2] = id;
1712
1713                    db.execSQL(updateSql, bindArgs);
1714                }
1715            } finally {
1716                cursor.close();
1717            }
1718        }
1719
1720        // Drop the backup table
1721        db.execSQL("DROP TABLE Calendars_Backup;");
1722    }
1723
1724    @VisibleForTesting
1725    static void upgradeToVersion69(SQLiteDatabase db) {
1726        // Clean up allDay events which could be in an invalid state from an earlier version
1727        // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This
1728        // will go through the allDay events and make sure they have proper values and are in the
1729        // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that
1730        // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2
1731        // and dtend2 are at midnight in their timezone.
1732        final String sql = "SELECT _id, " +
1733                "dtstart, " +
1734                "dtend, " +
1735                "duration, " +
1736                "dtstart2, " +
1737                "dtend2, " +
1738                "eventTimezone, " +
1739                "eventTimezone2, " +
1740                "rrule " +
1741                "FROM Events " +
1742                "WHERE allDay=?";
1743        Cursor cursor = db.rawQuery(sql, new String[] {"1"});
1744        if (cursor != null) {
1745            try {
1746                String timezone;
1747                String timezone2;
1748                String duration;
1749                Long dtstart;
1750                Long dtstart2;
1751                Long dtend;
1752                Long dtend2;
1753                Time time = new Time();
1754                Long id;
1755                // some things need to be in utc so we call this frequently, cache to make faster
1756                final String utc = Time.TIMEZONE_UTC;
1757                while (cursor.moveToNext()) {
1758                    String rrule = cursor.getString(8);
1759                    id = cursor.getLong(0);
1760                    dtstart = cursor.getLong(1);
1761                    dtstart2 = null;
1762                    timezone = cursor.getString(6);
1763                    timezone2 = cursor.getString(7);
1764                    duration = cursor.getString(3);
1765
1766                    if (TextUtils.isEmpty(rrule)) {
1767                        // For non-recurring events dtstart and dtend should both have values
1768                        // and duration should be null.
1769                        dtend = cursor.getLong(2);
1770                        dtend2 = null;
1771                        // Since we made all three of these at the same time if timezone2 exists
1772                        // so should dtstart2 and dtend2.
1773                        if(!TextUtils.isEmpty(timezone2)) {
1774                            dtstart2 = cursor.getLong(4);
1775                            dtend2 = cursor.getLong(5);
1776                        }
1777
1778                        boolean update = false;
1779                        if (!TextUtils.equals(timezone, utc)) {
1780                            update = true;
1781                            timezone = utc;
1782                        }
1783
1784                        time.clear(timezone);
1785                        update |= fixAllDayTime(time, timezone, dtstart);
1786                        dtstart = time.normalize(false);
1787
1788                        time.clear(timezone);
1789                        update |= fixAllDayTime(time, timezone, dtend);
1790                        dtend = time.normalize(false);
1791
1792                        if (dtstart2 != null) {
1793                            time.clear(timezone2);
1794                            update |= fixAllDayTime(time, timezone2, dtstart2);
1795                            dtstart2 = time.normalize(false);
1796                        }
1797
1798                        if (dtend2 != null) {
1799                            time.clear(timezone2);
1800                            update |= fixAllDayTime(time, timezone2, dtend2);
1801                            dtend2 = time.normalize(false);
1802                        }
1803
1804                        if (!TextUtils.isEmpty(duration)) {
1805                            update = true;
1806                        }
1807
1808                        if (update) {
1809                            // enforce duration being null
1810                            db.execSQL("UPDATE Events SET " +
1811                                    "dtstart=?, " +
1812                                    "dtend=?, " +
1813                                    "dtstart2=?, " +
1814                                    "dtend2=?, " +
1815                                    "duration=?, " +
1816                                    "eventTimezone=?, " +
1817                                    "eventTimezone2=? " +
1818                                    "WHERE _id=?",
1819                                    new Object[] {
1820                                            dtstart,
1821                                            dtend,
1822                                            dtstart2,
1823                                            dtend2,
1824                                            null,
1825                                            timezone,
1826                                            timezone2,
1827                                            id}
1828                            );
1829                        }
1830
1831                    } else {
1832                        // For recurring events only dtstart and duration should be used.
1833                        // We ignore dtend since it will be overwritten if the event changes to a
1834                        // non-recurring event and won't be used otherwise.
1835                        if(!TextUtils.isEmpty(timezone2)) {
1836                            dtstart2 = cursor.getLong(4);
1837                        }
1838
1839                        boolean update = false;
1840                        if (!TextUtils.equals(timezone, utc)) {
1841                            update = true;
1842                            timezone = utc;
1843                        }
1844
1845                        time.clear(timezone);
1846                        update |= fixAllDayTime(time, timezone, dtstart);
1847                        dtstart = time.normalize(false);
1848
1849                        if (dtstart2 != null) {
1850                            time.clear(timezone2);
1851                            update |= fixAllDayTime(time, timezone2, dtstart2);
1852                            dtstart2 = time.normalize(false);
1853                        }
1854
1855                        if (TextUtils.isEmpty(duration)) {
1856                            // If duration was missing assume a 1 day duration
1857                            duration = "P1D";
1858                            update = true;
1859                        } else {
1860                            int len = duration.length();
1861                            // TODO fix durations in other formats as well
1862                            if (duration.charAt(0) == 'P' &&
1863                                    duration.charAt(len - 1) == 'S') {
1864                                int seconds = Integer.parseInt(duration.substring(1, len - 1));
1865                                int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1866                                duration = "P" + days + "D";
1867                                update = true;
1868                            }
1869                        }
1870
1871                        if (update) {
1872                            // If there were other problems also enforce dtend being null
1873                            db.execSQL("UPDATE Events SET " +
1874                                    "dtstart=?, " +
1875                                    "dtend=?, " +
1876                                    "dtstart2=?, " +
1877                                    "dtend2=?, " +
1878                                    "duration=?," +
1879                                    "eventTimezone=?, " +
1880                                    "eventTimezone2=? " +
1881                                    "WHERE _id=?",
1882                                    new Object[] {
1883                                            dtstart,
1884                                            null,
1885                                            dtstart2,
1886                                            null,
1887                                            duration,
1888                                            timezone,
1889                                            timezone2,
1890                                            id}
1891                            );
1892                        }
1893                    }
1894                }
1895            } finally {
1896                cursor.close();
1897            }
1898        }
1899    }
1900
1901    private void upgradeToVersion66(SQLiteDatabase db) {
1902        // Add a column to indicate whether the event organizer can respond to his own events
1903        // The UI should not show attendee status for events in calendars with this column = 0
1904        db.execSQL("ALTER TABLE Calendars" +
1905                " ADD COLUMN organizerCanRespond INTEGER NOT NULL DEFAULT 1;");
1906    }
1907
1908    private void upgradeToVersion64(SQLiteDatabase db) {
1909        // Add a column that may be used by sync adapters
1910        db.execSQL("ALTER TABLE Events" +
1911                " ADD COLUMN syncAdapterData TEXT;");
1912    }
1913
1914    private void upgradeToVersion62(SQLiteDatabase db) {
1915        // New columns are to transition to having allDay events in the local timezone
1916        db.execSQL("ALTER TABLE Events" +
1917                " ADD COLUMN dtstart2 INTEGER;");
1918        db.execSQL("ALTER TABLE Events" +
1919                " ADD COLUMN dtend2 INTEGER;");
1920        db.execSQL("ALTER TABLE Events" +
1921                " ADD COLUMN eventTimezone2 TEXT;");
1922
1923        String[] allDayBit = new String[] {"0"};
1924        // Copy over all the data that isn't an all day event.
1925        db.execSQL("UPDATE Events SET " +
1926                "dtstart2=dtstart," +
1927                "dtend2=dtend," +
1928                "eventTimezone2=eventTimezone " +
1929                "WHERE allDay=?;",
1930                allDayBit /* selection args */);
1931
1932        // "cursor" iterates over all the calendars
1933        allDayBit[0] = "1";
1934        Cursor cursor = db.rawQuery("SELECT Events._id," +
1935                "dtstart," +
1936                "dtend," +
1937                "eventTimezone," +
1938                "timezone " +
1939                "FROM Events INNER JOIN Calendars " +
1940                "WHERE Events.calendar_id=Calendars._id" +
1941                " AND allDay=?",
1942                allDayBit /* selection args */);
1943
1944        Time oldTime = new Time();
1945        Time newTime = new Time();
1946        // Update the allday events in the new columns
1947        if (cursor != null) {
1948            try {
1949                String[] newData = new String[4];
1950                cursor.moveToPosition(-1);
1951                while (cursor.moveToNext()) {
1952                    long id = cursor.getLong(0); // Order from query above
1953                    long dtstart = cursor.getLong(1);
1954                    long dtend = cursor.getLong(2);
1955                    String eTz = cursor.getString(3); // current event timezone
1956                    String tz = cursor.getString(4); // Calendar timezone
1957                    //If there's no timezone for some reason use UTC by default.
1958                    if(eTz == null) {
1959                        eTz = Time.TIMEZONE_UTC;
1960                    }
1961
1962                    // Convert start time for all day events into the timezone of their calendar
1963                    oldTime.clear(eTz);
1964                    oldTime.set(dtstart);
1965                    newTime.clear(tz);
1966                    newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
1967                    newTime.normalize(false);
1968                    dtstart = newTime.toMillis(false /*ignoreDst*/);
1969
1970                    // Convert end time for all day events into the timezone of their calendar
1971                    oldTime.clear(eTz);
1972                    oldTime.set(dtend);
1973                    newTime.clear(tz);
1974                    newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
1975                    newTime.normalize(false);
1976                    dtend = newTime.toMillis(false /*ignoreDst*/);
1977
1978                    newData[0] = String.valueOf(dtstart);
1979                    newData[1] = String.valueOf(dtend);
1980                    newData[2] = tz;
1981                    newData[3] = String.valueOf(id);
1982                    db.execSQL("UPDATE Events SET " +
1983                            "dtstart2=?, " +
1984                            "dtend2=?, " +
1985                            "eventTimezone2=? " +
1986                            "WHERE _id=?",
1987                            newData);
1988                }
1989            } finally {
1990                cursor.close();
1991            }
1992        }
1993    }
1994
1995    private void upgradeToVersion61(SQLiteDatabase db) {
1996        db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
1997
1998        // IF NOT EXISTS should be normal pattern for table creation
1999        db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" +
2000                "_id INTEGER PRIMARY KEY," +
2001                "key TEXT NOT NULL," +
2002                "value TEXT" +
2003                ");");
2004
2005        db.execSQL("INSERT INTO CalendarCache (" +
2006                "key, " +
2007                "value) VALUES (" +
2008                "'timezoneDatabaseVersion',"  +
2009                "'2009s'" +
2010                ");");
2011    }
2012
2013    private void upgradeToVersion60(SQLiteDatabase db) {
2014        // Switch to CalendarProvider2
2015        upgradeSyncState(db);
2016        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
2017        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
2018                "BEGIN " +
2019                ("DELETE FROM Events" +
2020                        " WHERE calendar_id=old._id;") +
2021                "END");
2022        db.execSQL("ALTER TABLE Events" +
2023                " ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;");
2024        db.execSQL("DROP TRIGGER IF EXISTS events_insert");
2025        // Trigger to set event's sync_account
2026        db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
2027                "BEGIN " +
2028                "UPDATE Events" +
2029                " SET _sync_account=" +
2030                " (SELECT _sync_account FROM Calendars" +
2031                " WHERE Calendars._id=new.calendar_id)," +
2032                "_sync_account_type=" +
2033                " (SELECT _sync_account_type FROM Calendars" +
2034                " WHERE Calendars._id=new.calendar_id) " +
2035                "WHERE Events._id=new._id;" +
2036                "END");
2037        db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
2038        db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
2039        // Trigger to remove data tied to an event when we delete that event.
2040        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
2041                "BEGIN " +
2042                ("DELETE FROM Instances" +
2043                    " WHERE event_id=old._id;" +
2044                "DELETE FROM EventsRawTimes" +
2045                    " WHERE event_id=old._id;" +
2046                "DELETE FROM Attendees" +
2047                    " WHERE event_id=old._id;" +
2048                "DELETE FROM Reminders" +
2049                    " WHERE event_id=old._id;" +
2050                "DELETE FROM CalendarAlerts" +
2051                    " WHERE event_id=old._id;" +
2052                "DELETE FROM ExtendedProperties" +
2053                    " WHERE event_id=old._id;") +
2054                "END");
2055        db.execSQL("DROP TRIGGER IF EXISTS attendees_update");
2056        db.execSQL("DROP TRIGGER IF EXISTS attendees_insert");
2057        db.execSQL("DROP TRIGGER IF EXISTS attendees_delete");
2058        db.execSQL("DROP TRIGGER IF EXISTS reminders_update");
2059        db.execSQL("DROP TRIGGER IF EXISTS reminders_insert");
2060        db.execSQL("DROP TRIGGER IF EXISTS reminders_delete");
2061        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update");
2062        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert");
2063        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete");
2064    }
2065
2066    private void upgradeToVersion59(SQLiteDatabase db) {
2067        db.execSQL("DROP TABLE IF EXISTS BusyBits;");
2068        db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup(" +
2069                "_id," +
2070                "localTimezone," +
2071                "minInstance," +
2072                "maxInstance" +
2073                ");");
2074        db.execSQL("INSERT INTO CalendarMetaData_Backup " +
2075                "SELECT " +
2076                "_id," +
2077                "localTimezone," +
2078                "minInstance," +
2079                "maxInstance" +
2080                " FROM CalendarMetaData;");
2081        db.execSQL("DROP TABLE CalendarMetaData;");
2082        createCalendarMetaDataTable59(db);
2083        db.execSQL("INSERT INTO CalendarMetaData " +
2084                "SELECT " +
2085                "_id," +
2086                "localTimezone," +
2087                "minInstance," +
2088                "maxInstance" +
2089                " FROM CalendarMetaData_Backup;");
2090        db.execSQL("DROP TABLE CalendarMetaData_Backup;");
2091    }
2092
2093    private void upgradeToVersion57(SQLiteDatabase db) {
2094        db.execSQL("ALTER TABLE Events" +
2095                " ADD COLUMN guestsCanModify" +
2096                " INTEGER NOT NULL DEFAULT 0;");
2097        db.execSQL("ALTER TABLE Events" +
2098                " ADD COLUMN guestsCanInviteOthers" +
2099                " INTEGER NOT NULL DEFAULT 1;");
2100        db.execSQL("ALTER TABLE Events" +
2101                " ADD COLUMN guestsCanSeeGuests" +
2102                " INTEGER NOT NULL DEFAULT 1;");
2103        db.execSQL("ALTER TABLE Events" +
2104                " ADD COLUMN organizer" +
2105                " STRING;");
2106        db.execSQL("UPDATE Events SET organizer=" +
2107                "(SELECT attendeeEmail" +
2108                " FROM Attendees"  +
2109                " WHERE " +
2110                "Attendees.event_id=" +
2111                "Events._id" +
2112                " AND " +
2113                "Attendees.attendeeRelationship=2);");
2114    }
2115
2116    private void upgradeToVersion56(SQLiteDatabase db) {
2117        db.execSQL("ALTER TABLE Calendars" +
2118                " ADD COLUMN ownerAccount TEXT;");
2119        db.execSQL("ALTER TABLE Events" +
2120                " ADD COLUMN hasAttendeeData INTEGER NOT NULL DEFAULT 0;");
2121
2122        // Clear _sync_dirty to avoid a client-to-server sync that could blow away
2123        // server attendees.
2124        // Clear _sync_version to pull down the server's event (with attendees)
2125        // Change the URLs from full-selfattendance to full
2126        db.execSQL("UPDATE Events"
2127                + " SET _sync_dirty=0, "
2128                + "_sync_version=NULL, "
2129                + "_sync_id="
2130                + "REPLACE(_sync_id, " +
2131                    "'/private/full-selfattendance', '/private/full'),"
2132                + "commentsUri="
2133                + "REPLACE(commentsUri, " +
2134                    "'/private/full-selfattendance', '/private/full');");
2135
2136        db.execSQL("UPDATE Calendars"
2137                + " SET url="
2138                + "REPLACE(url, '/private/full-selfattendance', '/private/full');");
2139
2140        // "cursor" iterates over all the calendars
2141        Cursor cursor = db.rawQuery("SELECT _id, " +
2142                "url FROM Calendars",
2143                null /* selection args */);
2144        // Add the owner column.
2145        if (cursor != null) {
2146            try {
2147                final String updateSql = "UPDATE Calendars" +
2148                        " SET ownerAccount=?" +
2149                        " WHERE _id=?";
2150                while (cursor.moveToNext()) {
2151                    Long id = cursor.getLong(0);
2152                    String url = cursor.getString(1);
2153                    String owner = calendarEmailAddressFromFeedUrl(url);
2154                    db.execSQL(updateSql, new Object[] {owner, id});
2155                }
2156            } finally {
2157                cursor.close();
2158            }
2159        }
2160    }
2161
2162    private void upgradeResync(SQLiteDatabase db) {
2163        // Delete sync state, so all records will be re-synced.
2164        db.execSQL("DELETE FROM _sync_state;");
2165
2166        // "cursor" iterates over all the calendars
2167        Cursor cursor = db.rawQuery("SELECT _sync_account," +
2168                "_sync_account_type,url FROM Calendars",
2169                null /* selection args */);
2170        if (cursor != null) {
2171            try {
2172                while (cursor.moveToNext()) {
2173                    String accountName = cursor.getString(0);
2174                    String accountType = cursor.getString(1);
2175                    final Account account = new Account(accountName, accountType);
2176                    String calendarUrl = cursor.getString(2);
2177                    scheduleSync(account, false /* two-way sync */, calendarUrl);
2178                }
2179            } finally {
2180                cursor.close();
2181            }
2182        }
2183    }
2184
2185    private void upgradeToVersion55(SQLiteDatabase db) {
2186        db.execSQL("ALTER TABLE Calendars ADD COLUMN " +
2187                "_sync_account_type TEXT;");
2188        db.execSQL("ALTER TABLE Events ADD COLUMN " +
2189                "_sync_account_type TEXT;");
2190        db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
2191        db.execSQL("UPDATE Calendars"
2192                + " SET _sync_account_type='com.google'"
2193                + " WHERE _sync_account IS NOT NULL");
2194        db.execSQL("UPDATE Events"
2195                + " SET _sync_account_type='com.google'"
2196                + " WHERE _sync_account IS NOT NULL");
2197        db.execSQL("UPDATE DeletedEvents"
2198                + " SET _sync_account_type='com.google'"
2199                + " WHERE _sync_account IS NOT NULL");
2200        Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
2201        db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
2202        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
2203                + "_sync_account_type, "
2204                + "_sync_account, "
2205                + "_sync_id);");
2206    }
2207
2208    private void upgradeToVersion54(SQLiteDatabase db) {
2209        Log.w(TAG, "adding eventSyncAccountAndIdIndex");
2210        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
2211                + "_sync_account, _sync_id);");
2212    }
2213
2214    private void upgradeToVersion53(SQLiteDatabase db) {
2215        Log.w(TAG, "Upgrading CalendarAlerts table");
2216        db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN " +
2217                "creationTime INTEGER NOT NULL DEFAULT 0;");
2218        db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN " +
2219                "receivedTime INTEGER NOT NULL DEFAULT 0;");
2220        db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN " +
2221                "notifyTime INTEGER NOT NULL DEFAULT 0;");
2222    }
2223
2224    private void upgradeToVersion52(SQLiteDatabase db) {
2225        // We added "originalAllDay" to the Events table to keep track of
2226        // the allDay status of the original recurring event for entries
2227        // that are exceptions to that recurring event.  We need this so
2228        // that we can format the date correctly for the "originalInstanceTime"
2229        // column when we make a change to the recurrence exception and
2230        // send it to the server.
2231        db.execSQL("ALTER TABLE Events ADD COLUMN " +
2232                "originalAllDay INTEGER;");
2233
2234        // Iterate through the Events table and for each recurrence
2235        // exception, fill in the correct value for "originalAllDay",
2236        // if possible.  The only times where this might not be possible
2237        // are (1) the original recurring event no longer exists, or
2238        // (2) the original recurring event does not yet have a _sync_id
2239        // because it was created on the phone and hasn't been synced to the
2240        // server yet.  In both cases the originalAllDay field will be set
2241        // to null.  In the first case we don't care because the recurrence
2242        // exception will not be displayed and we won't be able to make
2243        // any changes to it (and even if we did, the server should ignore
2244        // them, right?).  In the second case, the calendar client already
2245        // disallows making changes to an instance of a recurring event
2246        // until the recurring event has been synced to the server so the
2247        // second case should never occur.
2248
2249        // "cursor" iterates over all the recurrences exceptions.
2250        Cursor cursor = db.rawQuery("SELECT _id," +
2251                "originalEvent" +
2252                " FROM Events" +
2253                " WHERE originalEvent IS NOT NULL",
2254                null /* selection args */);
2255        if (cursor != null) {
2256            try {
2257                while (cursor.moveToNext()) {
2258                    long id = cursor.getLong(0);
2259                    String originalEvent = cursor.getString(1);
2260
2261                    // Find the original recurring event (if it exists)
2262                    Cursor recur = db.rawQuery("SELECT allDay" +
2263                            " FROM Events" +
2264                            " WHERE _sync_id=?",
2265                            new String[] {originalEvent});
2266                    if (recur == null) {
2267                        continue;
2268                    }
2269
2270                    try {
2271                        // Fill in the "originalAllDay" field of the
2272                        // recurrence exception with the "allDay" value
2273                        // from the recurring event.
2274                        if (recur.moveToNext()) {
2275                            int allDay = recur.getInt(0);
2276                            db.execSQL("UPDATE Events" +
2277                                    " SET originalAllDay=" + allDay +
2278                                    " WHERE _id="+id);
2279                        }
2280                    } finally {
2281                        recur.close();
2282                    }
2283                }
2284            } finally {
2285                cursor.close();
2286            }
2287        }
2288    }
2289
2290    private void upgradeToVersion51(SQLiteDatabase db) {
2291        Log.w(TAG, "Upgrading DeletedEvents table");
2292
2293        // We don't have enough information to fill in the correct
2294        // value of the calendar_id for old rows in the DeletedEvents
2295        // table, but rows in that table are transient so it is unlikely
2296        // that there are any rows.  Plus, the calendar_id is used only
2297        // when deleting a calendar, which is a rare event.  All new rows
2298        // will have the correct calendar_id.
2299        db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
2300
2301        // Trigger to remove a calendar's events when we delete the calendar
2302        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
2303        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
2304                "BEGIN " +
2305                "DELETE FROM Events WHERE calendar_id=" +
2306                    "old._id;" +
2307                "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
2308                "END");
2309        db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
2310    }
2311
2312    private void dropTables(SQLiteDatabase db) {
2313        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDARS + ";");
2314        db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS + ";");
2315        db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS_RAW_TIMES + ";");
2316        db.execSQL("DROP TABLE IF EXISTS " + Tables.INSTANCES + ";");
2317        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_META_DATA + ";");
2318        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";");
2319        db.execSQL("DROP TABLE IF EXISTS " + Tables.ATTENDEES + ";");
2320        db.execSQL("DROP TABLE IF EXISTS " + Tables.REMINDERS + ";");
2321        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_ALERTS + ";");
2322        db.execSQL("DROP TABLE IF EXISTS " + Tables.EXTENDED_PROPERTIES + ";");
2323    }
2324
2325    @Override
2326    public synchronized SQLiteDatabase getWritableDatabase() {
2327        SQLiteDatabase db = super.getWritableDatabase();
2328        return db;
2329    }
2330
2331    public SyncStateContentProviderHelper getSyncState() {
2332        return mSyncState;
2333    }
2334
2335    /**
2336     * Schedule a calendar sync for the account.
2337     * @param account the account for which to schedule a sync
2338     * @param uploadChangesOnly if set, specify that the sync should only send
2339     *   up local changes.  This is typically used for a local sync, a user override of
2340     *   too many deletions, or a sync after a calendar is unselected.
2341     * @param url the url feed for the calendar to sync (may be null, in which case a poll of
2342     *   all feeds is done.)
2343     */
2344    void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
2345        Bundle extras = new Bundle();
2346        if (uploadChangesOnly) {
2347            extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
2348        }
2349        if (url != null) {
2350            extras.putString("feed", url);
2351            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
2352        }
2353        ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras);
2354    }
2355
2356    private static void createEventsView(SQLiteDatabase db) {
2357        db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";");
2358        String eventsSelect = "SELECT "
2359                + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + ","
2360                + Calendar.Events.HTML_URI + ","
2361                + Calendar.Events.TITLE + ","
2362                + Calendar.Events.DESCRIPTION + ","
2363                + Calendar.Events.EVENT_LOCATION + ","
2364                + Calendar.Events.STATUS + ","
2365                + Calendar.Events.SELF_ATTENDEE_STATUS + ","
2366                + Calendar.Events.COMMENTS_URI + ","
2367                + Calendar.Events.DTSTART + ","
2368                + Calendar.Events.DTEND + ","
2369                + Calendar.Events.DURATION + ","
2370                + Calendar.Events.EVENT_TIMEZONE + ","
2371                + Calendar.Events.EVENT_END_TIMEZONE + ","
2372                + Calendar.Events.ALL_DAY + ","
2373                + Calendar.Events.ACCESS_LEVEL + ","
2374                + Calendar.Calendars.CALENDAR_TIMEZONE + ","
2375                + Calendar.Calendars.VISIBLE + ","
2376                + Calendar.Calendars.ACCESS_LEVEL + ","
2377                + Calendar.Events.AVAILABILITY + ","
2378                + Calendar.Calendars.CALENDAR_COLOR + ","
2379                + Calendar.Events.HAS_ALARM + ","
2380                + Calendar.Events.HAS_EXTENDED_PROPERTIES + ","
2381                + Calendar.Events.RRULE + ","
2382                + Calendar.Events.RDATE + ","
2383                + Calendar.Events.EXRULE + ","
2384                + Calendar.Events.EXDATE + ","
2385                + Calendar.Events.ORIGINAL_SYNC_ID + ","
2386                + Calendar.Events.ORIGINAL_ID + ","
2387                + Calendar.Events.ORIGINAL_INSTANCE_TIME + ","
2388                + Calendar.Events.ORIGINAL_ALL_DAY + ","
2389                + Calendar.Events.LAST_DATE + ","
2390                + Calendar.Events.HAS_ATTENDEE_DATA + ","
2391                + Calendar.Events.CALENDAR_ID + ","
2392                + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + ","
2393                + Calendar.Events.GUESTS_CAN_MODIFY + ","
2394                + Calendar.Events.GUESTS_CAN_SEE_GUESTS + ","
2395                + Calendar.Events.ORGANIZER + ","
2396                + Tables.EVENTS + "." + Calendar.Events.DELETED
2397                + " AS " + Calendar.Events.DELETED + ","
2398                + Tables.EVENTS + "." + Calendar.Events._SYNC_ID
2399                + " AS " + Calendar.Events._SYNC_ID + ","
2400                + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION
2401                + " AS " + Calendar.Events._SYNC_VERSION + ","
2402                + Calendar.Events.SYNC_DATA7 + ","
2403                + Tables.EVENTS + "." + Calendar.Events.DIRTY
2404                + " AS " + Calendar.Events.DIRTY + ","
2405                + Calendar.Events.LAST_SYNCED + ","
2406                + Tables.CALENDARS + "." + Calendar.Calendars.ACCOUNT_NAME
2407                + " AS " + Calendar.Events.ACCOUNT_NAME + ","
2408                + Tables.CALENDARS + "." + Calendar.Calendars.ACCOUNT_TYPE
2409                + " AS " + Calendar.Events.ACCOUNT_TYPE + ","
2410                + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME
2411                + " AS " + Calendar.Events._SYNC_TIME + ","
2412                + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA
2413                + " AS " + Calendar.Events._SYNC_DATA + ","
2414                + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK
2415                + " AS " + Calendar.Events._SYNC_MARK + ","
2416                + Calendar.Calendars.CAN_PARTIALLY_UPDATE + ","
2417                + Calendar.Calendars.CAL_SYNC1 + ","
2418                + Calendar.Calendars.CAL_SYNC2 + ","
2419                + Calendar.Calendars.CAL_SYNC3 + ","
2420                + Calendar.Calendars.CAL_SYNC4 + ","
2421                + Calendar.Calendars.CAL_SYNC5 + ","
2422                + Calendar.Calendars.CAL_SYNC6 + ","
2423                + Calendar.Calendars.OWNER_ACCOUNT + ","
2424                + Calendar.Calendars.SYNC_EVENTS  + ","
2425                + Calendar.Events.SYNC_DATA1
2426                + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS
2427                + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID
2428                + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID
2429                + ")";
2430
2431        db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect);
2432    }
2433
2434    /**
2435     * Extracts the calendar email from a calendar feed url.
2436     * @param feed the calendar feed url
2437     * @return the calendar email that is in the feed url or null if it can't
2438     * find the email address.
2439     * TODO: this is duplicated in CalendarSyncAdapter; move to a library
2440     */
2441    public static String calendarEmailAddressFromFeedUrl(String feed) {
2442        // Example feed url:
2443        // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
2444        String[] pathComponents = feed.split("/");
2445        if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
2446            try {
2447                return URLDecoder.decode(pathComponents[5], "UTF-8");
2448            } catch (UnsupportedEncodingException e) {
2449                Log.e(TAG, "unable to url decode the email address in calendar " + feed);
2450                return null;
2451            }
2452        }
2453
2454        Log.e(TAG, "unable to find the email address in calendar " + feed);
2455        return null;
2456    }
2457
2458    /**
2459     * Get a "allcalendars" url from a "private/full" or "private/free-busy" url
2460     * @param url
2461     * @return the rewritten Url
2462     *
2463     * For example:
2464     *
2465     *      http://www.google.com/calendar/feeds/joe%40joe.com/private/full
2466     *      http://www.google.com/calendar/feeds/joe%40joe.com/private/free-busy
2467     *
2468     * will be rewriten into:
2469     *
2470     *      http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com
2471     *      http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com
2472     */
2473    @VisibleForTesting
2474    private static String getAllCalendarsUrlFromEventsUrl(String url) {
2475        if (url == null) {
2476            if (Log.isLoggable(TAG, Log.DEBUG)) {
2477                Log.d(TAG, "Cannot get AllCalendars url from a NULL url");
2478            }
2479            return null;
2480        }
2481        if (url.contains("/private/full")) {
2482            return url.replace("/private/full", "").
2483                    replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full");
2484        }
2485        if (url.contains("/private/free-busy")) {
2486            return url.replace("/private/free-busy", "").
2487                    replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full");
2488        }
2489        // Just log as we dont recognize the provided Url
2490        if (Log.isLoggable(TAG, Log.DEBUG)) {
2491            Log.d(TAG, "Cannot get AllCalendars url from the following url: " + url);
2492        }
2493        return null;
2494    }
2495
2496    /**
2497     * Get "selfUrl" from "events url"
2498     * @param url the Events url (either "private/full" or "private/free-busy"
2499     * @return the corresponding allcalendar url
2500     */
2501    private static String getSelfUrlFromEventsUrl(String url) {
2502        return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url));
2503    }
2504
2505    /**
2506     * Get "editUrl" from "events url"
2507     * @param url the Events url (either "private/full" or "private/free-busy"
2508     * @return the corresponding allcalendar url
2509     */
2510    private static String getEditUrlFromEventsUrl(String url) {
2511        return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url));
2512    }
2513
2514    /**
2515     * Rewrite the url from "http" to "https" scheme
2516     * @param url the url to rewrite
2517     * @return the rewritten URL
2518     */
2519    private static String rewriteUrlFromHttpToHttps(String url) {
2520        if (url == null) {
2521            if (Log.isLoggable(TAG, Log.DEBUG)) {
2522                Log.d(TAG, "Cannot rewrite a NULL url");
2523            }
2524            return null;
2525        }
2526        if (url.startsWith(SCHEMA_HTTPS)) {
2527            return url;
2528        }
2529        if (!url.startsWith(SCHEMA_HTTP)) {
2530            throw new IllegalArgumentException("invalid url parameter, unknown scheme: " + url);
2531        }
2532        return SCHEMA_HTTPS + url.substring(SCHEMA_HTTP.length());
2533    }
2534
2535    protected void duplicateEvent(final long id) {
2536        final SQLiteDatabase db = getWritableDatabase();
2537        final long canPartiallyUpdate = DatabaseUtils.longForQuery(
2538                db,
2539                "SELECT " + Calendar.Calendars.CAN_PARTIALLY_UPDATE + " FROM " + Views.EVENTS
2540                        + " WHERE " + Events._ID + " = ?",
2541                new String[]{String.valueOf(id)});
2542        if (canPartiallyUpdate == 0) {
2543            return;
2544        }
2545
2546        db.execSQL("INSERT INTO " + CalendarDatabaseHelper.Tables.EVENTS
2547                + "  (" + LAST_SYNCED_EVENT_COLUMNS + ","
2548                +         Events.DIRTY + "," + Events.LAST_SYNCED + ")"
2549                + " SELECT " + LAST_SYNCED_EVENT_COLUMNS + ", 0, 1"
2550                + " FROM " + Tables.EVENTS
2551                + " WHERE "  + Events._ID + " = ? AND " + Events.DIRTY + " = ?",
2552                new Object[]{
2553                        id,
2554                        0, // Events.DIRTY
2555                });
2556        final long newId = DatabaseUtils.longForQuery(
2557                db, "SELECT CASE changes() WHEN 0 THEN -1 ELSE last_insert_rowid() END", null);
2558        if (newId < 0) {
2559            return;
2560        }
2561
2562        if (Log.isLoggable(TAG, Log.VERBOSE)) {
2563            Log.v(TAG, "Duplicating event " + id + " into new event " + newId);
2564        }
2565
2566        copyEventRelatedTables(db, newId, id);
2567    }
2568
2569    static void copyEventRelatedTables(SQLiteDatabase db, long newId, long id) {
2570        db.execSQL("INSERT INTO " + Tables.REMINDERS
2571                + " ( "  + Calendar.Reminders.EVENT_ID + ", " + LAST_SYNCED_REMINDER_COLUMNS + ") "
2572                + "SELECT ?," + LAST_SYNCED_REMINDER_COLUMNS
2573                + " FROM " + Tables.REMINDERS
2574                + " WHERE " + Calendar.Reminders.EVENT_ID + " = ?",
2575                new Object[] {newId, id});
2576        db.execSQL("INSERT INTO "
2577                + Tables.ATTENDEES
2578                + " (" + Calendar.Attendees.EVENT_ID + "," + LAST_SYNCED_ATTENDEE_COLUMNS + ") "
2579                + "SELECT ?," + LAST_SYNCED_ATTENDEE_COLUMNS + " FROM " + Tables.ATTENDEES
2580                + " WHERE " + Calendar.Attendees.EVENT_ID + " = ?",
2581                new Object[] {newId, id});
2582        db.execSQL("INSERT INTO " + Tables.EXTENDED_PROPERTIES
2583                + " (" + Calendar.ExtendedProperties.EVENT_ID + ","
2584                + LAST_SYNCED_EXTENDED_PROPERTY_COLUMNS + ") "
2585                + "SELECT ?, " + LAST_SYNCED_EXTENDED_PROPERTY_COLUMNS
2586                + " FROM " + Tables.EXTENDED_PROPERTIES
2587                + " WHERE " + Calendar.ExtendedProperties.EVENT_ID + " = ?",
2588                new Object[]{newId, id});
2589    }
2590
2591    protected void removeDuplicateEvent(final long id) {
2592        final SQLiteDatabase db = getWritableDatabase();
2593        final Cursor cursor = db.rawQuery("SELECT " + Events._ID + " FROM " + Tables.EVENTS
2594                + " WHERE " + Events._SYNC_ID
2595                + " = (SELECT " + Events._SYNC_ID
2596                + " FROM " + Tables.EVENTS
2597                + " WHERE " + Events._ID + " = ?) "
2598                + "AND " + Events.LAST_SYNCED + " = ?",
2599                new String[]{
2600                        String.valueOf(id),
2601                        "1", // Events.LAST_SYNCED
2602                });
2603        try {
2604            // there should only be at most one but this can't hurt
2605            if (cursor.moveToNext()) {
2606                final long dupId = cursor.getLong(0);
2607
2608                if (Log.isLoggable(TAG, Log.VERBOSE)) {
2609                    Log.v(TAG, "Removing duplicate event " + dupId + " of original event " + id);
2610                }
2611                // triggers will clean up related tables.
2612                db.execSQL("DELETE FROM Events WHERE " + Events._ID + " = ?", new Object[]{dupId});
2613            }
2614        } finally {
2615          cursor.close();
2616        }
2617    }
2618}
2619