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