Home | History | Annotate | Download | only in calendar
      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 
     17 package com.android.providers.calendar;
     18 
     19 import com.google.common.annotations.VisibleForTesting;
     20 
     21 import com.android.internal.content.SyncStateContentProviderHelper;
     22 
     23 import android.accounts.Account;
     24 import android.content.ContentResolver;
     25 import android.content.ContentValues;
     26 import android.content.Context;
     27 import android.content.res.Resources;
     28 import android.database.Cursor;
     29 import android.database.DatabaseUtils;
     30 import android.database.sqlite.SQLiteDatabase;
     31 import android.database.sqlite.SQLiteException;
     32 import android.database.sqlite.SQLiteOpenHelper;
     33 import android.os.Bundle;
     34 import android.provider.Calendar;
     35 import android.provider.ContactsContract;
     36 import android.provider.SyncStateContract;
     37 import android.text.TextUtils;
     38 import android.text.format.Time;
     39 import android.util.Log;
     40 
     41 import java.io.UnsupportedEncodingException;
     42 import 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 = 69;
     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 || oldVersion == 68) {
    532                 upgradeToVersion69(db);
    533                 oldVersion = 69;
    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         time.set(timeInMillis);
    558         if(time.hour != 0 || time.minute != 0 || time.second != 0) {
    559             time.hour = 0;
    560             time.minute = 0;
    561             time.second = 0;
    562             return true;
    563         }
    564         return false;
    565     }
    566 
    567     @VisibleForTesting
    568     static void upgradeToVersion69(SQLiteDatabase db) {
    569         // Clean up allDay events which could be in an invalid state from an earlier version
    570         // Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This
    571         // will go through the allDay events and make sure they have proper values and are in the
    572         // correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that
    573         // eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2
    574         // and dtend2 are at midnight in their timezone.
    575         Cursor cursor = db.rawQuery("SELECT _id, dtstart, dtend, duration, dtstart2, dtend2, " +
    576                 "eventTimezone, eventTimezone2, rrule FROM Events WHERE allDay=?",
    577                 new String[] {"1"});
    578         if (cursor != null) {
    579             try {
    580                 String timezone;
    581                 String timezone2;
    582                 String duration;
    583                 Long dtstart;
    584                 Long dtstart2;
    585                 Long dtend;
    586                 Long dtend2;
    587                 Time time = new Time();
    588                 Long id;
    589                 // some things need to be in utc so we call this frequently, cache to make faster
    590                 final String utc = Time.TIMEZONE_UTC;
    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                         if (!TextUtils.equals(timezone, utc)) {
    614                             update = true;
    615                             timezone = utc;
    616                         }
    617 
    618                         time.clear(timezone);
    619                         update |= fixAllDayTime(time, timezone, dtstart);
    620                         dtstart = time.normalize(false);
    621 
    622                         time.clear(timezone);
    623                         update |= fixAllDayTime(time, timezone, dtend);
    624                         dtend = time.normalize(false);
    625 
    626                         if (dtstart2 != null) {
    627                             time.clear(timezone2);
    628                             update |= fixAllDayTime(time, timezone2, dtstart2);
    629                             dtstart2 = time.normalize(false);
    630                         }
    631 
    632                         if (dtend2 != null) {
    633                             time.clear(timezone2);
    634                             update |= fixAllDayTime(time, timezone2, dtend2);
    635                             dtend2 = time.normalize(false);
    636                         }
    637 
    638                         if (!TextUtils.isEmpty(duration)) {
    639                             update = true;
    640                         }
    641 
    642                         if (update) {
    643                             // enforce duration being null
    644                             db.execSQL("UPDATE Events " +
    645                                     "SET dtstart=?, dtend=?, dtstart2=?, dtend2=?, duration=?, " +
    646                                     "eventTimezone=?, eventTimezone2=? WHERE _id=?",
    647                                     new Object[] {dtstart, dtend, dtstart2, dtend2, null, timezone,
    648                                             timezone2, id});
    649                         }
    650 
    651                     } else {
    652                         // For recurring events only dtstart and duration should be used.
    653                         // We ignore dtend since it will be overwritten if the event changes to a
    654                         // non-recurring event and won't be used otherwise.
    655                         if(!TextUtils.isEmpty(timezone2)) {
    656                             dtstart2 = cursor.getLong(4);
    657                         }
    658 
    659                         boolean update = false;
    660                         if (!TextUtils.equals(timezone, utc)) {
    661                             update = true;
    662                             timezone = utc;
    663                         }
    664 
    665                         time.clear(timezone);
    666                         update |= fixAllDayTime(time, timezone, dtstart);
    667                         dtstart = time.normalize(false);
    668 
    669                         if (dtstart2 != null) {
    670                             time.clear(timezone2);
    671                             update |= fixAllDayTime(time, timezone2, dtstart2);
    672                             dtstart2 = time.normalize(false);
    673                         }
    674 
    675                         if (TextUtils.isEmpty(duration)) {
    676                             // If duration was missing assume a 1 day duration
    677                             duration = "P1D";
    678                             update = true;
    679                         } else {
    680                             int len = duration.length();
    681                             // TODO fix durations in other formats as well
    682                             if (duration.charAt(0) == 'P' &&
    683                                     duration.charAt(len - 1) == 'S') {
    684                                 int seconds = Integer.parseInt(duration.substring(1, len - 1));
    685                                 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
    686                                 duration = "P" + days + "D";
    687                                 update = true;
    688                             }
    689                         }
    690 
    691                         if (update) {
    692                             // If there were other problems also enforce dtend being null
    693                             db.execSQL("UPDATE Events " +
    694                                     "SET dtstart=?,dtend=?,dtstart2=?,dtend2=?,duration=?," +
    695                                     "eventTimezone=?, eventTimezone2=? WHERE _id=?",
    696                                     new Object[] {dtstart, null, dtstart2, null, duration,
    697                                             timezone, timezone2, id});
    698                         }
    699                     }
    700                 }
    701             } finally {
    702                 cursor.close();
    703             }
    704         }
    705     }
    706 
    707     private void upgradeToVersion66(SQLiteDatabase db) {
    708         // Add a column to indicate whether the event organizer can respond to his own events
    709         // The UI should not show attendee status for events in calendars with this column = 0
    710         db.execSQL("ALTER TABLE " +
    711                 "Calendars ADD COLUMN organizerCanRespond INTEGER NOT NULL DEFAULT 1;");
    712     }
    713 
    714     private void upgradeToVersion65(SQLiteDatabase db) {
    715         // we need to recreate the Events view
    716         createEventsView(db);
    717     }
    718 
    719     private void upgradeToVersion64(SQLiteDatabase db) {
    720         // Add a column that may be used by sync adapters
    721         db.execSQL("ALTER TABLE Events ADD COLUMN syncAdapterData TEXT;");
    722     }
    723 
    724     private void upgradeToVersion63(SQLiteDatabase db) {
    725         // we need to recreate the Events view
    726         createEventsView(db);
    727     }
    728 
    729     private void upgradeToVersion62(SQLiteDatabase db) {
    730         // New columns are to transition to having allDay events in the local timezone
    731         db.execSQL("ALTER TABLE Events ADD COLUMN dtstart2 INTEGER;");
    732         db.execSQL("ALTER TABLE Events ADD COLUMN dtend2 INTEGER;");
    733         db.execSQL("ALTER TABLE Events ADD COLUMN eventTimezone2 TEXT;");
    734 
    735         String[] allDayBit = new String[] {"0"};
    736         // Copy over all the data that isn't an all day event.
    737         db.execSQL("UPDATE Events " +
    738                 "SET dtstart2=dtstart,dtend2=dtend,eventTimezone2=eventTimezone " +
    739                 "WHERE allDay=?;",
    740                 allDayBit /* selection args */);
    741 
    742         // "cursor" iterates over all the calendars
    743         allDayBit[0] = "1";
    744         Cursor cursor = db.rawQuery("SELECT Events._id,dtstart,dtend,eventTimezone,timezone " +
    745                 "FROM Events INNER JOIN Calendars " +
    746                 "WHERE Events.calendar_id=Calendars._id AND allDay=?",
    747                 allDayBit /* selection args */);
    748 
    749         Time oldTime = new Time();
    750         Time newTime = new Time();
    751         // Update the allday events in the new columns
    752         if (cursor != null) {
    753             try {
    754                 String[] newData = new String[4];
    755                 cursor.moveToPosition(-1);
    756                 while (cursor.moveToNext()) {
    757                     long id = cursor.getLong(0); // Order from query above
    758                     long dtstart = cursor.getLong(1);
    759                     long dtend = cursor.getLong(2);
    760                     String eTz = cursor.getString(3); // current event timezone
    761                     String tz = cursor.getString(4); // Calendar timezone
    762                     //If there's no timezone for some reason use UTC by default.
    763                     if(eTz == null) {
    764                         eTz = Time.TIMEZONE_UTC;
    765                     }
    766 
    767                     // Convert start time for all day events into the timezone of their calendar
    768                     oldTime.clear(eTz);
    769                     oldTime.set(dtstart);
    770                     newTime.clear(tz);
    771                     newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
    772                     newTime.normalize(false);
    773                     dtstart = newTime.toMillis(false /*ignoreDst*/);
    774 
    775                     // Convert end time for all day events into the timezone of their calendar
    776                     oldTime.clear(eTz);
    777                     oldTime.set(dtend);
    778                     newTime.clear(tz);
    779                     newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
    780                     newTime.normalize(false);
    781                     dtend = newTime.toMillis(false /*ignoreDst*/);
    782 
    783                     newData[0] = String.valueOf(dtstart);
    784                     newData[1] = String.valueOf(dtend);
    785                     newData[2] = tz;
    786                     newData[3] = String.valueOf(id);
    787                     db.execSQL("UPDATE Events " +
    788                             "SET dtstart2=?,dtend2=?,eventTimezone2=? " +
    789                             "WHERE _id=?",
    790                             newData);
    791                 }
    792             } finally {
    793                 cursor.close();
    794             }
    795         }
    796     }
    797 
    798     private void upgradeToVersion61(SQLiteDatabase db) {
    799         createCalendarCacheTable(db);
    800     }
    801 
    802     private void upgradeToVersion60(SQLiteDatabase db) {
    803         // Switch to CalendarProvider2
    804         upgradeSyncState(db);
    805         db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
    806         db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
    807                 "BEGIN " +
    808                 "DELETE FROM Events WHERE calendar_id = old._id;" +
    809                 "END");
    810         db.execSQL("ALTER TABLE Events ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;");
    811         db.execSQL("DROP TRIGGER IF EXISTS events_insert");
    812         db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
    813                 "BEGIN " +
    814                 "UPDATE Events SET _sync_account=" +
    815                 "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," +
    816                 "_sync_account_type=" +
    817                 "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " +
    818                 "WHERE Events._id=new._id;" +
    819                 "END");
    820         db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
    821         db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
    822         db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
    823                 "BEGIN " +
    824                 "DELETE FROM Instances WHERE event_id = old._id;" +
    825                 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
    826                 "DELETE FROM Attendees WHERE event_id = old._id;" +
    827                 "DELETE FROM Reminders WHERE event_id = old._id;" +
    828                 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
    829                 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
    830                 "END");
    831         db.execSQL("DROP TRIGGER IF EXISTS attendees_update");
    832         db.execSQL("DROP TRIGGER IF EXISTS attendees_insert");
    833         db.execSQL("DROP TRIGGER IF EXISTS attendees_delete");
    834         db.execSQL("DROP TRIGGER IF EXISTS reminders_update");
    835         db.execSQL("DROP TRIGGER IF EXISTS reminders_insert");
    836         db.execSQL("DROP TRIGGER IF EXISTS reminders_delete");
    837         db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update");
    838         db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert");
    839         db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete");
    840 
    841         createEventsView(db);
    842     }
    843 
    844     private void upgradeToVersion59(SQLiteDatabase db) {
    845         db.execSQL("DROP TABLE IF EXISTS BusyBits;");
    846         db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup" +
    847                 "(_id,localTimezone,minInstance,maxInstance);");
    848         db.execSQL("INSERT INTO CalendarMetaData_Backup " +
    849                 "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData;");
    850         db.execSQL("DROP TABLE CalendarMetaData;");
    851         createCalendarMetaDataTable(db);
    852         db.execSQL("INSERT INTO CalendarMetaData " +
    853                 "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData_Backup;");
    854         db.execSQL("DROP TABLE CalendarMetaData_Backup;");
    855     }
    856 
    857     private void upgradeToVersion57(SQLiteDatabase db) {
    858         db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify"
    859                 + " INTEGER NOT NULL DEFAULT 0;");
    860         db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers"
    861                 + " INTEGER NOT NULL DEFAULT 1;");
    862         db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests"
    863                 + " INTEGER NOT NULL DEFAULT 1;");
    864         db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;");
    865         db.execSQL("UPDATE Events SET organizer="
    866                 + "(SELECT attendeeEmail FROM Attendees WHERE "
    867                 + "Attendees.event_id = Events._id"
    868                 + " AND Attendees.attendeeRelationship=2);");
    869     }
    870 
    871     private void upgradeToVersion56(SQLiteDatabase db) {
    872         db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;");
    873         db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;");
    874         // Clear _sync_dirty to avoid a client-to-server sync that could blow away
    875         // server attendees.
    876         // Clear _sync_version to pull down the server's event (with attendees)
    877         // Change the URLs from full-selfattendance to full
    878         db.execSQL("UPDATE Events"
    879                 + " SET _sync_dirty=0,"
    880                 + " _sync_version=NULL,"
    881                 + " _sync_id="
    882                 + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full'),"
    883                 + " commentsUri ="
    884                 + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');");
    885         db.execSQL("UPDATE Calendars"
    886                 + " SET url="
    887                 + "REPLACE(url, '/private/full-selfattendance', '/private/full');");
    888 
    889         // "cursor" iterates over all the calendars
    890         Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars",
    891                 null /* selection args */);
    892         // Add the owner column.
    893         if (cursor != null) {
    894             try {
    895                 while (cursor.moveToNext()) {
    896                     Long id = cursor.getLong(0);
    897                     String url = cursor.getString(1);
    898                     String owner = calendarEmailAddressFromFeedUrl(url);
    899                     db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?",
    900                             new Object[] {owner, id});
    901                 }
    902             } finally {
    903                 cursor.close();
    904             }
    905         }
    906     }
    907 
    908     private void upgradeResync(SQLiteDatabase db) {
    909         // Delete sync state, so all records will be re-synced.
    910         db.execSQL("DELETE FROM _sync_state;");
    911 
    912         // "cursor" iterates over all the calendars
    913         Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url "
    914                 + "FROM Calendars",
    915                 null /* selection args */);
    916         if (cursor != null) {
    917             try {
    918                 while (cursor.moveToNext()) {
    919                     String accountName = cursor.getString(0);
    920                     String accountType = cursor.getString(1);
    921                     final Account account = new Account(accountName, accountType);
    922                     String calendarUrl = cursor.getString(2);
    923                     scheduleSync(account, false /* two-way sync */, calendarUrl);
    924                 }
    925             } finally {
    926                 cursor.close();
    927             }
    928         }
    929     }
    930 
    931     private void upgradeToVersion55(SQLiteDatabase db) {
    932         db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
    933         db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
    934         db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
    935         db.execSQL("UPDATE Calendars"
    936                 + " SET _sync_account_type='com.google'"
    937                 + " WHERE _sync_account IS NOT NULL");
    938         db.execSQL("UPDATE Events"
    939                 + " SET _sync_account_type='com.google'"
    940                 + " WHERE _sync_account IS NOT NULL");
    941         db.execSQL("UPDATE DeletedEvents"
    942                 + " SET _sync_account_type='com.google'"
    943                 + " WHERE _sync_account IS NOT NULL");
    944         Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
    945         db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
    946         db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
    947                 + Calendar.Events._SYNC_ACCOUNT_TYPE + ", "
    948                 + Calendar.Events._SYNC_ACCOUNT + ", "
    949                 + Calendar.Events._SYNC_ID + ");");
    950     }
    951 
    952     private void upgradeToVersion54(SQLiteDatabase db) {
    953         Log.w(TAG, "adding eventSyncAccountAndIdIndex");
    954         db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
    955                 + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");");
    956     }
    957 
    958     private void upgradeToVersion53(SQLiteDatabase db) {
    959         Log.w(TAG, "Upgrading CalendarAlerts table");
    960         db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;");
    961         db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;");
    962         db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;");
    963     }
    964 
    965     private void upgradeToVersion52(SQLiteDatabase db) {
    966         // We added "originalAllDay" to the Events table to keep track of
    967         // the allDay status of the original recurring event for entries
    968         // that are exceptions to that recurring event.  We need this so
    969         // that we can format the date correctly for the "originalInstanceTime"
    970         // column when we make a change to the recurrence exception and
    971         // send it to the server.
    972         db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;");
    973 
    974         // Iterate through the Events table and for each recurrence
    975         // exception, fill in the correct value for "originalAllDay",
    976         // if possible.  The only times where this might not be possible
    977         // are (1) the original recurring event no longer exists, or
    978         // (2) the original recurring event does not yet have a _sync_id
    979         // because it was created on the phone and hasn't been synced to the
    980         // server yet.  In both cases the originalAllDay field will be set
    981         // to null.  In the first case we don't care because the recurrence
    982         // exception will not be displayed and we won't be able to make
    983         // any changes to it (and even if we did, the server should ignore
    984         // them, right?).  In the second case, the calendar client already
    985         // disallows making changes to an instance of a recurring event
    986         // until the recurring event has been synced to the server so the
    987         // second case should never occur.
    988 
    989         // "cursor" iterates over all the recurrences exceptions.
    990         Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events"
    991                 + " WHERE originalEvent IS NOT NULL", null /* selection args */);
    992         if (cursor != null) {
    993             try {
    994                 while (cursor.moveToNext()) {
    995                     long id = cursor.getLong(0);
    996                     String originalEvent = cursor.getString(1);
    997 
    998                     // Find the original recurring event (if it exists)
    999                     Cursor recur = db.rawQuery("SELECT allDay FROM Events"
   1000                             + " WHERE _sync_id=?", new String[] {originalEvent});
   1001                     if (recur == null) {
   1002                         continue;
   1003                     }
   1004 
   1005                     try {
   1006                         // Fill in the "originalAllDay" field of the
   1007                         // recurrence exception with the "allDay" value
   1008                         // from the recurring event.
   1009                         if (recur.moveToNext()) {
   1010                             int allDay = recur.getInt(0);
   1011                             db.execSQL("UPDATE Events SET originalAllDay=" + allDay
   1012                                     + " WHERE _id="+id);
   1013                         }
   1014                     } finally {
   1015                         recur.close();
   1016                     }
   1017                 }
   1018             } finally {
   1019                 cursor.close();
   1020             }
   1021         }
   1022     }
   1023 
   1024     private void upgradeToVersion51(SQLiteDatabase db) {
   1025         Log.w(TAG, "Upgrading DeletedEvents table");
   1026 
   1027         // We don't have enough information to fill in the correct
   1028         // value of the calendar_id for old rows in the DeletedEvents
   1029         // table, but rows in that table are transient so it is unlikely
   1030         // that there are any rows.  Plus, the calendar_id is used only
   1031         // when deleting a calendar, which is a rare event.  All new rows
   1032         // will have the correct calendar_id.
   1033         db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
   1034 
   1035         // Trigger to remove a calendar's events when we delete the calendar
   1036         db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
   1037         db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
   1038                 "BEGIN " +
   1039                 "DELETE FROM Events WHERE calendar_id = old._id;" +
   1040                 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
   1041                 "END");
   1042         db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
   1043     }
   1044 
   1045     private void dropTables(SQLiteDatabase db) {
   1046         db.execSQL("DROP TABLE IF EXISTS Calendars;");
   1047         db.execSQL("DROP TABLE IF EXISTS Events;");
   1048         db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
   1049         db.execSQL("DROP TABLE IF EXISTS Instances;");
   1050         db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
   1051         db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
   1052         db.execSQL("DROP TABLE IF EXISTS Attendees;");
   1053         db.execSQL("DROP TABLE IF EXISTS Reminders;");
   1054         db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
   1055         db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
   1056     }
   1057 
   1058     @Override
   1059     public synchronized SQLiteDatabase getWritableDatabase() {
   1060         SQLiteDatabase db = super.getWritableDatabase();
   1061         return db;
   1062     }
   1063 
   1064     public SyncStateContentProviderHelper getSyncState() {
   1065         return mSyncState;
   1066     }
   1067 
   1068     /**
   1069      * Schedule a calendar sync for the account.
   1070      * @param account the account for which to schedule a sync
   1071      * @param uploadChangesOnly if set, specify that the sync should only send
   1072      *   up local changes.  This is typically used for a local sync, a user override of
   1073      *   too many deletions, or a sync after a calendar is unselected.
   1074      * @param url the url feed for the calendar to sync (may be null, in which case a poll of
   1075      *   all feeds is done.)
   1076      */
   1077     void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
   1078         Bundle extras = new Bundle();
   1079         if (uploadChangesOnly) {
   1080             extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
   1081         }
   1082         if (url != null) {
   1083             extras.putString("feed", url);
   1084             extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   1085         }
   1086         ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras);
   1087     }
   1088 
   1089     public interface Views {
   1090       public static final String EVENTS = "view_events";
   1091     }
   1092 
   1093     public interface Tables {
   1094       public static final String EVENTS = "Events";
   1095       public static final String CALENDARS = "Calendars";
   1096     }
   1097 
   1098     private static void createEventsView(SQLiteDatabase db) {
   1099         db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";");
   1100         String eventsSelect = "SELECT "
   1101                 + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + ","
   1102                 + Calendar.Events.HTML_URI + ","
   1103                 + Calendar.Events.TITLE + ","
   1104                 + Calendar.Events.DESCRIPTION + ","
   1105                 + Calendar.Events.EVENT_LOCATION + ","
   1106                 + Calendar.Events.STATUS + ","
   1107                 + Calendar.Events.SELF_ATTENDEE_STATUS + ","
   1108                 + Calendar.Events.COMMENTS_URI + ","
   1109                 + Calendar.Events.DTSTART + ","
   1110                 + Calendar.Events.DTEND + ","
   1111                 + Calendar.Events.DURATION + ","
   1112                 + Calendar.Events.EVENT_TIMEZONE + ","
   1113                 + Calendar.Events.ALL_DAY + ","
   1114                 + Calendar.Events.VISIBILITY + ","
   1115                 + Calendar.Events.TIMEZONE + ","
   1116                 + Calendar.Events.SELECTED + ","
   1117                 + Calendar.Events.ACCESS_LEVEL + ","
   1118                 + Calendar.Events.TRANSPARENCY + ","
   1119                 + Calendar.Events.COLOR + ","
   1120                 + Calendar.Events.HAS_ALARM + ","
   1121                 + Calendar.Events.HAS_EXTENDED_PROPERTIES + ","
   1122                 + Calendar.Events.RRULE + ","
   1123                 + Calendar.Events.RDATE + ","
   1124                 + Calendar.Events.EXRULE + ","
   1125                 + Calendar.Events.EXDATE + ","
   1126                 + Calendar.Events.ORIGINAL_EVENT + ","
   1127                 + Calendar.Events.ORIGINAL_INSTANCE_TIME + ","
   1128                 + Calendar.Events.ORIGINAL_ALL_DAY + ","
   1129                 + Calendar.Events.LAST_DATE + ","
   1130                 + Calendar.Events.HAS_ATTENDEE_DATA + ","
   1131                 + Calendar.Events.CALENDAR_ID + ","
   1132                 + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + ","
   1133                 + Calendar.Events.GUESTS_CAN_MODIFY + ","
   1134                 + Calendar.Events.GUESTS_CAN_SEE_GUESTS + ","
   1135                 + Calendar.Events.ORGANIZER + ","
   1136                 + Calendar.Events.DELETED + ","
   1137                 + Tables.EVENTS + "." + Calendar.Events._SYNC_ID
   1138                 + " AS " + Calendar.Events._SYNC_ID + ","
   1139                 + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION
   1140                 + " AS " + Calendar.Events._SYNC_VERSION + ","
   1141                 + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY
   1142                 + " AS " + Calendar.Events._SYNC_DIRTY + ","
   1143                 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT
   1144                 + " AS " + Calendar.Events._SYNC_ACCOUNT + ","
   1145                 + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE
   1146                 + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + ","
   1147                 + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME
   1148                 + " AS " + Calendar.Events._SYNC_TIME + ","
   1149                 + Tables.EVENTS + "." + Calendar.Events._SYNC_DATA
   1150                 + " AS " + Calendar.Events._SYNC_DATA + ","
   1151                 + Tables.EVENTS + "." + Calendar.Events._SYNC_MARK
   1152                 + " AS " + Calendar.Events._SYNC_MARK + ","
   1153                 + Calendar.Calendars.URL + ","
   1154                 + Calendar.Calendars.OWNER_ACCOUNT + ","
   1155                 + Calendar.Calendars.SYNC_EVENTS
   1156                 + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS
   1157                 + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID
   1158                 + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID
   1159                 + ")";
   1160 
   1161         db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect);
   1162     }
   1163 
   1164     /**
   1165      * Extracts the calendar email from a calendar feed url.
   1166      * @param feed the calendar feed url
   1167      * @return the calendar email that is in the feed url or null if it can't
   1168      * find the email address.
   1169      * TODO: this is duplicated in CalendarSyncAdapter; move to a library
   1170      */
   1171     public static String calendarEmailAddressFromFeedUrl(String feed) {
   1172         // Example feed url:
   1173         // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
   1174         String[] pathComponents = feed.split("/");
   1175         if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
   1176             try {
   1177                 return URLDecoder.decode(pathComponents[5], "UTF-8");
   1178             } catch (UnsupportedEncodingException e) {
   1179                 Log.e(TAG, "unable to url decode the email address in calendar " + feed);
   1180                 return null;
   1181             }
   1182         }
   1183 
   1184         Log.e(TAG, "unable to find the email address in calendar " + feed);
   1185         return null;
   1186     }
   1187 }
   1188