CalendarDatabaseHelper.java revision 58313767dd0e7e3823c35ca61b40eddb9dd229bb
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            // This is needed for "converting" 72 to 200, 73 to 201 and 74 to 202
671            // TODO: This code needs to be deleted later on especially when Froyo starts hitting
672            // those numbers
673            if (oldVersion == 72) {
674                oldVersion = 200;
675            }
676            if (oldVersion == 73) {
677                oldVersion = 201;
678            }
679            if (oldVersion == 74) {
680                oldVersion = 202;
681            }
682            if (oldVersion == 200) {
683                upgradeToVersion201(db);
684                oldVersion += 1;
685            }
686            if (oldVersion == 201) {
687                upgradeToVersion202(db);
688                oldVersion += 1;
689            }
690            if (oldVersion == 202 && need203Update) {
691                upgradeToVersion203(db);
692                oldVersion += 1;
693            }
694            if (oldVersion != DATABASE_VERSION) {
695                Log.e(TAG, "Need to recreate Calendar schema because of "
696                        + "unknown Calendar database version: " + oldVersion);
697                dropTables(db);
698                bootstrapDB(db);
699                oldVersion = DATABASE_VERSION;
700            }
701        } catch (SQLiteException e) {
702            Log.e(TAG, "onUpgrade: SQLiteException, recreating db. " + e);
703            dropTables(db);
704            bootstrapDB(db);
705            return; // this was lossy
706        }
707    }
708
709    /**
710     * If the user_version of the database if between 59 and 66 (those versions has been deployed
711     * with no primary key for the CalendarMetaData table)
712     */
713    private void recreateMetaDataAndInstances(SQLiteDatabase db) {
714        // Recreate the CalendarMetaData table with correct primary key
715        db.execSQL("DROP TABLE " + Tables.CALENDAR_META_DATA + ";");
716        createCalendarMetaDataTable(db);
717
718        // Also clean the Instance table as this table may be corrupted
719        db.execSQL("DELETE FROM " + Tables.INSTANCES + ";");
720    }
721
722    private static boolean fixAllDayTime(Time time, String timezone, Long timeInMillis) {
723        time.set(timeInMillis);
724        if(time.hour != 0 || time.minute != 0 || time.second != 0) {
725            time.hour = 0;
726            time.minute = 0;
727            time.second = 0;
728            return true;
729        }
730        return false;
731    }
732
733    @VisibleForTesting
734    void upgradeToVersion203(SQLiteDatabase db) {
735        // Same as Gingerbread version 100
736        Cursor cursor = db.rawQuery("SELECT value from CalendarCache WHERE key=?",
737                new String[] {"timezoneDatabaseVersion"});
738
739        String oldTimezoneDbVersion = null;
740        if (cursor != null && cursor.moveToNext()) {
741            try {
742                oldTimezoneDbVersion = cursor.getString(0);
743            } finally {
744                cursor.close();
745            }
746        }
747        initCalendarCacheTable(db, oldTimezoneDbVersion);
748
749        // Same as Gingerbread version 101
750        updateCalendarCacheTableTo203(db);
751    }
752
753    @VisibleForTesting
754    void upgradeToVersion202(SQLiteDatabase db) {
755        // We will drop the "hidden" column from the calendar schema and add the "sync5" column
756        db.execSQL("ALTER TABLE " + Tables.CALENDARS +" RENAME TO " +
757                Tables.CALENDARS + "_Backup;");
758
759        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
760        createCalendarsTable(db);
761
762        // Populate the new Calendars table and put into the "sync5" column the value of the
763        // old "hidden" column
764        db.execSQL("INSERT INTO " + Tables.CALENDARS + " (" +
765                Calendar.Calendars._ID + ", " +
766                Calendar.Calendars._SYNC_ACCOUNT + ", " +
767                Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " +
768                Calendar.Calendars._SYNC_ID + ", " +
769                Calendar.Calendars._SYNC_VERSION + ", " +
770                Calendar.Calendars._SYNC_TIME + ", " +
771                Calendar.Calendars._SYNC_DATA + ", " +
772                Calendar.Calendars._SYNC_DIRTY + ", " +
773                Calendar.Calendars._SYNC_MARK + ", " +
774                Calendar.Calendars.NAME + ", " +
775                Calendar.Calendars.DISPLAY_NAME + ", " +
776                Calendar.Calendars.COLOR + ", " +
777                Calendar.Calendars.ACCESS_LEVEL + ", " +
778                Calendar.Calendars.SELECTED + ", " +
779                Calendar.Calendars.SYNC_EVENTS + ", " +
780                Calendar.Calendars.LOCATION + ", " +
781                Calendar.Calendars.TIMEZONE + ", " +
782                Calendar.Calendars.OWNER_ACCOUNT + ", " +
783                Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " +
784                Calendar.Calendars.DELETED + ", " +
785                Calendar.Calendars.SYNC1 + ", " +
786                Calendar.Calendars.SYNC2 + ", " +
787                Calendar.Calendars.SYNC3 + ", " +
788                Calendar.Calendars.SYNC4 + ", " +
789                Calendar.Calendars.SYNC5 + ") " +
790                "SELECT " +
791                Calendar.Calendars._ID + ", " +
792                Calendar.Calendars._SYNC_ACCOUNT + ", " +
793                Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " +
794                Calendar.Calendars._SYNC_ID + ", " +
795                Calendar.Calendars._SYNC_VERSION + ", " +
796                Calendar.Calendars._SYNC_TIME + ", " +
797                Calendar.Calendars._SYNC_DATA + ", " +
798                Calendar.Calendars._SYNC_DIRTY + ", " +
799                Calendar.Calendars._SYNC_MARK + ", " +
800                Calendar.Calendars.NAME + ", " +
801                Calendar.Calendars.DISPLAY_NAME + ", " +
802                Calendar.Calendars.COLOR + ", " +
803                Calendar.Calendars.ACCESS_LEVEL + ", " +
804                Calendar.Calendars.SELECTED + ", " +
805                Calendar.Calendars.SYNC_EVENTS + ", " +
806                Calendar.Calendars.LOCATION + ", " +
807                Calendar.Calendars.TIMEZONE + ", " +
808                Calendar.Calendars.OWNER_ACCOUNT + ", " +
809                Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " +
810                Calendar.Calendars.DELETED + ", " +
811                Calendar.Calendars.SYNC1 + ", " +
812                Calendar.Calendars.SYNC2 + ", " +
813                Calendar.Calendars.SYNC3 + ", " +
814                Calendar.Calendars.SYNC4 + " " +
815                "hidden" + " " +
816                "FROM " + Tables.CALENDARS + "_Backup" + ";"
817        );
818
819        // Drop the backup table
820        db.execSQL("DROP TABLE " + Tables.CALENDARS + "_Backup;");
821
822        // Recreate the Events Views as column "hidden" has been deleted
823        createEventsView(db);
824    }
825
826    @VisibleForTesting
827    void upgradeToVersion201(SQLiteDatabase db) {
828        db.execSQL("ALTER TABLE " + Tables.CALENDARS +
829                " ADD COLUMN " + Calendar.Calendars.SYNC4 + " TEXT;");
830    }
831
832    @VisibleForTesting
833    void upgradeToVersion200(SQLiteDatabase db) {
834        // we cannot use here a Calendar.Calendars,URL constant for "url" as we are trying to make
835        // it disappear so we are keeping the hardcoded name "url" in all the SQLs
836        db.execSQL("ALTER TABLE " + Tables.CALENDARS +" RENAME TO " +
837                Tables.CALENDARS + "_Backup;");
838
839        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
840        createCalendarsTable(db);
841
842        // Populate the new Calendars table except the SYNC2 / SYNC3 columns
843        db.execSQL("INSERT INTO " + Tables.CALENDARS + " (" +
844                Calendar.Calendars._ID + ", " +
845                Calendar.Calendars._SYNC_ACCOUNT + ", " +
846                Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " +
847                Calendar.Calendars._SYNC_ID + ", " +
848                Calendar.Calendars._SYNC_VERSION + ", " +
849                Calendar.Calendars._SYNC_TIME + ", " +
850                Calendar.Calendars._SYNC_DATA + ", " +
851                Calendar.Calendars._SYNC_DIRTY + ", " +
852                Calendar.Calendars._SYNC_MARK + ", " +
853                Calendar.Calendars.NAME + ", " +
854                Calendar.Calendars.DISPLAY_NAME + ", " +
855                Calendar.Calendars.COLOR + ", " +
856                Calendar.Calendars.ACCESS_LEVEL + ", " +
857                Calendar.Calendars.SELECTED + ", " +
858                Calendar.Calendars.SYNC_EVENTS + ", " +
859                Calendar.Calendars.LOCATION + ", " +
860                Calendar.Calendars.TIMEZONE + ", " +
861                Calendar.Calendars.OWNER_ACCOUNT + ", " +
862                Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " +
863                Calendar.Calendars.DELETED + ", " +
864                Calendar.Calendars.SYNC1 + ") " +
865                "SELECT " +
866                Calendar.Calendars._ID + ", " +
867                Calendar.Calendars._SYNC_ACCOUNT + ", " +
868                Calendar.Calendars._SYNC_ACCOUNT_TYPE + ", " +
869                Calendar.Calendars._SYNC_ID + ", " +
870                Calendar.Calendars._SYNC_VERSION + ", " +
871                Calendar.Calendars._SYNC_TIME + ", " +
872                Calendar.Calendars._SYNC_DATA + ", " +
873                Calendar.Calendars._SYNC_DIRTY + ", " +
874                Calendar.Calendars._SYNC_MARK + ", " +
875                Calendar.Calendars.NAME + ", " +
876                Calendar.Calendars.DISPLAY_NAME + ", " +
877                Calendar.Calendars.COLOR + ", " +
878                Calendar.Calendars.ACCESS_LEVEL + ", " +
879                Calendar.Calendars.SELECTED + ", " +
880                Calendar.Calendars.SYNC_EVENTS + ", " +
881                Calendar.Calendars.LOCATION + ", " +
882                Calendar.Calendars.TIMEZONE + ", " +
883                Calendar.Calendars.OWNER_ACCOUNT + ", " +
884                Calendar.Calendars.ORGANIZER_CAN_RESPOND + ", " +
885                "0" + ", " +
886                "url" + " " +
887                "FROM " + Tables.CALENDARS + "_Backup" + ";"
888        );
889
890        // Populate SYNC2 and SYNC3 columns - SYNC1 represent the old "url" column
891        // We will need to iterate over all the "com.google" type of calendars
892        String selectSql = "SELECT " + Calendar.Calendars._ID + ", " + "url" +
893                " FROM " + Tables.CALENDARS + "_Backup" +
894                " WHERE " + Calendar.Calendars._SYNC_ACCOUNT_TYPE  + "='com.google'" +
895                " AND url IS NOT NULL;";
896
897        String updateSql = "UPDATE " + Tables.CALENDARS + " SET " +
898                Calendar.Calendars.SYNC2 + "=?, " + // edit Url
899                Calendar.Calendars.SYNC3 + "=? " + // self Url
900                "WHERE " + Calendar.Calendars._ID + "=?;";
901
902        Cursor cursor = db.rawQuery(selectSql, null /* selection args */);
903        if (cursor != null && cursor.getCount() > 0) {
904            try {
905                Object[] bindArgs = new Object[3];
906
907                while (cursor.moveToNext()) {
908                    Long id = cursor.getLong(0);
909                    String url = cursor.getString(1);
910                    String selfUrl = getSelfUrlFromEventsUrl(url);
911                    String editUrl = getEditUrlFromEventsUrl(url);
912
913                    bindArgs[0] = editUrl;
914                    bindArgs[1] = selfUrl;
915                    bindArgs[2] = id;
916
917                    db.execSQL(updateSql, bindArgs);
918                }
919            } finally {
920                cursor.close();
921            }
922        }
923
924        // Drop the backup table
925        db.execSQL("DROP TABLE " + Tables.CALENDARS + "_Backup;");
926
927        // Recreate the Events Views as column "deleted" is now ambiguous
928        // ("deleted" is now defined in both Calendars and Events tables)
929        createEventsView(db);
930    }
931
932    @VisibleForTesting
933    static void upgradeToVersion69(SQLiteDatabase db) {
934        // Clean up allDay events which could be in an invalid state from an earlier version
935        // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This
936        // will go through the allDay events and make sure they have proper values and are in the
937        // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that
938        // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2
939        // and dtend2 are at midnight in their timezone.
940        final String sql = "SELECT " + Calendar.Events._ID + ", " +
941                Calendar.Events.DTSTART + ", " +
942                Calendar.Events.DTEND + ", " +
943                Calendar.Events.DURATION + ", " +
944                Calendar.Events.DTSTART2 + ", " +
945                Calendar.Events.DTEND2 + ", " +
946                Calendar.Events.EVENT_TIMEZONE + ", " +
947                Calendar.Events.EVENT_TIMEZONE2 + ", " +
948                Calendar.Events.RRULE + " " +
949                "FROM " + Tables.EVENTS + " " +
950                "WHERE " + Calendar.Events.ALL_DAY + "=?";
951        Cursor cursor = db.rawQuery(sql, new String[] {"1"});
952        if (cursor != null) {
953            try {
954                String timezone;
955                String timezone2;
956                String duration;
957                Long dtstart;
958                Long dtstart2;
959                Long dtend;
960                Long dtend2;
961                Time time = new Time();
962                Long id;
963                // some things need to be in utc so we call this frequently, cache to make faster
964                final String utc = Time.TIMEZONE_UTC;
965                while (cursor.moveToNext()) {
966                    String rrule = cursor.getString(8);
967                    id = cursor.getLong(0);
968                    dtstart = cursor.getLong(1);
969                    dtstart2 = null;
970                    timezone = cursor.getString(6);
971                    timezone2 = cursor.getString(7);
972                    duration = cursor.getString(3);
973
974                    if (TextUtils.isEmpty(rrule)) {
975                        // For non-recurring events dtstart and dtend should both have values
976                        // and duration should be null.
977                        dtend = cursor.getLong(2);
978                        dtend2 = null;
979                        // Since we made all three of these at the same time if timezone2 exists
980                        // so should dtstart2 and dtend2.
981                        if(!TextUtils.isEmpty(timezone2)) {
982                            dtstart2 = cursor.getLong(4);
983                            dtend2 = cursor.getLong(5);
984                        }
985
986                        boolean update = false;
987                        if (!TextUtils.equals(timezone, utc)) {
988                            update = true;
989                            timezone = utc;
990                        }
991
992                        time.clear(timezone);
993                        update |= fixAllDayTime(time, timezone, dtstart);
994                        dtstart = time.normalize(false);
995
996                        time.clear(timezone);
997                        update |= fixAllDayTime(time, timezone, dtend);
998                        dtend = time.normalize(false);
999
1000                        if (dtstart2 != null) {
1001                            time.clear(timezone2);
1002                            update |= fixAllDayTime(time, timezone2, dtstart2);
1003                            dtstart2 = time.normalize(false);
1004                        }
1005
1006                        if (dtend2 != null) {
1007                            time.clear(timezone2);
1008                            update |= fixAllDayTime(time, timezone2, dtend2);
1009                            dtend2 = time.normalize(false);
1010                        }
1011
1012                        if (!TextUtils.isEmpty(duration)) {
1013                            update = true;
1014                        }
1015
1016                        if (update) {
1017                            // enforce duration being null
1018                            db.execSQL("UPDATE " + Tables.EVENTS + " SET " +
1019                                    Calendar.Events.DTSTART + "=?, " +
1020                                    Calendar.Events.DTEND + "=?, " +
1021                                    Calendar.Events.DTSTART2 + "=?, " +
1022                                    Calendar.Events.DTEND2 + "=?, " +
1023                                    Calendar.Events.DURATION + "=?, " +
1024                                    Calendar.Events.EVENT_TIMEZONE + "=?, " +
1025                                    Calendar.Events.EVENT_TIMEZONE2 + "=? " +
1026                                    "WHERE " + Calendar.Events._ID + "=?",
1027                                    new Object[] {
1028                                            dtstart,
1029                                            dtend,
1030                                            dtstart2,
1031                                            dtend2,
1032                                            null,
1033                                            timezone,
1034                                            timezone2,
1035                                            id}
1036                            );
1037                        }
1038
1039                    } else {
1040                        // For recurring events only dtstart and duration should be used.
1041                        // We ignore dtend since it will be overwritten if the event changes to a
1042                        // non-recurring event and won't be used otherwise.
1043                        if(!TextUtils.isEmpty(timezone2)) {
1044                            dtstart2 = cursor.getLong(4);
1045                        }
1046
1047                        boolean update = false;
1048                        if (!TextUtils.equals(timezone, utc)) {
1049                            update = true;
1050                            timezone = utc;
1051                        }
1052
1053                        time.clear(timezone);
1054                        update |= fixAllDayTime(time, timezone, dtstart);
1055                        dtstart = time.normalize(false);
1056
1057                        if (dtstart2 != null) {
1058                            time.clear(timezone2);
1059                            update |= fixAllDayTime(time, timezone2, dtstart2);
1060                            dtstart2 = time.normalize(false);
1061                        }
1062
1063                        if (TextUtils.isEmpty(duration)) {
1064                            // If duration was missing assume a 1 day duration
1065                            duration = "P1D";
1066                            update = true;
1067                        } else {
1068                            int len = duration.length();
1069                            // TODO fix durations in other formats as well
1070                            if (duration.charAt(0) == 'P' &&
1071                                    duration.charAt(len - 1) == 'S') {
1072                                int seconds = Integer.parseInt(duration.substring(1, len - 1));
1073                                int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1074                                duration = "P" + days + "D";
1075                                update = true;
1076                            }
1077                        }
1078
1079                        if (update) {
1080                            // If there were other problems also enforce dtend being null
1081                            db.execSQL("UPDATE " + Tables.EVENTS + " SET " +
1082                                    Calendar.Events.DTSTART + "=?, " +
1083                                    Calendar.Events.DTEND + "=?, " +
1084                                    Calendar.Events.DTSTART2 + "=?, " +
1085                                    Calendar.Events.DTEND2 + "=?, " +
1086                                    Calendar.Events.DURATION + "=?," +
1087                                    Calendar.Events.EVENT_TIMEZONE + "=?, " +
1088                                    Calendar.Events.EVENT_TIMEZONE2 + "=? " +
1089                                    "WHERE " + Calendar.Events._ID + "=?",
1090                                    new Object[] {
1091                                            dtstart,
1092                                            null,
1093                                            dtstart2,
1094                                            null,
1095                                            duration,
1096                                            timezone,
1097                                            timezone2,
1098                                            id}
1099                            );
1100                        }
1101                    }
1102                }
1103            } finally {
1104                cursor.close();
1105            }
1106        }
1107    }
1108
1109    private void upgradeToVersion66(SQLiteDatabase db) {
1110        // Add a column to indicate whether the event organizer can respond to his own events
1111        // The UI should not show attendee status for events in calendars with this column = 0
1112        db.execSQL("ALTER TABLE " + Tables.CALENDARS +
1113                " ADD COLUMN " + Calendar.Calendars.ORGANIZER_CAN_RESPOND +
1114                    " INTEGER NOT NULL DEFAULT 1;");
1115    }
1116
1117    private void upgradeToVersion65(SQLiteDatabase db) {
1118        // we need to recreate the Events view
1119        createEventsView(db);
1120    }
1121
1122    private void upgradeToVersion64(SQLiteDatabase db) {
1123        // Add a column that may be used by sync adapters
1124        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1125                " ADD COLUMN " + Calendar.Events.SYNC_ADAPTER_DATA + " TEXT;");
1126    }
1127
1128    private void upgradeToVersion63(SQLiteDatabase db) {
1129        // we need to recreate the Events view
1130        createEventsView(db);
1131    }
1132
1133    private void upgradeToVersion62(SQLiteDatabase db) {
1134        // New columns are to transition to having allDay events in the local timezone
1135        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1136                " ADD COLUMN " + Calendar.Events.DTSTART2 + " INTEGER;");
1137        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1138                " ADD COLUMN " + Calendar.Events.DTEND2 + " INTEGER;");
1139        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1140                " ADD COLUMN " + Calendar.Events.EVENT_TIMEZONE2 + " TEXT;");
1141
1142        String[] allDayBit = new String[] {"0"};
1143        // Copy over all the data that isn't an all day event.
1144        db.execSQL("UPDATE " + Tables.EVENTS + " SET " +
1145                Calendar.Events.DTSTART2 + "=" + Calendar.Events.DTSTART + "," +
1146                Calendar.Events.DTEND2 + "=" + Calendar.Events.DTEND + "," +
1147                Calendar.Events.EVENT_TIMEZONE2 + "=" + Calendar.Events.EVENT_TIMEZONE + " " +
1148                "WHERE " + Calendar.Events.ALL_DAY + "=?;",
1149                allDayBit /* selection args */);
1150
1151        // "cursor" iterates over all the calendars
1152        allDayBit[0] = "1";
1153        Cursor cursor = db.rawQuery("SELECT " + Tables.EVENTS + "." + Calendar.Events._ID + "," +
1154                Calendar.Events.DTSTART + "," +
1155                Calendar.Events.DTEND + "," +
1156                Calendar.Events.EVENT_TIMEZONE + "," +
1157                Calendar.Calendars.TIMEZONE + " " +
1158                "FROM " + Tables.EVENTS + " INNER JOIN " + Tables.CALENDARS + " " +
1159                "WHERE " + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID + "=" +
1160                    Tables.CALENDARS + "." + Calendar.Calendars._ID +
1161                " AND "
1162                    + Calendar.Events.ALL_DAY + "=?",
1163                allDayBit /* selection args */);
1164
1165        Time oldTime = new Time();
1166        Time newTime = new Time();
1167        // Update the allday events in the new columns
1168        if (cursor != null) {
1169            try {
1170                String[] newData = new String[4];
1171                cursor.moveToPosition(-1);
1172                while (cursor.moveToNext()) {
1173                    long id = cursor.getLong(0); // Order from query above
1174                    long dtstart = cursor.getLong(1);
1175                    long dtend = cursor.getLong(2);
1176                    String eTz = cursor.getString(3); // current event timezone
1177                    String tz = cursor.getString(4); // Calendar timezone
1178                    //If there's no timezone for some reason use UTC by default.
1179                    if(eTz == null) {
1180                        eTz = Time.TIMEZONE_UTC;
1181                    }
1182
1183                    // Convert start time for all day events into the timezone of their calendar
1184                    oldTime.clear(eTz);
1185                    oldTime.set(dtstart);
1186                    newTime.clear(tz);
1187                    newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
1188                    newTime.normalize(false);
1189                    dtstart = newTime.toMillis(false /*ignoreDst*/);
1190
1191                    // Convert end time for all day events into the timezone of their calendar
1192                    oldTime.clear(eTz);
1193                    oldTime.set(dtend);
1194                    newTime.clear(tz);
1195                    newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
1196                    newTime.normalize(false);
1197                    dtend = newTime.toMillis(false /*ignoreDst*/);
1198
1199                    newData[0] = String.valueOf(dtstart);
1200                    newData[1] = String.valueOf(dtend);
1201                    newData[2] = tz;
1202                    newData[3] = String.valueOf(id);
1203                    db.execSQL("UPDATE " + Tables.EVENTS + " SET " +
1204                            Calendar.Events.DTSTART2 + "=?, " +
1205                            Calendar.Events.DTEND2 + "=?, " +
1206                            Calendar.Events.EVENT_TIMEZONE2 + "=? " +
1207                            "WHERE " + Calendar.Events._ID + "=?",
1208                            newData);
1209                }
1210            } finally {
1211                cursor.close();
1212            }
1213        }
1214    }
1215
1216    private void upgradeToVersion61(SQLiteDatabase db) {
1217        db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
1218
1219        // IF NOT EXISTS should be normal pattern for table creation
1220        db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" +
1221                "_id INTEGER PRIMARY KEY," +
1222                "key TEXT NOT NULL," +
1223                "value TEXT" +
1224                ");");
1225
1226        db.execSQL("INSERT INTO CalendarCache (key, value) VALUES (" +
1227                "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "',"  +
1228                "'" + CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION + "'" +
1229                ");");
1230    }
1231
1232    private void upgradeToVersion60(SQLiteDatabase db) {
1233        // Switch to CalendarProvider2
1234        upgradeSyncState(db);
1235        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1236        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
1237                "BEGIN " +
1238                CALENDAR_CLEANUP_TRIGGER_SQL +
1239                "END");
1240        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1241                " ADD COLUMN " + Calendar.Events.DELETED + " INTEGER NOT NULL DEFAULT 0;");
1242        db.execSQL("DROP TRIGGER IF EXISTS events_insert");
1243        // Trigger to set event's sync_account
1244        db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON " + Tables.EVENTS + " " +
1245                "BEGIN " +
1246                AFTER_EVENT_INSERT_SQL +
1247                "END");
1248        db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
1249        db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
1250        // Trigger to remove data tied to an event when we delete that event.
1251        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
1252                "BEGIN " +
1253                EVENTS_CLEANUP_TRIGGER_SQL +
1254                "END");
1255        db.execSQL("DROP TRIGGER IF EXISTS attendees_update");
1256        db.execSQL("DROP TRIGGER IF EXISTS attendees_insert");
1257        db.execSQL("DROP TRIGGER IF EXISTS attendees_delete");
1258        db.execSQL("DROP TRIGGER IF EXISTS reminders_update");
1259        db.execSQL("DROP TRIGGER IF EXISTS reminders_insert");
1260        db.execSQL("DROP TRIGGER IF EXISTS reminders_delete");
1261        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update");
1262        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert");
1263        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete");
1264
1265        createEventsView(db);
1266    }
1267
1268    private void upgradeToVersion59(SQLiteDatabase db) {
1269        db.execSQL("DROP TABLE IF EXISTS BusyBits;");
1270        db.execSQL("CREATE TEMPORARY TABLE " + Tables.CALENDAR_META_DATA + "_Backup" + "(" +
1271                Calendar.CalendarMetaData._ID + "," +
1272                Calendar.CalendarMetaData.LOCAL_TIMEZONE + "," +
1273                Calendar.CalendarMetaData.MIN_INSTANCE + "," +
1274                Calendar.CalendarMetaData.MAX_INSTANCE +
1275                ");");
1276        db.execSQL("INSERT INTO " + Tables.CALENDAR_META_DATA + "_Backup " +
1277                "SELECT " +
1278                Calendar.CalendarMetaData._ID + "," +
1279                Calendar.CalendarMetaData.LOCAL_TIMEZONE + "," +
1280                Calendar.CalendarMetaData.MIN_INSTANCE + "," +
1281                Calendar.CalendarMetaData.MAX_INSTANCE +
1282                " FROM " + Tables.CALENDAR_META_DATA + ";");
1283        db.execSQL("DROP TABLE " + Tables.CALENDAR_META_DATA + ";");
1284        createCalendarMetaDataTable(db);
1285        db.execSQL("INSERT INTO " + Tables.CALENDAR_META_DATA + " " +
1286                "SELECT " +
1287                Calendar.CalendarMetaData._ID + "," +
1288                Calendar.CalendarMetaData.LOCAL_TIMEZONE + "," +
1289                Calendar.CalendarMetaData.MIN_INSTANCE + "," +
1290                Calendar.CalendarMetaData.MAX_INSTANCE +
1291                " FROM " + Tables.CALENDAR_META_DATA + "_Backup;");
1292        db.execSQL("DROP TABLE " + Tables.CALENDAR_META_DATA + "_Backup;");
1293    }
1294
1295    private void upgradeToVersion57(SQLiteDatabase db) {
1296        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1297                " ADD COLUMN " + Calendar.Events.GUESTS_CAN_MODIFY +
1298                " INTEGER NOT NULL DEFAULT 0;");
1299        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1300                " ADD COLUMN " + Calendar.Events.GUESTS_CAN_INVITE_OTHERS +
1301                " INTEGER NOT NULL DEFAULT 1;");
1302        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1303                " ADD COLUMN " + Calendar.Events.GUESTS_CAN_SEE_GUESTS +
1304                " INTEGER NOT NULL DEFAULT 1;");
1305        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1306                " ADD COLUMN " + Calendar.Events.ORGANIZER +
1307                " STRING;");
1308        db.execSQL("UPDATE " + Tables.EVENTS + " SET " + Calendar.Events.ORGANIZER + "=" +
1309                "(SELECT " + Calendar.Attendees.ATTENDEE_EMAIL +
1310                " FROM " + Tables.ATTENDEES + ""  +
1311                " WHERE " +
1312                Tables.ATTENDEES + "." + Calendar.Attendees.EVENT_ID + "=" +
1313                Tables.EVENTS + "." + Calendar.Events._ID +
1314                " AND " +
1315                Tables.ATTENDEES + "." + Calendar.Attendees.ATTENDEE_RELATIONSHIP + "=2);");
1316    }
1317
1318    private void upgradeToVersion56(SQLiteDatabase db) {
1319        db.execSQL("ALTER TABLE " + Tables.CALENDARS +
1320                " ADD COLUMN " + Calendar.Calendars.OWNER_ACCOUNT + " TEXT;");
1321        db.execSQL("ALTER TABLE " + Tables.EVENTS +
1322                " ADD COLUMN " + Calendar.Events.HAS_ATTENDEE_DATA + " INTEGER;");
1323
1324        // Clear _sync_dirty to avoid a client-to-server sync that could blow away
1325        // server attendees.
1326        // Clear _sync_version to pull down the server's event (with attendees)
1327        // Change the URLs from full-selfattendance to full
1328        db.execSQL("UPDATE " + Tables.EVENTS
1329                + " SET " + Calendar.Events._SYNC_DIRTY + "=0, "
1330                + Calendar.Events._SYNC_VERSION + "=NULL, "
1331                + Calendar.Events._SYNC_ID + "="
1332                + "REPLACE(" + Calendar.Events._SYNC_ID + ", " +
1333                    "'/private/full-selfattendance', '/private/full'),"
1334                + Calendar.Events.COMMENTS_URI + "="
1335                + "REPLACE(" + Calendar.Events.COMMENTS_URI + ", " +
1336                    "'/private/full-selfattendance', '/private/full');");
1337
1338        db.execSQL("UPDATE " + Tables.CALENDARS
1339                + " SET " + "url="
1340                + "REPLACE(" + "url, " + "'/private/full-selfattendance', '/private/full');");
1341
1342        // "cursor" iterates over all the calendars
1343        Cursor cursor = db.rawQuery("SELECT " + Calendar.Calendars._ID + ", " +
1344                Calendar.Calendars.URL + " FROM " + Tables.CALENDARS,
1345                null /* selection args */);
1346        // Add the owner column.
1347        if (cursor != null) {
1348            try {
1349                final String updateSql = "UPDATE " + Tables.CALENDARS +
1350                        " SET " + Calendar.Calendars.OWNER_ACCOUNT + "=?" +
1351                        " WHERE " + Calendar.Calendars._ID + "=?";
1352                while (cursor.moveToNext()) {
1353                    Long id = cursor.getLong(0);
1354                    String url = cursor.getString(1);
1355                    String owner = calendarEmailAddressFromFeedUrl(url);
1356                    db.execSQL(updateSql, new Object[] {owner, id});
1357                }
1358            } finally {
1359                cursor.close();
1360            }
1361        }
1362    }
1363
1364    private void upgradeResync(SQLiteDatabase db) {
1365        // Delete sync state, so all records will be re-synced.
1366        db.execSQL("DELETE FROM " + Tables.SYNC_STATE + ";");
1367
1368        // "cursor" iterates over all the calendars
1369        Cursor cursor = db.rawQuery("SELECT " + Calendar.Calendars._SYNC_ACCOUNT + "," +
1370                Calendar.Calendars._SYNC_ACCOUNT_TYPE + ",url FROM " + Tables.CALENDARS,
1371                null /* selection args */);
1372        if (cursor != null) {
1373            try {
1374                while (cursor.moveToNext()) {
1375                    String accountName = cursor.getString(0);
1376                    String accountType = cursor.getString(1);
1377                    final Account account = new Account(accountName, accountType);
1378                    String calendarUrl = cursor.getString(2);
1379                    scheduleSync(account, false /* two-way sync */, calendarUrl);
1380                }
1381            } finally {
1382                cursor.close();
1383            }
1384        }
1385    }
1386
1387    private void upgradeToVersion55(SQLiteDatabase db) {
1388        db.execSQL("ALTER TABLE " + Tables.CALENDARS + " ADD COLUMN " +
1389                Calendar.Calendars._SYNC_ACCOUNT_TYPE + " TEXT;");
1390        db.execSQL("ALTER TABLE " + Tables.EVENTS + " ADD COLUMN " +
1391                Calendar.Events._SYNC_ACCOUNT_TYPE + " TEXT;");
1392        db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
1393        db.execSQL("UPDATE " + Tables.CALENDARS
1394                + " SET " + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "='com.google'"
1395                + " WHERE " + Calendar.Calendars._SYNC_ACCOUNT + " IS NOT NULL");
1396        db.execSQL("UPDATE " + Tables.EVENTS
1397                + " SET " + Calendar.Events._SYNC_ACCOUNT_TYPE + "='com.google'"
1398                + " WHERE " + Calendar.Events._SYNC_ACCOUNT + " IS NOT NULL");
1399        db.execSQL("UPDATE DeletedEvents"
1400                + " SET _sync_account_type='com.google'"
1401                + " WHERE _sync_account IS NOT NULL");
1402        Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
1403        db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
1404        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON " + Tables.EVENTS + " ("
1405                + Calendar.Events._SYNC_ACCOUNT_TYPE + ", "
1406                + Calendar.Events._SYNC_ACCOUNT + ", "
1407                + Calendar.Events._SYNC_ID + ");");
1408    }
1409
1410    private void upgradeToVersion54(SQLiteDatabase db) {
1411        Log.w(TAG, "adding eventSyncAccountAndIdIndex");
1412        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
1413                + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");");
1414    }
1415
1416    private void upgradeToVersion53(SQLiteDatabase db) {
1417        Log.w(TAG, "Upgrading CalendarAlerts table");
1418        db.execSQL("ALTER TABLE " + Tables.CALENDAR_ALERTS + " ADD COLUMN " +
1419                Calendar.CalendarAlerts.CREATION_TIME + " INTEGER DEFAULT 0;");
1420        db.execSQL("ALTER TABLE " + Tables.CALENDAR_ALERTS + " ADD COLUMN " +
1421                Calendar.CalendarAlerts.RECEIVED_TIME + " INTEGER DEFAULT 0;");
1422        db.execSQL("ALTER TABLE " + Tables.CALENDAR_ALERTS + " ADD COLUMN " +
1423                Calendar.CalendarAlerts.NOTIFY_TIME + " INTEGER DEFAULT 0;");
1424    }
1425
1426    private void upgradeToVersion52(SQLiteDatabase db) {
1427        // We added "originalAllDay" to the Events table to keep track of
1428        // the allDay status of the original recurring event for entries
1429        // that are exceptions to that recurring event.  We need this so
1430        // that we can format the date correctly for the "originalInstanceTime"
1431        // column when we make a change to the recurrence exception and
1432        // send it to the server.
1433        db.execSQL("ALTER TABLE " + Tables.EVENTS + " ADD COLUMN " +
1434                Calendar.Events.ORIGINAL_ALL_DAY + " INTEGER;");
1435
1436        // Iterate through the Events table and for each recurrence
1437        // exception, fill in the correct value for "originalAllDay",
1438        // if possible.  The only times where this might not be possible
1439        // are (1) the original recurring event no longer exists, or
1440        // (2) the original recurring event does not yet have a _sync_id
1441        // because it was created on the phone and hasn't been synced to the
1442        // server yet.  In both cases the originalAllDay field will be set
1443        // to null.  In the first case we don't care because the recurrence
1444        // exception will not be displayed and we won't be able to make
1445        // any changes to it (and even if we did, the server should ignore
1446        // them, right?).  In the second case, the calendar client already
1447        // disallows making changes to an instance of a recurring event
1448        // until the recurring event has been synced to the server so the
1449        // second case should never occur.
1450
1451        // "cursor" iterates over all the recurrences exceptions.
1452        Cursor cursor = db.rawQuery("SELECT " + Calendar.Events._ID + "," +
1453                Calendar.Events.ORIGINAL_EVENT +
1454                " FROM " + Tables.EVENTS +
1455                " WHERE " + Calendar.Events.ORIGINAL_EVENT + " IS NOT NULL",
1456                null /* selection args */);
1457        if (cursor != null) {
1458            try {
1459                while (cursor.moveToNext()) {
1460                    long id = cursor.getLong(0);
1461                    String originalEvent = cursor.getString(1);
1462
1463                    // Find the original recurring event (if it exists)
1464                    Cursor recur = db.rawQuery("SELECT " + Calendar.Events.ALL_DAY +
1465                            " FROM " + Tables.EVENTS +
1466                            " WHERE " + Calendar.Events._SYNC_ID + "=?",
1467                            new String[] {originalEvent});
1468                    if (recur == null) {
1469                        continue;
1470                    }
1471
1472                    try {
1473                        // Fill in the "originalAllDay" field of the
1474                        // recurrence exception with the "allDay" value
1475                        // from the recurring event.
1476                        if (recur.moveToNext()) {
1477                            int allDay = recur.getInt(0);
1478                            db.execSQL("UPDATE " + Tables.EVENTS +
1479                                    " SET " + Calendar.Events.ORIGINAL_ALL_DAY + "=" + allDay +
1480                                    " WHERE " + Calendar.Events._ID + "="+id);
1481                        }
1482                    } finally {
1483                        recur.close();
1484                    }
1485                }
1486            } finally {
1487                cursor.close();
1488            }
1489        }
1490    }
1491
1492    private void upgradeToVersion51(SQLiteDatabase db) {
1493        Log.w(TAG, "Upgrading DeletedEvents table");
1494
1495        // We don't have enough information to fill in the correct
1496        // value of the calendar_id for old rows in the DeletedEvents
1497        // table, but rows in that table are transient so it is unlikely
1498        // that there are any rows.  Plus, the calendar_id is used only
1499        // when deleting a calendar, which is a rare event.  All new rows
1500        // will have the correct calendar_id.
1501        db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
1502
1503        // Trigger to remove a calendar's events when we delete the calendar
1504        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1505        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
1506                "BEGIN " +
1507                "DELETE FROM " + Tables.EVENTS + " WHERE " + Calendar.Events.CALENDAR_ID + "=" +
1508                    "old." + Calendar.Events._ID + ";" +
1509                "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
1510                "END");
1511        db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
1512    }
1513
1514    private void dropTables(SQLiteDatabase db) {
1515        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDARS + ";");
1516        db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS + ";");
1517        db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS_RAW_TIMES + ";");
1518        db.execSQL("DROP TABLE IF EXISTS " + Tables.INSTANCES + ";");
1519        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_META_DATA + ";");
1520        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";");
1521        db.execSQL("DROP TABLE IF EXISTS " + Tables.ATTENDEES + ";");
1522        db.execSQL("DROP TABLE IF EXISTS " + Tables.REMINDERS + ";");
1523        db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_ALERTS + ";");
1524        db.execSQL("DROP TABLE IF EXISTS " + Tables.EXTENDED_PROPERTIES + ";");
1525    }
1526
1527    @Override
1528    public synchronized SQLiteDatabase getWritableDatabase() {
1529        SQLiteDatabase db = super.getWritableDatabase();
1530        return db;
1531    }
1532
1533    public SyncStateContentProviderHelper getSyncState() {
1534        return mSyncState;
1535    }
1536
1537    /**
1538     * Schedule a calendar sync for the account.
1539     * @param account the account for which to schedule a sync
1540     * @param uploadChangesOnly if set, specify that the sync should only send
1541     *   up local changes.  This is typically used for a local sync, a user override of
1542     *   too many deletions, or a sync after a calendar is unselected.
1543     * @param url the url feed for the calendar to sync (may be null, in which case a poll of
1544     *   all feeds is done.)
1545     */
1546    void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
1547        Bundle extras = new Bundle();
1548        if (uploadChangesOnly) {
1549            extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
1550        }
1551        if (url != null) {
1552            extras.putString("feed", url);
1553            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
1554        }
1555        ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras);
1556    }
1557
1558    private static void createEventsView(SQLiteDatabase db) {
1559        db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";");
1560        String eventsSelect = "SELECT "
1561                + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + ","
1562                + Calendar.Events.HTML_URI + ","
1563                + Calendar.Events.TITLE + ","
1564                + Calendar.Events.DESCRIPTION + ","
1565                + Calendar.Events.EVENT_LOCATION + ","
1566                + Calendar.Events.STATUS + ","
1567                + Calendar.Events.SELF_ATTENDEE_STATUS + ","
1568                + Calendar.Events.COMMENTS_URI + ","
1569                + Calendar.Events.DTSTART + ","
1570                + Calendar.Events.DTEND + ","
1571                + Calendar.Events.DURATION + ","
1572                + Calendar.Events.EVENT_TIMEZONE + ","
1573                + Calendar.Events.ALL_DAY + ","
1574                + Calendar.Events.VISIBILITY + ","
1575                + Calendar.Calendars.TIMEZONE + ","
1576                + Calendar.Calendars.SELECTED + ","
1577                + Calendar.Calendars.ACCESS_LEVEL + ","
1578                + Calendar.Events.TRANSPARENCY + ","
1579                + Calendar.Calendars.COLOR + ","
1580                + Calendar.Events.HAS_ALARM + ","
1581                + Calendar.Events.HAS_EXTENDED_PROPERTIES + ","
1582                + Calendar.Events.RRULE + ","
1583                + Calendar.Events.RDATE + ","
1584                + Calendar.Events.EXRULE + ","
1585                + Calendar.Events.EXDATE + ","
1586                + Calendar.Events.ORIGINAL_EVENT + ","
1587                + Calendar.Events.ORIGINAL_INSTANCE_TIME + ","
1588                + Calendar.Events.ORIGINAL_ALL_DAY + ","
1589                + Calendar.Events.LAST_DATE + ","
1590                + Calendar.Events.HAS_ATTENDEE_DATA + ","
1591                + Calendar.Events.CALENDAR_ID + ","
1592                + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + ","
1593                + Calendar.Events.GUESTS_CAN_MODIFY + ","
1594                + Calendar.Events.GUESTS_CAN_SEE_GUESTS + ","
1595                + Calendar.Events.ORGANIZER + ","
1596                + Tables.EVENTS + "." + Calendar.Events.DELETED
1597                + " AS " + Calendar.Events.DELETED + ","
1598                + Tables.EVENTS + "." + Calendar.Events._SYNC_ID
1599                + " AS " + Calendar.Events._SYNC_ID + ","
1600                + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION
1601                + " AS " + Calendar.Events._SYNC_VERSION + ","
1602                + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY
1603                + " AS " + Calendar.Events._SYNC_DIRTY + ","
1604                + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT
1605                + " AS " + Calendar.Events._SYNC_ACCOUNT + ","
1606                + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE
1607                + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + ","
1608                + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME
1609                + " AS " + Calendar.Events._SYNC_TIME + ","
1610                + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA
1611                + " AS " + Calendar.Events._SYNC_DATA + ","
1612                + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK
1613                + " AS " + Calendar.Events._SYNC_MARK + ","
1614                + Calendar.Calendars.SYNC1 + ","
1615                + Calendar.Calendars.OWNER_ACCOUNT + ","
1616                + Calendar.Calendars.SYNC_EVENTS
1617                + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS
1618                + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID
1619                + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID
1620                + ")";
1621
1622        db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect);
1623    }
1624
1625    /**
1626     * Extracts the calendar email from a calendar feed url.
1627     * @param feed the calendar feed url
1628     * @return the calendar email that is in the feed url or null if it can't
1629     * find the email address.
1630     * TODO: this is duplicated in CalendarSyncAdapter; move to a library
1631     */
1632    public static String calendarEmailAddressFromFeedUrl(String feed) {
1633        // Example feed url:
1634        // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
1635        String[] pathComponents = feed.split("/");
1636        if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
1637            try {
1638                return URLDecoder.decode(pathComponents[5], "UTF-8");
1639            } catch (UnsupportedEncodingException e) {
1640                Log.e(TAG, "unable to url decode the email address in calendar " + feed);
1641                return null;
1642            }
1643        }
1644
1645        Log.e(TAG, "unable to find the email address in calendar " + feed);
1646        return null;
1647    }
1648
1649    /**
1650     * Get a "allcalendars" url from a "private/full" or "private/free-busy" url
1651     * @param url
1652     * @return the rewritten Url
1653     *
1654     * For example:
1655     *
1656     *      http://www.google.com/calendar/feeds/joe%40joe.com/private/full
1657     *      http://www.google.com/calendar/feeds/joe%40joe.com/private/free-busy
1658     *
1659     * will be rewriten into:
1660     *
1661     *      http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com
1662     *      http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com
1663     */
1664    @VisibleForTesting
1665    private static String getAllCalendarsUrlFromEventsUrl(String url) {
1666        if (url == null) {
1667            if (Log.isLoggable(TAG, Log.DEBUG)) {
1668                Log.d(TAG, "Cannot get AllCalendars url from a NULL url");
1669            }
1670            return null;
1671        }
1672        if (url.contains("/private/full")) {
1673            return url.replace("/private/full", "").
1674                    replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full");
1675        }
1676        if (url.contains("/private/free-busy")) {
1677            return url.replace("/private/free-busy", "").
1678                    replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full");
1679        }
1680        // Just log as we dont recognize the provided Url
1681        if (Log.isLoggable(TAG, Log.DEBUG)) {
1682            Log.d(TAG, "Cannot get AllCalendars url from the following url: " + url);
1683        }
1684        return null;
1685    }
1686
1687    /**
1688     * Get "selfUrl" from "events url"
1689     * @param url the Events url (either "private/full" or "private/free-busy"
1690     * @return the corresponding allcalendar url
1691     */
1692    private static String getSelfUrlFromEventsUrl(String url) {
1693        return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url));
1694    }
1695
1696    /**
1697     * Get "editUrl" from "events url"
1698     * @param url the Events url (either "private/full" or "private/free-busy"
1699     * @return the corresponding allcalendar url
1700     */
1701    private static String getEditUrlFromEventsUrl(String url) {
1702        return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url));
1703    }
1704
1705    /**
1706     * Rewrite the url from "http" to "https" scheme
1707     * @param url the url to rewrite
1708     * @return the rewritten URL
1709     */
1710    private static String rewriteUrlFromHttpToHttps(String url) {
1711        if (url == null) {
1712            if (Log.isLoggable(TAG, Log.DEBUG)) {
1713                Log.d(TAG, "Cannot rewrite a NULL url");
1714            }
1715            return null;
1716        }
1717        if (url.startsWith(SCHEMA_HTTPS)) {
1718            return url;
1719        }
1720        if (!url.startsWith(SCHEMA_HTTP)) {
1721            throw new IllegalArgumentException("invalid url parameter, unknown scheme: " + url);
1722        }
1723        return SCHEMA_HTTPS + url.substring(SCHEMA_HTTP.length());
1724    }
1725}
1726