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