CalendarDatabaseHelper.java revision 68040cf2602bb15bb4cf1072cf99132118d5a805
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    private static final String TAG = "CalendarDatabaseHelper";
51
52    private static final String DATABASE_NAME = "calendar.db";
53
54    private static final int DAY_IN_SECONDS = 24 * 60 * 60;
55
56    // TODO: change the Calendar contract so these are defined there.
57    static final String ACCOUNT_NAME = "_sync_account";
58    static final String ACCOUNT_TYPE = "_sync_account_type";
59
60    // Note: if you update the version number, you must also update the code
61    // in upgradeDatabase() to modify the database (gracefully, if possible).
62    static final int DATABASE_VERSION = 101;
63
64    private static final int PRE_FROYO_SYNC_STATE_VERSION = 3;
65
66    // Copied from SyncStateContentProviderHelper.  Don't really want to make them public there.
67    private static final String SYNC_STATE_TABLE = "_sync_state";
68    private static final String SYNC_STATE_META_TABLE = "_sync_state_metadata";
69    private static final String SYNC_STATE_META_VERSION_COLUMN = "version";
70
71    private final Context mContext;
72    private final SyncStateContentProviderHelper mSyncState;
73
74    private static CalendarDatabaseHelper sSingleton = null;
75
76    private DatabaseUtils.InsertHelper mCalendarsInserter;
77    private DatabaseUtils.InsertHelper mEventsInserter;
78    private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
79    private DatabaseUtils.InsertHelper mInstancesInserter;
80    private DatabaseUtils.InsertHelper mAttendeesInserter;
81    private DatabaseUtils.InsertHelper mRemindersInserter;
82    private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
83    private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
84
85    public long calendarsInsert(ContentValues values) {
86        return mCalendarsInserter.insert(values);
87    }
88
89    public long eventsInsert(ContentValues values) {
90        return mEventsInserter.insert(values);
91    }
92
93    public long eventsRawTimesInsert(ContentValues values) {
94        return mEventsRawTimesInserter.insert(values);
95    }
96
97    public long eventsRawTimesReplace(ContentValues values) {
98        return mEventsRawTimesInserter.replace(values);
99    }
100
101    public long instancesInsert(ContentValues values) {
102        return mInstancesInserter.insert(values);
103    }
104
105    public long instancesReplace(ContentValues values) {
106        return mInstancesInserter.replace(values);
107    }
108
109    public long attendeesInsert(ContentValues values) {
110        return mAttendeesInserter.insert(values);
111    }
112
113    public long remindersInsert(ContentValues values) {
114        return mRemindersInserter.insert(values);
115    }
116
117    public long calendarAlertsInsert(ContentValues values) {
118        return mCalendarAlertsInserter.insert(values);
119    }
120
121    public long extendedPropertiesInsert(ContentValues values) {
122        return mExtendedPropertiesInserter.insert(values);
123    }
124
125    public static synchronized CalendarDatabaseHelper getInstance(Context context) {
126        if (sSingleton == null) {
127            sSingleton = new CalendarDatabaseHelper(context);
128        }
129        return sSingleton;
130    }
131
132    /**
133     * Private constructor, callers except unit tests should obtain an instance through
134     * {@link #getInstance(android.content.Context)} instead.
135     */
136    /* package */ CalendarDatabaseHelper(Context context) {
137        super(context, DATABASE_NAME, null, DATABASE_VERSION);
138        if (false) Log.i(TAG, "Creating OpenHelper");
139        Resources resources = context.getResources();
140
141        mContext = context;
142        mSyncState = new SyncStateContentProviderHelper();
143    }
144
145    @Override
146    public void onOpen(SQLiteDatabase db) {
147        mSyncState.onDatabaseOpened(db);
148
149        mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars");
150        mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events");
151        mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes");
152        mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances");
153        mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees");
154        mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders");
155        mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts");
156        mExtendedPropertiesInserter =
157                new DatabaseUtils.InsertHelper(db, "ExtendedProperties");
158    }
159
160    /*
161     * Upgrade sync state table if necessary.  Note that the data bundle
162     * in the table is not upgraded.
163     *
164     * The sync state used to be stored with version 3, but now uses the
165     * same sync state code as contacts, which is version 1.  This code
166     * upgrades from 3 to 1 if necessary.  (Yes, the numbers are unfortunately
167     * backwards.)
168     *
169     * This code is only called when upgrading from an old calendar version,
170     * so there is no problem if sync state version 3 gets used again in the
171     * future.
172     */
173    private void upgradeSyncState(SQLiteDatabase db) {
174        long version = DatabaseUtils.longForQuery(db,
175                 "SELECT " + SYNC_STATE_META_VERSION_COLUMN
176                 + " FROM " + SYNC_STATE_META_TABLE,
177                 null);
178        if (version == PRE_FROYO_SYNC_STATE_VERSION) {
179            Log.i(TAG, "Upgrading calendar sync state table");
180            db.execSQL("CREATE TEMPORARY TABLE state_backup(_sync_account TEXT, "
181                    + "_sync_account_type TEXT, data TEXT);");
182            db.execSQL("INSERT INTO state_backup SELECT _sync_account, _sync_account_type, data"
183                    + " FROM "
184                    + SYNC_STATE_TABLE
185                    + " WHERE _sync_account is not NULL and _sync_account_type is not NULL;");
186            db.execSQL("DROP TABLE " + SYNC_STATE_TABLE + ";");
187            mSyncState.onDatabaseOpened(db);
188            db.execSQL("INSERT INTO " + SYNC_STATE_TABLE + "("
189                    + SyncStateContract.Columns.ACCOUNT_NAME + ","
190                    + SyncStateContract.Columns.ACCOUNT_TYPE + ","
191                    + SyncStateContract.Columns.DATA
192                    + ") SELECT _sync_account, _sync_account_type, data from state_backup;");
193            db.execSQL("DROP TABLE state_backup;");
194        } else {
195            // Wrong version to upgrade.
196            // Don't need to do anything more here because mSyncState.onDatabaseOpened() will blow
197            // away and recreate  the database (which will result in a resync).
198            Log.w(TAG, "upgradeSyncState: current version is " + version + ", skipping upgrade.");
199        }
200    }
201
202    @Override
203    public void onCreate(SQLiteDatabase db) {
204        bootstrapDB(db);
205    }
206
207    private void bootstrapDB(SQLiteDatabase db) {
208        Log.i(TAG, "Bootstrapping database");
209
210        mSyncState.createDatabase(db);
211
212        db.execSQL("CREATE TABLE Calendars (" +
213                "_id INTEGER PRIMARY KEY," +
214                ACCOUNT_NAME + " TEXT," +
215                ACCOUNT_TYPE + " TEXT," +
216                "_sync_id TEXT," +
217                "_sync_version TEXT," +
218                "_sync_time TEXT," +            // UTC
219                "_sync_local_id INTEGER," +
220                "_sync_dirty INTEGER," +
221                "_sync_mark INTEGER," + // Used to filter out new rows
222                "url TEXT," +
223                "name TEXT," +
224                "displayName TEXT," +
225                "hidden INTEGER NOT NULL DEFAULT 0," +
226                "color INTEGER," +
227                "access_level INTEGER," +
228                "selected INTEGER NOT NULL DEFAULT 1," +
229                "sync_events INTEGER NOT NULL DEFAULT 0," +
230                "location TEXT," +
231                "timezone TEXT," +
232                "ownerAccount TEXT, " +
233                "organizerCanRespond INTEGER NOT NULL DEFAULT 1" +
234                ");");
235
236        // Trigger to remove a calendar's events when we delete the calendar
237        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
238                "BEGIN " +
239                "DELETE FROM Events WHERE calendar_id = old._id;" +
240                "END");
241
242        // TODO: do we need both dtend and duration?
243        db.execSQL("CREATE TABLE Events (" +
244                "_id INTEGER PRIMARY KEY," +
245                ACCOUNT_NAME + " TEXT," +
246                ACCOUNT_TYPE + " TEXT," +
247                "_sync_id TEXT," +
248                "_sync_version TEXT," +
249                "_sync_time TEXT," +            // UTC
250                "_sync_local_id INTEGER," +
251                "_sync_dirty INTEGER," +
252                "_sync_mark INTEGER," + // To filter out new rows
253                "calendar_id INTEGER NOT NULL," +
254                "htmlUri TEXT," +
255                "title TEXT," +
256                "eventLocation TEXT," +
257                "description TEXT," +
258                "eventStatus INTEGER," +
259                "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
260                "commentsUri TEXT," +
261                "dtstart INTEGER," +               // millis since epoch
262                "dtend INTEGER," +                 // millis since epoch
263                "eventTimezone TEXT," +         // timezone for event
264                "duration TEXT," +
265                "allDay INTEGER NOT NULL DEFAULT 0," +
266                "visibility INTEGER NOT NULL DEFAULT 0," +
267                "transparency INTEGER NOT NULL DEFAULT 0," +
268                "hasAlarm INTEGER NOT NULL DEFAULT 0," +
269                "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
270                "rrule TEXT," +
271                "rdate TEXT," +
272                "exrule TEXT," +
273                "exdate TEXT," +
274                "originalEvent TEXT," +  // _sync_id of recurring event
275                "originalInstanceTime INTEGER," +  // millis since epoch
276                "originalAllDay INTEGER," +
277                "lastDate INTEGER," +               // millis since epoch
278                "hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
279                "guestsCanModify INTEGER NOT NULL DEFAULT 0," +
280                "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
281                "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
282                "organizer STRING," +
283                "deleted INTEGER NOT NULL DEFAULT 0," +
284                "dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone
285                "dtend2 INTEGER," + //millis since epoch, allDay events in local timezone
286                "eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone
287                "syncAdapterData TEXT" + //available for use by sync adapters
288                ");");
289
290        // Trigger to set event's sync_account
291        db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
292                "BEGIN " +
293                "UPDATE Events SET _sync_account=" +
294                "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," +
295                "_sync_account_type=" +
296                "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " +
297                "WHERE Events._id=new._id;" +
298                "END");
299
300        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
301                + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " + Calendar.Events._SYNC_ACCOUNT + ", "
302                + Calendar.Events._SYNC_ID + ");");
303
304        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
305                Calendar.Events.CALENDAR_ID +
306                ");");
307
308        db.execSQL("CREATE TABLE EventsRawTimes (" +
309                "_id INTEGER PRIMARY KEY," +
310                "event_id INTEGER NOT NULL," +
311                "dtstart2445 TEXT," +
312                "dtend2445 TEXT," +
313                "originalInstanceTime2445 TEXT," +
314                "lastDate2445 TEXT," +
315                "UNIQUE (event_id)" +
316                ");");
317
318        db.execSQL("CREATE TABLE Instances (" +
319                "_id INTEGER PRIMARY KEY," +
320                "event_id INTEGER," +
321                "begin INTEGER," +         // UTC millis
322                "end INTEGER," +           // UTC millis
323                "startDay INTEGER," +      // Julian start day
324                "endDay INTEGER," +        // Julian end day
325                "startMinute INTEGER," +   // minutes from midnight
326                "endMinute INTEGER," +     // minutes from midnight
327                "UNIQUE (event_id, begin, end)" +
328                ");");
329
330        db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" +
331                Calendar.Instances.START_DAY +
332                ");");
333
334        createCalendarMetaDataTable(db);
335
336        createCalendarCacheTable(db, null);
337
338        db.execSQL("CREATE TABLE Attendees (" +
339                "_id INTEGER PRIMARY KEY," +
340                "event_id INTEGER," +
341                "attendeeName TEXT," +
342                "attendeeEmail TEXT," +
343                "attendeeStatus INTEGER," +
344                "attendeeRelationship INTEGER," +
345                "attendeeType INTEGER" +
346                ");");
347
348        db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" +
349                Calendar.Attendees.EVENT_ID +
350                ");");
351
352        db.execSQL("CREATE TABLE Reminders (" +
353                "_id INTEGER PRIMARY KEY," +
354                "event_id INTEGER," +
355                "minutes INTEGER," +
356                "method INTEGER NOT NULL" +
357                " DEFAULT " + Calendar.Reminders.METHOD_DEFAULT +
358                ");");
359
360        db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" +
361                Calendar.Reminders.EVENT_ID +
362                ");");
363
364         // This table stores the Calendar notifications that have gone off.
365        db.execSQL("CREATE TABLE CalendarAlerts (" +
366                "_id INTEGER PRIMARY KEY," +
367                "event_id INTEGER," +
368                "begin INTEGER NOT NULL," +         // UTC millis
369                "end INTEGER NOT NULL," +           // UTC millis
370                "alarmTime INTEGER NOT NULL," +     // UTC millis
371                "creationTime INTEGER NOT NULL," +  // UTC millis
372                "receivedTime INTEGER NOT NULL," +  // UTC millis
373                "notifyTime INTEGER NOT NULL," +    // UTC millis
374                "state INTEGER NOT NULL," +
375                "minutes INTEGER," +
376                "UNIQUE (alarmTime, begin, event_id)" +
377                ");");
378
379        db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" +
380                Calendar.CalendarAlerts.EVENT_ID +
381                ");");
382
383        db.execSQL("CREATE TABLE ExtendedProperties (" +
384                "_id INTEGER PRIMARY KEY," +
385                "event_id INTEGER," +
386                "name TEXT," +
387                "value TEXT" +
388                ");");
389
390        db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" +
391                Calendar.ExtendedProperties.EVENT_ID +
392                ");");
393
394        // Trigger to remove data tied to an event when we delete that event.
395        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
396                "BEGIN " +
397                "DELETE FROM Instances WHERE event_id = old._id;" +
398                "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
399                "DELETE FROM Attendees WHERE event_id = old._id;" +
400                "DELETE FROM Reminders WHERE event_id = old._id;" +
401                "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
402                "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
403                "END");
404
405        createEventsView(db);
406
407        ContentResolver.requestSync(null /* all accounts */,
408                ContactsContract.AUTHORITY, new Bundle());
409    }
410
411    private void createCalendarMetaDataTable(SQLiteDatabase db) {
412        db.execSQL("CREATE TABLE CalendarMetaData (" +
413                "_id INTEGER PRIMARY KEY," +
414                "localTimezone TEXT," +
415                "minInstance INTEGER," +      // UTC millis
416                "maxInstance INTEGER" +       // UTC millis
417                ");");
418    }
419
420    private void createCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) {
421        // This is a hack because versioning skipped version number 61 of schema
422        // TODO after version 70 this can be removed
423        db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
424
425        // IF NOT EXISTS should be normal pattern for table creation
426        db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" +
427                "_id INTEGER PRIMARY KEY," +
428                "key TEXT NOT NULL," +
429                "value TEXT" +
430                ");");
431
432        initCalendarCacheTable(db, oldTimezoneDbVersion);
433        updateCalendarCacheTableTo101(db);
434    }
435
436    private void initCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) {
437        String timezoneDbVersion = (oldTimezoneDbVersion != null) ?
438                oldTimezoneDbVersion : CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION;
439
440        // Set the default timezone database version
441        db.execSQL("INSERT OR REPLACE INTO CalendarCache (_id, key, value) VALUES (" +
442                CalendarCache.KEY_TIMEZONE_DATABASE_VERSION.hashCode() + "," +
443                "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "',"  +
444                "'" + timezoneDbVersion + "'" +
445                ");");
446    }
447
448    private void updateCalendarCacheTableTo101(SQLiteDatabase db) {
449        // Define the default timezone type for Instances timezone management
450        db.execSQL("INSERT INTO CalendarCache (_id, key, value) VALUES (" +
451                CalendarCache.KEY_TIMEZONE_TYPE.hashCode() + "," +
452                "'" + CalendarCache.KEY_TIMEZONE_TYPE + "',"  +
453                "'" + CalendarCache.TIMEZONE_TYPE_AUTO + "'" +
454                ");");
455
456        String defaultTimezone = TimeZone.getDefault().getID();
457
458        // Define the default timezone for Instances
459        db.execSQL("INSERT INTO CalendarCache (_id, key, value) VALUES (" +
460                CalendarCache.KEY_TIMEZONE_INSTANCES.hashCode() + "," +
461                "'" + CalendarCache.KEY_TIMEZONE_INSTANCES + "',"  +
462                "'" + defaultTimezone + "'" +
463                ");");
464
465        // Define the default previous timezone for Instances
466        db.execSQL("INSERT INTO CalendarCache (_id, key, value) VALUES (" +
467                CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS.hashCode() + "," +
468                "'" + CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + "',"  +
469                "'" + defaultTimezone + "'" +
470                ");");
471    }
472
473    @Override
474    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
475        Log.i(TAG, "Upgrading DB from version " + oldVersion
476                + " to " + newVersion);
477        if (oldVersion < 49) {
478            dropTables(db);
479            mSyncState.createDatabase(db);
480            return; // this was lossy
481        }
482
483        // From schema versions 59 to version 66, the CalendarMetaData table definition had lost
484        // the primary key leading to having the CalendarMetaData with multiple rows instead of
485        // only one. The Instance table was then corrupted (during Instance expansion we are using
486        // the localTimezone, minInstance and maxInstance from CalendarMetaData table.
487        // This boolean helps us tracking the need to recreate the CalendarMetaData table and
488        // clear the Instance table (and thus force an Instance expansion).
489        boolean recreateMetaDataAndInstances = (oldVersion >= 59 && oldVersion <= 66);
490
491        try {
492            if (oldVersion < 51) {
493                upgradeToVersion51(db); // From 50 or 51
494                oldVersion = 51;
495            }
496            if (oldVersion == 51) {
497                upgradeToVersion52(db);
498                oldVersion += 1;
499            }
500            if (oldVersion == 52) {
501                upgradeToVersion53(db);
502                oldVersion += 1;
503            }
504            if (oldVersion == 53) {
505                upgradeToVersion54(db);
506                oldVersion += 1;
507            }
508            if (oldVersion == 54) {
509                upgradeToVersion55(db);
510                oldVersion += 1;
511            }
512            if (oldVersion == 55 || oldVersion == 56) {
513                // Both require resync, so just schedule it once
514                upgradeResync(db);
515            }
516            if (oldVersion == 55) {
517                upgradeToVersion56(db);
518                oldVersion += 1;
519            }
520            if (oldVersion == 56) {
521                upgradeToVersion57(db);
522                oldVersion += 1;
523            }
524            if (oldVersion == 57) {
525                // Changes are undone upgrading to 60, so don't do anything.
526                oldVersion += 1;
527            }
528            if (oldVersion == 58) {
529                upgradeToVersion59(db);
530                oldVersion += 1;
531            }
532            if (oldVersion == 59) {
533                upgradeToVersion60(db);
534                oldVersion += 1;
535            }
536            if (oldVersion == 60) {
537                upgradeToVersion61(db);
538                oldVersion += 1;
539            }
540            if (oldVersion == 61) {
541                upgradeToVersion62(db);
542                oldVersion += 1;
543            }
544            if (oldVersion == 62) {
545                upgradeToVersion63(db);
546                oldVersion += 1;
547            }
548            if (oldVersion == 63) {
549                upgradeToVersion64(db);
550                oldVersion += 1;
551            }
552            if (oldVersion == 64) {
553                upgradeToVersion65(db);
554                oldVersion += 1;
555            }
556            if (oldVersion == 65) {
557                upgradeToVersion66(db);
558                oldVersion += 1;
559            }
560            if (oldVersion == 66) {
561                // Changes are done thru recreateMetaDataAndInstances() method
562                oldVersion += 1;
563            }
564            if (recreateMetaDataAndInstances) {
565                recreateMetaDataAndInstances(db);
566            }
567            if(oldVersion == 67 || oldVersion == 68) {
568                upgradeToVersion69(db);
569                oldVersion = 69;
570            }
571            if(oldVersion == 69) {
572                upgradeToVersion100(db);
573                oldVersion = 100;
574            }
575            if(oldVersion == 70) {
576                // Froyo version "70" already has the CalendarCache fix
577                oldVersion = 100;
578            }
579            if(oldVersion == 100) {
580                upgradeToVersion101(db);
581                oldVersion = 101;
582            }
583        } catch (SQLiteException e) {
584            Log.e(TAG, "onUpgrade: SQLiteException, recreating db. " + e);
585            dropTables(db);
586            bootstrapDB(db);
587            return; // this was lossy
588        }
589    }
590
591    /**
592     * If the user_version of the database if between 59 and 66 (those versions has been deployed
593     * with no primary key for the CalendarMetaData table)
594     */
595    private void recreateMetaDataAndInstances(SQLiteDatabase db) {
596        // Recreate the CalendarMetaData table with correct primary key
597        db.execSQL("DROP TABLE CalendarMetaData;");
598        createCalendarMetaDataTable(db);
599
600        // Also clean the Instance table as this table may be corrupted
601        db.execSQL("DELETE FROM Instances;");
602    }
603
604    private static boolean fixAllDayTime(Time time, String timezone, Long timeInMillis) {
605        time.set(timeInMillis);
606        if(time.hour != 0 || time.minute != 0 || time.second != 0) {
607            time.hour = 0;
608            time.minute = 0;
609            time.second = 0;
610            return true;
611        }
612        return false;
613    }
614
615    @VisibleForTesting
616    void upgradeToVersion101(SQLiteDatabase db) {
617        updateCalendarCacheTableTo101(db);
618    }
619
620    @VisibleForTesting
621    void upgradeToVersion100(SQLiteDatabase db) {
622        Cursor cursor = db.rawQuery("SELECT value from CalendarCache WHERE key=?",
623                new String[] {"timezoneDatabaseVersion"});
624
625        String oldTimezoneDbVersion = null;
626        if (cursor != null && cursor.moveToNext()) {
627            oldTimezoneDbVersion = cursor.getString(0);
628        }
629        initCalendarCacheTable(db, oldTimezoneDbVersion);
630    }
631
632    @VisibleForTesting
633    static void upgradeToVersion69(SQLiteDatabase db) {
634        // Clean up allDay events which could be in an invalid state from an earlier version
635        // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This
636        // will go through the allDay events and make sure they have proper values and are in the
637        // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that
638        // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2
639        // and dtend2 are at midnight in their timezone.
640        Cursor cursor = db.rawQuery("SELECT _id, dtstart, dtend, duration, dtstart2, dtend2, " +
641                "eventTimezone, eventTimezone2, rrule FROM Events WHERE allDay=?",
642                new String[] {"1"});
643        if (cursor != null) {
644            try {
645                String timezone;
646                String timezone2;
647                String duration;
648                Long dtstart;
649                Long dtstart2;
650                Long dtend;
651                Long dtend2;
652                Time time = new Time();
653                Long id;
654                // some things need to be in utc so we call this frequently, cache to make faster
655                final String utc = Time.TIMEZONE_UTC;
656                while (cursor.moveToNext()) {
657                    String rrule = cursor.getString(8);
658                    id = cursor.getLong(0);
659                    dtstart = cursor.getLong(1);
660                    dtstart2 = null;
661                    timezone = cursor.getString(6);
662                    timezone2 = cursor.getString(7);
663                    duration = cursor.getString(3);
664
665                    if (TextUtils.isEmpty(rrule)) {
666                        // For non-recurring events dtstart and dtend should both have values
667                        // and duration should be null.
668                        dtend = cursor.getLong(2);
669                        dtend2 = null;
670                        // Since we made all three of these at the same time if timezone2 exists
671                        // so should dtstart2 and dtend2.
672                        if(!TextUtils.isEmpty(timezone2)) {
673                            dtstart2 = cursor.getLong(4);
674                            dtend2 = cursor.getLong(5);
675                        }
676
677                        boolean update = false;
678                        if (!TextUtils.equals(timezone, utc)) {
679                            update = true;
680                            timezone = utc;
681                        }
682
683                        time.clear(timezone);
684                        update |= fixAllDayTime(time, timezone, dtstart);
685                        dtstart = time.normalize(false);
686
687                        time.clear(timezone);
688                        update |= fixAllDayTime(time, timezone, dtend);
689                        dtend = time.normalize(false);
690
691                        if (dtstart2 != null) {
692                            time.clear(timezone2);
693                            update |= fixAllDayTime(time, timezone2, dtstart2);
694                            dtstart2 = time.normalize(false);
695                        }
696
697                        if (dtend2 != null) {
698                            time.clear(timezone2);
699                            update |= fixAllDayTime(time, timezone2, dtend2);
700                            dtend2 = time.normalize(false);
701                        }
702
703                        if (!TextUtils.isEmpty(duration)) {
704                            update = true;
705                        }
706
707                        if (update) {
708                            // enforce duration being null
709                            db.execSQL("UPDATE Events " +
710                                    "SET dtstart=?, dtend=?, dtstart2=?, dtend2=?, duration=?, " +
711                                    "eventTimezone=?, eventTimezone2=? WHERE _id=?",
712                                    new Object[] {dtstart, dtend, dtstart2, dtend2, null, timezone,
713                                            timezone2, id});
714                        }
715
716                    } else {
717                        // For recurring events only dtstart and duration should be used.
718                        // We ignore dtend since it will be overwritten if the event changes to a
719                        // non-recurring event and won't be used otherwise.
720                        if(!TextUtils.isEmpty(timezone2)) {
721                            dtstart2 = cursor.getLong(4);
722                        }
723
724                        boolean update = false;
725                        if (!TextUtils.equals(timezone, utc)) {
726                            update = true;
727                            timezone = utc;
728                        }
729
730                        time.clear(timezone);
731                        update |= fixAllDayTime(time, timezone, dtstart);
732                        dtstart = time.normalize(false);
733
734                        if (dtstart2 != null) {
735                            time.clear(timezone2);
736                            update |= fixAllDayTime(time, timezone2, dtstart2);
737                            dtstart2 = time.normalize(false);
738                        }
739
740                        if (TextUtils.isEmpty(duration)) {
741                            // If duration was missing assume a 1 day duration
742                            duration = "P1D";
743                            update = true;
744                        } else {
745                            int len = duration.length();
746                            // TODO fix durations in other formats as well
747                            if (duration.charAt(0) == 'P' &&
748                                    duration.charAt(len - 1) == 'S') {
749                                int seconds = Integer.parseInt(duration.substring(1, len - 1));
750                                int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
751                                duration = "P" + days + "D";
752                                update = true;
753                            }
754                        }
755
756                        if (update) {
757                            // If there were other problems also enforce dtend being null
758                            db.execSQL("UPDATE Events " +
759                                    "SET dtstart=?,dtend=?,dtstart2=?,dtend2=?,duration=?," +
760                                    "eventTimezone=?, eventTimezone2=? WHERE _id=?",
761                                    new Object[] {dtstart, null, dtstart2, null, duration,
762                                            timezone, timezone2, id});
763                        }
764                    }
765                }
766            } finally {
767                cursor.close();
768            }
769        }
770    }
771
772    private void upgradeToVersion66(SQLiteDatabase db) {
773        // Add a column to indicate whether the event organizer can respond to his own events
774        // The UI should not show attendee status for events in calendars with this column = 0
775        db.execSQL("ALTER TABLE " +
776                "Calendars ADD COLUMN organizerCanRespond INTEGER NOT NULL DEFAULT 1;");
777    }
778
779    private void upgradeToVersion65(SQLiteDatabase db) {
780        // we need to recreate the Events view
781        createEventsView(db);
782    }
783
784    private void upgradeToVersion64(SQLiteDatabase db) {
785        // Add a column that may be used by sync adapters
786        db.execSQL("ALTER TABLE Events ADD COLUMN syncAdapterData TEXT;");
787    }
788
789    private void upgradeToVersion63(SQLiteDatabase db) {
790        // we need to recreate the Events view
791        createEventsView(db);
792    }
793
794    private void upgradeToVersion62(SQLiteDatabase db) {
795        // New columns are to transition to having allDay events in the local timezone
796        db.execSQL("ALTER TABLE Events ADD COLUMN dtstart2 INTEGER;");
797        db.execSQL("ALTER TABLE Events ADD COLUMN dtend2 INTEGER;");
798        db.execSQL("ALTER TABLE Events ADD COLUMN eventTimezone2 TEXT;");
799
800        String[] allDayBit = new String[] {"0"};
801        // Copy over all the data that isn't an all day event.
802        db.execSQL("UPDATE Events " +
803                "SET dtstart2=dtstart,dtend2=dtend,eventTimezone2=eventTimezone " +
804                "WHERE allDay=?;",
805                allDayBit /* selection args */);
806
807        // "cursor" iterates over all the calendars
808        allDayBit[0] = "1";
809        Cursor cursor = db.rawQuery("SELECT Events._id,dtstart,dtend,eventTimezone,timezone " +
810                "FROM Events INNER JOIN Calendars " +
811                "WHERE Events.calendar_id=Calendars._id AND allDay=?",
812                allDayBit /* selection args */);
813
814        Time oldTime = new Time();
815        Time newTime = new Time();
816        // Update the allday events in the new columns
817        if (cursor != null) {
818            try {
819                String[] newData = new String[4];
820                cursor.moveToPosition(-1);
821                while (cursor.moveToNext()) {
822                    long id = cursor.getLong(0); // Order from query above
823                    long dtstart = cursor.getLong(1);
824                    long dtend = cursor.getLong(2);
825                    String eTz = cursor.getString(3); // current event timezone
826                    String tz = cursor.getString(4); // Calendar timezone
827                    //If there's no timezone for some reason use UTC by default.
828                    if(eTz == null) {
829                        eTz = Time.TIMEZONE_UTC;
830                    }
831
832                    // Convert start time for all day events into the timezone of their calendar
833                    oldTime.clear(eTz);
834                    oldTime.set(dtstart);
835                    newTime.clear(tz);
836                    newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
837                    newTime.normalize(false);
838                    dtstart = newTime.toMillis(false /*ignoreDst*/);
839
840                    // Convert end time for all day events into the timezone of their calendar
841                    oldTime.clear(eTz);
842                    oldTime.set(dtend);
843                    newTime.clear(tz);
844                    newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
845                    newTime.normalize(false);
846                    dtend = newTime.toMillis(false /*ignoreDst*/);
847
848                    newData[0] = String.valueOf(dtstart);
849                    newData[1] = String.valueOf(dtend);
850                    newData[2] = tz;
851                    newData[3] = String.valueOf(id);
852                    db.execSQL("UPDATE Events " +
853                            "SET dtstart2=?,dtend2=?,eventTimezone2=? " +
854                            "WHERE _id=?",
855                            newData);
856                }
857            } finally {
858                cursor.close();
859            }
860        }
861    }
862
863    private void upgradeToVersion61(SQLiteDatabase db) {
864        db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
865
866        // IF NOT EXISTS should be normal pattern for table creation
867        db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" +
868                "_id INTEGER PRIMARY KEY," +
869                "key TEXT NOT NULL," +
870                "value TEXT" +
871                ");");
872
873        db.execSQL("INSERT INTO CalendarCache (key, value) VALUES (" +
874                "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "',"  +
875                "'" + CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION + "'" +
876                ");");
877    }
878
879    private void upgradeToVersion60(SQLiteDatabase db) {
880        // Switch to CalendarProvider2
881        upgradeSyncState(db);
882        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
883        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
884                "BEGIN " +
885                "DELETE FROM Events WHERE calendar_id = old._id;" +
886                "END");
887        db.execSQL("ALTER TABLE Events ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;");
888        db.execSQL("DROP TRIGGER IF EXISTS events_insert");
889        db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
890                "BEGIN " +
891                "UPDATE Events SET _sync_account=" +
892                "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," +
893                "_sync_account_type=" +
894                "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " +
895                "WHERE Events._id=new._id;" +
896                "END");
897        db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
898        db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
899        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
900                "BEGIN " +
901                "DELETE FROM Instances WHERE event_id = old._id;" +
902                "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
903                "DELETE FROM Attendees WHERE event_id = old._id;" +
904                "DELETE FROM Reminders WHERE event_id = old._id;" +
905                "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
906                "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
907                "END");
908        db.execSQL("DROP TRIGGER IF EXISTS attendees_update");
909        db.execSQL("DROP TRIGGER IF EXISTS attendees_insert");
910        db.execSQL("DROP TRIGGER IF EXISTS attendees_delete");
911        db.execSQL("DROP TRIGGER IF EXISTS reminders_update");
912        db.execSQL("DROP TRIGGER IF EXISTS reminders_insert");
913        db.execSQL("DROP TRIGGER IF EXISTS reminders_delete");
914        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update");
915        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert");
916        db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete");
917
918        createEventsView(db);
919    }
920
921    private void upgradeToVersion59(SQLiteDatabase db) {
922        db.execSQL("DROP TABLE IF EXISTS BusyBits;");
923        db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup" +
924                "(_id,localTimezone,minInstance,maxInstance);");
925        db.execSQL("INSERT INTO CalendarMetaData_Backup " +
926                "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData;");
927        db.execSQL("DROP TABLE CalendarMetaData;");
928        createCalendarMetaDataTable(db);
929        db.execSQL("INSERT INTO CalendarMetaData " +
930                "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData_Backup;");
931        db.execSQL("DROP TABLE CalendarMetaData_Backup;");
932    }
933
934    private void upgradeToVersion57(SQLiteDatabase db) {
935        db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify"
936                + " INTEGER NOT NULL DEFAULT 0;");
937        db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers"
938                + " INTEGER NOT NULL DEFAULT 1;");
939        db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests"
940                + " INTEGER NOT NULL DEFAULT 1;");
941        db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;");
942        db.execSQL("UPDATE Events SET organizer="
943                + "(SELECT attendeeEmail FROM Attendees WHERE "
944                + "Attendees.event_id = Events._id"
945                + " AND Attendees.attendeeRelationship=2);");
946    }
947
948    private void upgradeToVersion56(SQLiteDatabase db) {
949        db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;");
950        db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;");
951        // Clear _sync_dirty to avoid a client-to-server sync that could blow away
952        // server attendees.
953        // Clear _sync_version to pull down the server's event (with attendees)
954        // Change the URLs from full-selfattendance to full
955        db.execSQL("UPDATE Events"
956                + " SET _sync_dirty=0,"
957                + " _sync_version=NULL,"
958                + " _sync_id="
959                + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full'),"
960                + " commentsUri ="
961                + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');");
962        db.execSQL("UPDATE Calendars"
963                + " SET url="
964                + "REPLACE(url, '/private/full-selfattendance', '/private/full');");
965
966        // "cursor" iterates over all the calendars
967        Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars",
968                null /* selection args */);
969        // Add the owner column.
970        if (cursor != null) {
971            try {
972                while (cursor.moveToNext()) {
973                    Long id = cursor.getLong(0);
974                    String url = cursor.getString(1);
975                    String owner = calendarEmailAddressFromFeedUrl(url);
976                    db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?",
977                            new Object[] {owner, id});
978                }
979            } finally {
980                cursor.close();
981            }
982        }
983    }
984
985    private void upgradeResync(SQLiteDatabase db) {
986        // Delete sync state, so all records will be re-synced.
987        db.execSQL("DELETE FROM _sync_state;");
988
989        // "cursor" iterates over all the calendars
990        Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url "
991                + "FROM Calendars",
992                null /* selection args */);
993        if (cursor != null) {
994            try {
995                while (cursor.moveToNext()) {
996                    String accountName = cursor.getString(0);
997                    String accountType = cursor.getString(1);
998                    final Account account = new Account(accountName, accountType);
999                    String calendarUrl = cursor.getString(2);
1000                    scheduleSync(account, false /* two-way sync */, calendarUrl);
1001                }
1002            } finally {
1003                cursor.close();
1004            }
1005        }
1006    }
1007
1008    private void upgradeToVersion55(SQLiteDatabase db) {
1009        db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
1010        db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
1011        db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
1012        db.execSQL("UPDATE Calendars"
1013                + " SET _sync_account_type='com.google'"
1014                + " WHERE _sync_account IS NOT NULL");
1015        db.execSQL("UPDATE Events"
1016                + " SET _sync_account_type='com.google'"
1017                + " WHERE _sync_account IS NOT NULL");
1018        db.execSQL("UPDATE DeletedEvents"
1019                + " SET _sync_account_type='com.google'"
1020                + " WHERE _sync_account IS NOT NULL");
1021        Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
1022        db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
1023        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
1024                + Calendar.Events._SYNC_ACCOUNT_TYPE + ", "
1025                + Calendar.Events._SYNC_ACCOUNT + ", "
1026                + Calendar.Events._SYNC_ID + ");");
1027    }
1028
1029    private void upgradeToVersion54(SQLiteDatabase db) {
1030        Log.w(TAG, "adding eventSyncAccountAndIdIndex");
1031        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
1032                + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");");
1033    }
1034
1035    private void upgradeToVersion53(SQLiteDatabase db) {
1036        Log.w(TAG, "Upgrading CalendarAlerts table");
1037        db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;");
1038        db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;");
1039        db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;");
1040    }
1041
1042    private void upgradeToVersion52(SQLiteDatabase db) {
1043        // We added "originalAllDay" to the Events table to keep track of
1044        // the allDay status of the original recurring event for entries
1045        // that are exceptions to that recurring event.  We need this so
1046        // that we can format the date correctly for the "originalInstanceTime"
1047        // column when we make a change to the recurrence exception and
1048        // send it to the server.
1049        db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;");
1050
1051        // Iterate through the Events table and for each recurrence
1052        // exception, fill in the correct value for "originalAllDay",
1053        // if possible.  The only times where this might not be possible
1054        // are (1) the original recurring event no longer exists, or
1055        // (2) the original recurring event does not yet have a _sync_id
1056        // because it was created on the phone and hasn't been synced to the
1057        // server yet.  In both cases the originalAllDay field will be set
1058        // to null.  In the first case we don't care because the recurrence
1059        // exception will not be displayed and we won't be able to make
1060        // any changes to it (and even if we did, the server should ignore
1061        // them, right?).  In the second case, the calendar client already
1062        // disallows making changes to an instance of a recurring event
1063        // until the recurring event has been synced to the server so the
1064        // second case should never occur.
1065
1066        // "cursor" iterates over all the recurrences exceptions.
1067        Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events"
1068                + " WHERE originalEvent IS NOT NULL", null /* selection args */);
1069        if (cursor != null) {
1070            try {
1071                while (cursor.moveToNext()) {
1072                    long id = cursor.getLong(0);
1073                    String originalEvent = cursor.getString(1);
1074
1075                    // Find the original recurring event (if it exists)
1076                    Cursor recur = db.rawQuery("SELECT allDay FROM Events"
1077                            + " WHERE _sync_id=?", new String[] {originalEvent});
1078                    if (recur == null) {
1079                        continue;
1080                    }
1081
1082                    try {
1083                        // Fill in the "originalAllDay" field of the
1084                        // recurrence exception with the "allDay" value
1085                        // from the recurring event.
1086                        if (recur.moveToNext()) {
1087                            int allDay = recur.getInt(0);
1088                            db.execSQL("UPDATE Events SET originalAllDay=" + allDay
1089                                    + " WHERE _id="+id);
1090                        }
1091                    } finally {
1092                        recur.close();
1093                    }
1094                }
1095            } finally {
1096                cursor.close();
1097            }
1098        }
1099    }
1100
1101    private void upgradeToVersion51(SQLiteDatabase db) {
1102        Log.w(TAG, "Upgrading DeletedEvents table");
1103
1104        // We don't have enough information to fill in the correct
1105        // value of the calendar_id for old rows in the DeletedEvents
1106        // table, but rows in that table are transient so it is unlikely
1107        // that there are any rows.  Plus, the calendar_id is used only
1108        // when deleting a calendar, which is a rare event.  All new rows
1109        // will have the correct calendar_id.
1110        db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
1111
1112        // Trigger to remove a calendar's events when we delete the calendar
1113        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
1114        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
1115                "BEGIN " +
1116                "DELETE FROM Events WHERE calendar_id = old._id;" +
1117                "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
1118                "END");
1119        db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
1120    }
1121
1122    private void dropTables(SQLiteDatabase db) {
1123        db.execSQL("DROP TABLE IF EXISTS Calendars;");
1124        db.execSQL("DROP TABLE IF EXISTS Events;");
1125        db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
1126        db.execSQL("DROP TABLE IF EXISTS Instances;");
1127        db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
1128        db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
1129        db.execSQL("DROP TABLE IF EXISTS Attendees;");
1130        db.execSQL("DROP TABLE IF EXISTS Reminders;");
1131        db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
1132        db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
1133    }
1134
1135    @Override
1136    public synchronized SQLiteDatabase getWritableDatabase() {
1137        SQLiteDatabase db = super.getWritableDatabase();
1138        return db;
1139    }
1140
1141    public SyncStateContentProviderHelper getSyncState() {
1142        return mSyncState;
1143    }
1144
1145    /**
1146     * Schedule a calendar sync for the account.
1147     * @param account the account for which to schedule a sync
1148     * @param uploadChangesOnly if set, specify that the sync should only send
1149     *   up local changes.  This is typically used for a local sync, a user override of
1150     *   too many deletions, or a sync after a calendar is unselected.
1151     * @param url the url feed for the calendar to sync (may be null, in which case a poll of
1152     *   all feeds is done.)
1153     */
1154    void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
1155        Bundle extras = new Bundle();
1156        if (uploadChangesOnly) {
1157            extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
1158        }
1159        if (url != null) {
1160            extras.putString("feed", url);
1161            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
1162        }
1163        ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras);
1164    }
1165
1166    public interface Views {
1167      public static final String EVENTS = "view_events";
1168    }
1169
1170    public interface Tables {
1171      public static final String EVENTS = "Events";
1172      public static final String CALENDARS = "Calendars";
1173    }
1174
1175    private static void createEventsView(SQLiteDatabase db) {
1176        db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";");
1177        String eventsSelect = "SELECT "
1178                + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + ","
1179                + Calendar.Events.HTML_URI + ","
1180                + Calendar.Events.TITLE + ","
1181                + Calendar.Events.DESCRIPTION + ","
1182                + Calendar.Events.EVENT_LOCATION + ","
1183                + Calendar.Events.STATUS + ","
1184                + Calendar.Events.SELF_ATTENDEE_STATUS + ","
1185                + Calendar.Events.COMMENTS_URI + ","
1186                + Calendar.Events.DTSTART + ","
1187                + Calendar.Events.DTEND + ","
1188                + Calendar.Events.DURATION + ","
1189                + Calendar.Events.EVENT_TIMEZONE + ","
1190                + Calendar.Events.ALL_DAY + ","
1191                + Calendar.Events.VISIBILITY + ","
1192                + Calendar.Events.TIMEZONE + ","
1193                + Calendar.Events.SELECTED + ","
1194                + Calendar.Events.ACCESS_LEVEL + ","
1195                + Calendar.Events.TRANSPARENCY + ","
1196                + Calendar.Events.COLOR + ","
1197                + Calendar.Events.HAS_ALARM + ","
1198                + Calendar.Events.HAS_EXTENDED_PROPERTIES + ","
1199                + Calendar.Events.RRULE + ","
1200                + Calendar.Events.RDATE + ","
1201                + Calendar.Events.EXRULE + ","
1202                + Calendar.Events.EXDATE + ","
1203                + Calendar.Events.ORIGINAL_EVENT + ","
1204                + Calendar.Events.ORIGINAL_INSTANCE_TIME + ","
1205                + Calendar.Events.ORIGINAL_ALL_DAY + ","
1206                + Calendar.Events.LAST_DATE + ","
1207                + Calendar.Events.HAS_ATTENDEE_DATA + ","
1208                + Calendar.Events.CALENDAR_ID + ","
1209                + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + ","
1210                + Calendar.Events.GUESTS_CAN_MODIFY + ","
1211                + Calendar.Events.GUESTS_CAN_SEE_GUESTS + ","
1212                + Calendar.Events.ORGANIZER + ","
1213                + Calendar.Events.DELETED + ","
1214                + Tables.EVENTS + "." + Calendar.Events._SYNC_ID
1215                + " AS " + Calendar.Events._SYNC_ID + ","
1216                + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION
1217                + " AS " + Calendar.Events._SYNC_VERSION + ","
1218                + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY
1219                + " AS " + Calendar.Events._SYNC_DIRTY + ","
1220                + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT
1221                + " AS " + Calendar.Events._SYNC_ACCOUNT + ","
1222                + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE
1223                + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + ","
1224                + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME
1225                + " AS " + Calendar.Events._SYNC_TIME + ","
1226                + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA
1227                + " AS " + Calendar.Events._SYNC_DATA + ","
1228                + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK
1229                + " AS " + Calendar.Events._SYNC_MARK + ","
1230                + Calendar.Calendars.URL + ","
1231                + Calendar.Calendars.OWNER_ACCOUNT + ","
1232                + Calendar.Calendars.SYNC_EVENTS
1233                + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS
1234                + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID
1235                + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID
1236                + ")";
1237
1238        db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect);
1239    }
1240
1241    /**
1242     * Extracts the calendar email from a calendar feed url.
1243     * @param feed the calendar feed url
1244     * @return the calendar email that is in the feed url or null if it can't
1245     * find the email address.
1246     * TODO: this is duplicated in CalendarSyncAdapter; move to a library
1247     */
1248    public static String calendarEmailAddressFromFeedUrl(String feed) {
1249        // Example feed url:
1250        // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
1251        String[] pathComponents = feed.split("/");
1252        if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
1253            try {
1254                return URLDecoder.decode(pathComponents[5], "UTF-8");
1255            } catch (UnsupportedEncodingException e) {
1256                Log.e(TAG, "unable to url decode the email address in calendar " + feed);
1257                return null;
1258            }
1259        }
1260
1261        Log.e(TAG, "unable to find the email address in calendar " + feed);
1262        return null;
1263    }
1264}
1265