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