blob: db702695c79e538ee74571c8bf6b12bef6addd90 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.providers.calendar;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Colors;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.provider.SyncStateContract;
import android.text.TextUtils;
import android.text.format.Time;
import android.util.Log;
import com.android.common.content.SyncStateContentProviderHelper;
import com.google.common.annotations.VisibleForTesting;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.TimeZone;
/**
* Database helper for calendar. Designed as a singleton to make sure that all
* {@link android.content.ContentProvider} users get the same reference.
*/
/* package */ class CalendarDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "CalendarDatabaseHelper";
private static final boolean LOGD = false;
private static final String DATABASE_NAME = "calendar.db";
private static final int DAY_IN_SECONDS = 24 * 60 * 60;
// Note: if you update the version number, you must also update the code
// in upgradeDatabase() to modify the database (gracefully, if possible).
// Versions under 100 cover through Froyo, 1xx version are for Gingerbread,
// 2xx for Honeycomb, and 3xx for ICS. For future versions bump this to the
// next hundred at each major release.
static final int DATABASE_VERSION = 308;
private static final int PRE_FROYO_SYNC_STATE_VERSION = 3;
// columns used to duplicate an event row
private static final String LAST_SYNCED_EVENT_COLUMNS =
Events._SYNC_ID + "," +
Events.CALENDAR_ID + "," +
Events.TITLE + "," +
Events.EVENT_LOCATION + "," +
Events.DESCRIPTION + "," +
Events.EVENT_COLOR + "," +
Events.EVENT_COLOR_KEY + "," +
Events.STATUS + "," +
Events.SELF_ATTENDEE_STATUS + "," +
Events.DTSTART + "," +
Events.DTEND + "," +
Events.EVENT_TIMEZONE + "," +
Events.EVENT_END_TIMEZONE + "," +
Events.DURATION + "," +
Events.ALL_DAY + "," +
Events.ACCESS_LEVEL + "," +
Events.AVAILABILITY + "," +
Events.HAS_ALARM + "," +
Events.HAS_EXTENDED_PROPERTIES + "," +
Events.RRULE + "," +
Events.RDATE + "," +
Events.EXRULE + "," +
Events.EXDATE + "," +
Events.ORIGINAL_SYNC_ID + "," +
Events.ORIGINAL_ID + "," +
Events.ORIGINAL_INSTANCE_TIME + "," +
Events.ORIGINAL_ALL_DAY + "," +
Events.LAST_DATE + "," +
Events.HAS_ATTENDEE_DATA + "," +
Events.GUESTS_CAN_MODIFY + "," +
Events.GUESTS_CAN_INVITE_OTHERS + "," +
Events.GUESTS_CAN_SEE_GUESTS + "," +
Events.ORGANIZER;
// columns used to duplicate a reminder row
private static final String LAST_SYNCED_REMINDER_COLUMNS =
Reminders.MINUTES + "," +
Reminders.METHOD;
// columns used to duplicate an attendee row
private static final String LAST_SYNCED_ATTENDEE_COLUMNS =
Attendees.ATTENDEE_NAME + "," +
Attendees.ATTENDEE_EMAIL + "," +
Attendees.ATTENDEE_STATUS + "," +
Attendees.ATTENDEE_RELATIONSHIP + "," +
Attendees.ATTENDEE_TYPE;
// columns used to duplicate an extended property row
private static final String LAST_SYNCED_EXTENDED_PROPERTY_COLUMNS =
CalendarContract.ExtendedProperties.NAME + "," +
CalendarContract.ExtendedProperties.VALUE;
public interface Tables {
public static final String CALENDARS = "Calendars";
public static final String EVENTS = "Events";
public static final String EVENTS_RAW_TIMES = "EventsRawTimes";
public static final String INSTANCES = "Instances";
public static final String ATTENDEES = "Attendees";
public static final String REMINDERS = "Reminders";
public static final String CALENDAR_ALERTS = "CalendarAlerts";
public static final String EXTENDED_PROPERTIES = "ExtendedProperties";
public static final String CALENDAR_META_DATA = "CalendarMetaData";
public static final String CALENDAR_CACHE = "CalendarCache";
public static final String SYNC_STATE = "_sync_state";
public static final String SYNC_STATE_META = "_sync_state_metadata";
public static final String COLORS = "Colors";
}
public interface Views {
public static final String EVENTS = "view_events";
}
// Copied from SyncStateContentProviderHelper. Don't really want to make them public there.
private static final String SYNC_STATE_META_VERSION_COLUMN = "version";
// This needs to be done when all the tables are already created
private static final String EVENTS_CLEANUP_TRIGGER_SQL =
"DELETE FROM " + Tables.INSTANCES +
" WHERE "+ CalendarContract.Instances.EVENT_ID + "=" +
"old." + CalendarContract.Events._ID + ";" +
"DELETE FROM " + Tables.EVENTS_RAW_TIMES +
" WHERE " + CalendarContract.EventsRawTimes.EVENT_ID + "=" +
"old." + CalendarContract.Events._ID + ";" +
"DELETE FROM " + Tables.ATTENDEES +
" WHERE " + CalendarContract.Attendees.EVENT_ID + "=" +
"old." + CalendarContract.Events._ID + ";" +
"DELETE FROM " + Tables.REMINDERS +
" WHERE " + CalendarContract.Reminders.EVENT_ID + "=" +
"old." + CalendarContract.Events._ID + ";" +
"DELETE FROM " + Tables.CALENDAR_ALERTS +
" WHERE " + CalendarContract.CalendarAlerts.EVENT_ID + "=" +
"old." + CalendarContract.Events._ID + ";" +
"DELETE FROM " + Tables.EXTENDED_PROPERTIES +
" WHERE " + CalendarContract.ExtendedProperties.EVENT_ID + "=" +
"old." + CalendarContract.Events._ID + ";";
// This ensures any exceptions based on an event get their original_sync_id
// column set when an the _sync_id is set.
private static final String EVENTS_ORIGINAL_SYNC_TRIGGER_SQL =
"UPDATE " + Tables.EVENTS +
" SET " + Events.ORIGINAL_SYNC_ID + "=new." + Events._SYNC_ID +
" WHERE " + Events.ORIGINAL_ID + "=old." + Events._ID + ";";
private static final String SYNC_ID_UPDATE_TRIGGER_NAME = "original_sync_update";
private static final String CREATE_SYNC_ID_UPDATE_TRIGGER =
"CREATE TRIGGER " + SYNC_ID_UPDATE_TRIGGER_NAME + " UPDATE OF " + Events._SYNC_ID +
" ON " + Tables.EVENTS +
" BEGIN " +
EVENTS_ORIGINAL_SYNC_TRIGGER_SQL +
" END";
private static final String CALENDAR_CLEANUP_TRIGGER_SQL = "DELETE FROM " + Tables.EVENTS +
" WHERE " + CalendarContract.Events.CALENDAR_ID + "=" +
"old." + CalendarContract.Events._ID + ";";
private static final String CALENDAR_UPDATE_COLOR_TRIGGER_SQL = "UPDATE " + Tables.CALENDARS
+ " SET calendar_color=(SELECT " + Colors.COLOR + " FROM " + Tables.COLORS + " WHERE "
+ Colors.ACCOUNT_NAME + "=" + "new." + Calendars.ACCOUNT_NAME + " AND "
+ Colors.ACCOUNT_TYPE + "=" + "new." + Calendars.ACCOUNT_TYPE + " AND "
+ Colors.COLOR_KEY + "=" + "new." + Calendars.CALENDAR_COLOR_KEY + ") "
+ " WHERE " + Calendars._ID + "=" + "old." + Calendars._ID
+ ";";
private static final String CALENDAR_COLOR_UPDATE_TRIGGER_NAME = "calendar_color_update";
private static final String CREATE_CALENDAR_COLOR_UPDATE_TRIGGER = "CREATE TRIGGER "
+ CALENDAR_COLOR_UPDATE_TRIGGER_NAME + " UPDATE OF " + Calendars.CALENDAR_COLOR_KEY
+ " ON " + Tables.CALENDARS + " WHEN new." + Calendars.CALENDAR_COLOR_KEY
+ " NOT NULL BEGIN " + CALENDAR_UPDATE_COLOR_TRIGGER_SQL + " END";
private static final String EVENT_UPDATE_COLOR_TRIGGER_SQL = "UPDATE " + Tables.EVENTS
+ " SET eventColor=(SELECT " + Colors.COLOR + " FROM " + Tables.COLORS + " WHERE "
+ Colors.ACCOUNT_NAME + "=" + "(SELECT " + Calendars.ACCOUNT_NAME + " FROM "
+ Tables.CALENDARS + " WHERE " + Calendars._ID + "=new." + Events.CALENDAR_ID
+ ") AND " + Colors.ACCOUNT_TYPE + "=" + "(SELECT " + Calendars.ACCOUNT_TYPE + " FROM "
+ Tables.CALENDARS + " WHERE " + Calendars._ID + "=new." + Events.CALENDAR_ID
+ ") AND " + Colors.COLOR_KEY + "=" + "new." + Events.EVENT_COLOR_KEY + ") "
+ " WHERE " + Events._ID + "=" + "old." + Events._ID + ";";
private static final String EVENT_COLOR_UPDATE_TRIGGER_NAME = "event_color_update";
private static final String CREATE_EVENT_COLOR_UPDATE_TRIGGER = "CREATE TRIGGER "
+ EVENT_COLOR_UPDATE_TRIGGER_NAME + " UPDATE OF " + Events.EVENT_COLOR_KEY + " ON "
+ Tables.EVENTS + " WHEN new." + Events.EVENT_COLOR_KEY + " NOT NULL BEGIN "
+ EVENT_UPDATE_COLOR_TRIGGER_SQL + " END";
/** Selects rows from Attendees for which the event_id refers to a nonexistent Event */
private static final String WHERE_ATTENDEES_ORPHANS =
Attendees.EVENT_ID + " IN (SELECT " + Attendees.EVENT_ID + " FROM " +
Tables.ATTENDEES + " LEFT OUTER JOIN " + Tables.EVENTS + " ON " +
Attendees.EVENT_ID + "=" + Tables.EVENTS + "." + Events._ID +
" WHERE " + Tables.EVENTS + "." + Events._ID + " IS NULL)";
/** Selects rows from Reminders for which the event_id refers to a nonexistent Event */
private static final String WHERE_REMINDERS_ORPHANS =
Reminders.EVENT_ID + " IN (SELECT " + Reminders.EVENT_ID + " FROM " +
Tables.REMINDERS + " LEFT OUTER JOIN " + Tables.EVENTS + " ON " +
Reminders.EVENT_ID + "=" + Tables.EVENTS + "." + Events._ID +
" WHERE " + Tables.EVENTS + "." + Events._ID + " IS NULL)";
private static final String SCHEMA_HTTPS = "https://";
private static final String SCHEMA_HTTP = "http://";
private final SyncStateContentProviderHelper mSyncState;
private static CalendarDatabaseHelper sSingleton = null;
private DatabaseUtils.InsertHelper mCalendarsInserter;
private DatabaseUtils.InsertHelper mColorsInserter;
private DatabaseUtils.InsertHelper mEventsInserter;
private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
private DatabaseUtils.InsertHelper mInstancesInserter;
private DatabaseUtils.InsertHelper mAttendeesInserter;
private DatabaseUtils.InsertHelper mRemindersInserter;
private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
public long calendarsInsert(ContentValues values) {
return mCalendarsInserter.insert(values);
}
public long colorsInsert(ContentValues values) {
return mColorsInserter.insert(values);
}
public long eventsInsert(ContentValues values) {
return mEventsInserter.insert(values);
}
public long eventsRawTimesInsert(ContentValues values) {
return mEventsRawTimesInserter.insert(values);
}
public long eventsRawTimesReplace(ContentValues values) {
return mEventsRawTimesInserter.replace(values);
}
public long instancesInsert(ContentValues values) {
return mInstancesInserter.insert(values);
}
public long instancesReplace(ContentValues values) {
return mInstancesInserter.replace(values);
}
public long attendeesInsert(ContentValues values) {
return mAttendeesInserter.insert(values);
}
public long remindersInsert(ContentValues values) {
return mRemindersInserter.insert(values);
}
public long calendarAlertsInsert(ContentValues values) {
return mCalendarAlertsInserter.insert(values);
}
public long extendedPropertiesInsert(ContentValues values) {
return mExtendedPropertiesInserter.insert(values);
}
public static synchronized CalendarDatabaseHelper getInstance(Context context) {
if (sSingleton == null) {
sSingleton = new CalendarDatabaseHelper(context);
}
return sSingleton;
}
/**
* Private constructor, callers except unit tests should obtain an instance through
* {@link #getInstance(android.content.Context)} instead.
*/
/* package */ CalendarDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
if (LOGD) Log.d(TAG, "Creating OpenHelper");
mSyncState = new SyncStateContentProviderHelper();
}
@Override
public void onOpen(SQLiteDatabase db) {
mSyncState.onDatabaseOpened(db);
mCalendarsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDARS);
mColorsInserter = new DatabaseUtils.InsertHelper(db, Tables.COLORS);
mEventsInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS);
mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS_RAW_TIMES);
mInstancesInserter = new DatabaseUtils.InsertHelper(db, Tables.INSTANCES);
mAttendeesInserter = new DatabaseUtils.InsertHelper(db, Tables.ATTENDEES);
mRemindersInserter = new DatabaseUtils.InsertHelper(db, Tables.REMINDERS);
mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDAR_ALERTS);
mExtendedPropertiesInserter =
new DatabaseUtils.InsertHelper(db, Tables.EXTENDED_PROPERTIES);
}
/*
* Upgrade sync state table if necessary. Note that the data bundle
* in the table is not upgraded.
*
* The sync state used to be stored with version 3, but now uses the
* same sync state code as contacts, which is version 1. This code
* upgrades from 3 to 1 if necessary. (Yes, the numbers are unfortunately
* backwards.)
*
* This code is only called when upgrading from an old calendar version,
* so there is no problem if sync state version 3 gets used again in the
* future.
*/
private void upgradeSyncState(SQLiteDatabase db) {
long version = DatabaseUtils.longForQuery(db,
"SELECT " + SYNC_STATE_META_VERSION_COLUMN
+ " FROM " + Tables.SYNC_STATE_META,
null);
if (version == PRE_FROYO_SYNC_STATE_VERSION) {
Log.i(TAG, "Upgrading calendar sync state table");
db.execSQL("CREATE TEMPORARY TABLE state_backup(_sync_account TEXT, "
+ "_sync_account_type TEXT, data TEXT);");
db.execSQL("INSERT INTO state_backup SELECT _sync_account, _sync_account_type, data"
+ " FROM "
+ Tables.SYNC_STATE
+ " WHERE _sync_account is not NULL and _sync_account_type is not NULL;");
db.execSQL("DROP TABLE " + Tables.SYNC_STATE + ";");
mSyncState.onDatabaseOpened(db);
db.execSQL("INSERT INTO " + Tables.SYNC_STATE + "("
+ SyncStateContract.Columns.ACCOUNT_NAME + ","
+ SyncStateContract.Columns.ACCOUNT_TYPE + ","
+ SyncStateContract.Columns.DATA
+ ") SELECT _sync_account, _sync_account_type, data from state_backup;");
db.execSQL("DROP TABLE state_backup;");
} else {
// Wrong version to upgrade.
// Don't need to do anything more here because mSyncState.onDatabaseOpened() will blow
// away and recreate the database (which will result in a resync).
Log.w(TAG, "upgradeSyncState: current version is " + version + ", skipping upgrade.");
}
}
@Override
public void onCreate(SQLiteDatabase db) {
bootstrapDB(db);
}
private void bootstrapDB(SQLiteDatabase db) {
Log.i(TAG, "Bootstrapping database");
mSyncState.createDatabase(db);
createColorsTable(db);
createCalendarsTable(db);
createEventsTable(db);
db.execSQL("CREATE TABLE " + Tables.EVENTS_RAW_TIMES + " (" +
CalendarContract.EventsRawTimes._ID + " INTEGER PRIMARY KEY," +
CalendarContract.EventsRawTimes.EVENT_ID + " INTEGER NOT NULL," +
CalendarContract.EventsRawTimes.DTSTART_2445 + " TEXT," +
CalendarContract.EventsRawTimes.DTEND_2445 + " TEXT," +
CalendarContract.EventsRawTimes.ORIGINAL_INSTANCE_TIME_2445 + " TEXT," +
CalendarContract.EventsRawTimes.LAST_DATE_2445 + " TEXT," +
"UNIQUE (" + CalendarContract.EventsRawTimes.EVENT_ID + ")" +
");");
db.execSQL("CREATE TABLE " + Tables.INSTANCES + " (" +
CalendarContract.Instances._ID + " INTEGER PRIMARY KEY," +
CalendarContract.Instances.EVENT_ID + " INTEGER," +
CalendarContract.Instances.BEGIN + " INTEGER," + // UTC millis
CalendarContract.Instances.END + " INTEGER," + // UTC millis
CalendarContract.Instances.START_DAY + " INTEGER," + // Julian start day
CalendarContract.Instances.END_DAY + " INTEGER," + // Julian end day
CalendarContract.Instances.START_MINUTE + " INTEGER," + // minutes from midnight
CalendarContract.Instances.END_MINUTE + " INTEGER," + // minutes from midnight
"UNIQUE (" +
CalendarContract.Instances.EVENT_ID + ", " +
CalendarContract.Instances.BEGIN + ", " +
CalendarContract.Instances.END + ")" +
");");
db.execSQL("CREATE INDEX instancesStartDayIndex ON " + Tables.INSTANCES + " (" +
CalendarContract.Instances.START_DAY +
");");
createCalendarMetaDataTable(db);
createCalendarCacheTable(db, null);
db.execSQL("CREATE TABLE " + Tables.ATTENDEES + " (" +
CalendarContract.Attendees._ID + " INTEGER PRIMARY KEY," +
CalendarContract.Attendees.EVENT_ID + " INTEGER," +
CalendarContract.Attendees.ATTENDEE_NAME + " TEXT," +
CalendarContract.Attendees.ATTENDEE_EMAIL + " TEXT," +
CalendarContract.Attendees.ATTENDEE_STATUS + " INTEGER," +
CalendarContract.Attendees.ATTENDEE_RELATIONSHIP + " INTEGER," +
CalendarContract.Attendees.ATTENDEE_TYPE + " INTEGER" +
");");
db.execSQL("CREATE INDEX attendeesEventIdIndex ON " + Tables.ATTENDEES + " (" +
CalendarContract.Attendees.EVENT_ID +
");");
db.execSQL("CREATE TABLE " + Tables.REMINDERS + " (" +
CalendarContract.Reminders._ID + " INTEGER PRIMARY KEY," +
CalendarContract.Reminders.EVENT_ID + " INTEGER," +
CalendarContract.Reminders.MINUTES + " INTEGER," +
CalendarContract.Reminders.METHOD + " INTEGER NOT NULL" +
" DEFAULT " + CalendarContract.Reminders.METHOD_DEFAULT +
");");
db.execSQL("CREATE INDEX remindersEventIdIndex ON " + Tables.REMINDERS + " (" +
CalendarContract.Reminders.EVENT_ID +
");");
// This table stores the Calendar notifications that have gone off.
db.execSQL("CREATE TABLE " + Tables.CALENDAR_ALERTS + " (" +
CalendarContract.CalendarAlerts._ID + " INTEGER PRIMARY KEY," +
CalendarContract.CalendarAlerts.EVENT_ID + " INTEGER," +
CalendarContract.CalendarAlerts.BEGIN + " INTEGER NOT NULL," + // UTC millis
CalendarContract.CalendarAlerts.END + " INTEGER NOT NULL," + // UTC millis
CalendarContract.CalendarAlerts.ALARM_TIME + " INTEGER NOT NULL," + // UTC millis
// UTC millis
CalendarContract.CalendarAlerts.CREATION_TIME + " INTEGER NOT NULL DEFAULT 0," +
// UTC millis
CalendarContract.CalendarAlerts.RECEIVED_TIME + " INTEGER NOT NULL DEFAULT 0," +
// UTC millis
CalendarContract.CalendarAlerts.NOTIFY_TIME + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.CalendarAlerts.STATE + " INTEGER NOT NULL," +
CalendarContract.CalendarAlerts.MINUTES + " INTEGER," +
"UNIQUE (" +
CalendarContract.CalendarAlerts.ALARM_TIME + ", " +
CalendarContract.CalendarAlerts.BEGIN + ", " +
CalendarContract.CalendarAlerts.EVENT_ID + ")" +
");");
db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON " + Tables.CALENDAR_ALERTS + " (" +
CalendarContract.CalendarAlerts.EVENT_ID +
");");
db.execSQL("CREATE TABLE " + Tables.EXTENDED_PROPERTIES + " (" +
CalendarContract.ExtendedProperties._ID + " INTEGER PRIMARY KEY," +
CalendarContract.ExtendedProperties.EVENT_ID + " INTEGER," +
CalendarContract.ExtendedProperties.NAME + " TEXT," +
CalendarContract.ExtendedProperties.VALUE + " TEXT" +
");");
db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON " + Tables.EXTENDED_PROPERTIES
+ " (" +
CalendarContract.ExtendedProperties.EVENT_ID +
");");
createEventsView(db);
// Trigger to remove data tied to an event when we delete that event.
db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
"BEGIN " +
EVENTS_CLEANUP_TRIGGER_SQL +
"END");
// Triggers to update the color stored in an event or a calendar when
// the color_index is changed.
createColorsTriggers(db);
// Trigger to update exceptions when an original event updates its
// _sync_id
db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
scheduleSync(null /* all accounts */, false, null);
}
private void createEventsTable(SQLiteDatabase db) {
// IMPORTANT: when adding new columns, be sure to update ALLOWED_IN_EXCEPTION and
// DONT_CLONE_INTO_EXCEPTION in CalendarProvider2.
//
// TODO: do we need both dtend and duration?
// **When updating this be sure to also update LAST_SYNCED_EVENT_COLUMNS
db.execSQL("CREATE TABLE " + Tables.EVENTS + " (" +
CalendarContract.Events._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
CalendarContract.Events._SYNC_ID + " TEXT," +
CalendarContract.Events.DIRTY + " INTEGER," +
CalendarContract.Events.LAST_SYNCED + " INTEGER DEFAULT 0," +
CalendarContract.Events.CALENDAR_ID + " INTEGER NOT NULL," +
CalendarContract.Events.TITLE + " TEXT," +
CalendarContract.Events.EVENT_LOCATION + " TEXT," +
CalendarContract.Events.DESCRIPTION + " TEXT," +
CalendarContract.Events.EVENT_COLOR + " INTEGER," +
CalendarContract.Events.EVENT_COLOR_KEY + " TEXT," +
CalendarContract.Events.STATUS + " INTEGER," +
CalendarContract.Events.SELF_ATTENDEE_STATUS + " INTEGER NOT NULL DEFAULT 0," +
// dtstart in millis since epoch
CalendarContract.Events.DTSTART + " INTEGER," +
// dtend in millis since epoch
CalendarContract.Events.DTEND + " INTEGER," +
// timezone for event
CalendarContract.Events.EVENT_TIMEZONE + " TEXT," +
CalendarContract.Events.DURATION + " TEXT," +
CalendarContract.Events.ALL_DAY + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.ACCESS_LEVEL + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.AVAILABILITY + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.HAS_ALARM + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.HAS_EXTENDED_PROPERTIES + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.RRULE + " TEXT," +
CalendarContract.Events.RDATE + " TEXT," +
CalendarContract.Events.EXRULE + " TEXT," +
CalendarContract.Events.EXDATE + " TEXT," +
CalendarContract.Events.ORIGINAL_ID + " INTEGER," +
// ORIGINAL_SYNC_ID is the _sync_id of recurring event
CalendarContract.Events.ORIGINAL_SYNC_ID + " TEXT," +
// originalInstanceTime is in millis since epoch
CalendarContract.Events.ORIGINAL_INSTANCE_TIME + " INTEGER," +
CalendarContract.Events.ORIGINAL_ALL_DAY + " INTEGER," +
// lastDate is in millis since epoch
CalendarContract.Events.LAST_DATE + " INTEGER," +
CalendarContract.Events.HAS_ATTENDEE_DATA + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.GUESTS_CAN_MODIFY + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Events.GUESTS_CAN_INVITE_OTHERS + " INTEGER NOT NULL DEFAULT 1," +
CalendarContract.Events.GUESTS_CAN_SEE_GUESTS + " INTEGER NOT NULL DEFAULT 1," +
CalendarContract.Events.ORGANIZER + " STRING," +
CalendarContract.Events.DELETED + " INTEGER NOT NULL DEFAULT 0," +
// timezone for event with allDay events are in local timezone
CalendarContract.Events.EVENT_END_TIMEZONE + " TEXT," +
// SYNC_DATAX columns are available for use by sync adapters
CalendarContract.Events.SYNC_DATA1 + " TEXT," +
CalendarContract.Events.SYNC_DATA2 + " TEXT," +
CalendarContract.Events.SYNC_DATA3 + " TEXT," +
CalendarContract.Events.SYNC_DATA4 + " TEXT," +
CalendarContract.Events.SYNC_DATA5 + " TEXT," +
CalendarContract.Events.SYNC_DATA6 + " TEXT," +
CalendarContract.Events.SYNC_DATA7 + " TEXT," +
CalendarContract.Events.SYNC_DATA8 + " TEXT," +
CalendarContract.Events.SYNC_DATA9 + " TEXT," +
CalendarContract.Events.SYNC_DATA10 + " TEXT" + ");");
// **When updating this be sure to also update LAST_SYNCED_EVENT_COLUMNS
db.execSQL("CREATE INDEX eventsCalendarIdIndex ON " + Tables.EVENTS + " ("
+ CalendarContract.Events.CALENDAR_ID + ");");
}
private void createEventsTable307(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Events ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "_sync_id TEXT,"
+ "dirty INTEGER,"
+ "lastSynced INTEGER DEFAULT 0,"
+ "calendar_id INTEGER NOT NULL,"
+ "title TEXT,"
+ "eventLocation TEXT,"
+ "description TEXT,"
+ "eventColor INTEGER,"
+ "eventStatus INTEGER,"
+ "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0,"
// dtstart in millis since epoch
+ "dtstart INTEGER,"
// dtend in millis since epoch
+ "dtend INTEGER,"
// timezone for event
+ "eventTimezone TEXT,"
+ "duration TEXT,"
+ "allDay INTEGER NOT NULL DEFAULT 0,"
+ "accessLevel INTEGER NOT NULL DEFAULT 0,"
+ "availability INTEGER NOT NULL DEFAULT 0,"
+ "hasAlarm INTEGER NOT NULL DEFAULT 0,"
+ "hasExtendedProperties INTEGER NOT NULL DEFAULT 0,"
+ "rrule TEXT,"
+ "rdate TEXT,"
+ "exrule TEXT,"
+ "exdate TEXT,"
+ "original_id INTEGER,"
// ORIGINAL_SYNC_ID is the _sync_id of recurring event
+ "original_sync_id TEXT,"
// originalInstanceTime is in millis since epoch
+ "originalInstanceTime INTEGER,"
+ "originalAllDay INTEGER,"
// lastDate is in millis since epoch
+ "lastDate INTEGER,"
+ "hasAttendeeData INTEGER NOT NULL DEFAULT 0,"
+ "guestsCanModify INTEGER NOT NULL DEFAULT 0,"
+ "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1,"
+ "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1,"
+ "organizer STRING,"
+ "deleted INTEGER NOT NULL DEFAULT 0,"
// timezone for event with allDay events are in local timezone
+ "eventEndTimezone TEXT,"
// SYNC_DATAX columns are available for use by sync adapters
+ "sync_data1 TEXT,"
+ "sync_data2 TEXT,"
+ "sync_data3 TEXT,"
+ "sync_data4 TEXT,"
+ "sync_data5 TEXT,"
+ "sync_data6 TEXT,"
+ "sync_data7 TEXT,"
+ "sync_data8 TEXT,"
+ "sync_data9 TEXT,"
+ "sync_data10 TEXT);");
// **When updating this be sure to also update LAST_SYNCED_EVENT_COLUMNS
db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (calendar_id);");
}
// TODO Remove this method after merging all ICS upgrades
private void createEventsTable300(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Events (" +
"_id INTEGER PRIMARY KEY," +
"_sync_id TEXT," +
"_sync_version TEXT," +
// sync time in UTC
"_sync_time TEXT," +
"_sync_local_id INTEGER," +
"dirty INTEGER," +
// sync mark to filter out new rows
"_sync_mark INTEGER," +
"calendar_id INTEGER NOT NULL," +
"htmlUri TEXT," +
"title TEXT," +
"eventLocation TEXT," +
"description TEXT," +
"eventStatus INTEGER," +
"selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
"commentsUri TEXT," +
// dtstart in millis since epoch
"dtstart INTEGER," +
// dtend in millis since epoch
"dtend INTEGER," +
// timezone for event
"eventTimezone TEXT," +
"duration TEXT," +
"allDay INTEGER NOT NULL DEFAULT 0," +
"accessLevel INTEGER NOT NULL DEFAULT 0," +
"availability INTEGER NOT NULL DEFAULT 0," +
"hasAlarm INTEGER NOT NULL DEFAULT 0," +
"hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
"rrule TEXT," +
"rdate TEXT," +
"exrule TEXT," +
"exdate TEXT," +
// originalEvent is the _sync_id of recurring event
"original_sync_id TEXT," +
// originalInstanceTime is in millis since epoch
"originalInstanceTime INTEGER," +
"originalAllDay INTEGER," +
// lastDate is in millis since epoch
"lastDate INTEGER," +
"hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
"guestsCanModify INTEGER NOT NULL DEFAULT 0," +
"guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
"guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
"organizer STRING," +
"deleted INTEGER NOT NULL DEFAULT 0," +
// timezone for event with allDay events are in local timezone
"eventEndTimezone TEXT," +
// syncAdapterData is available for use by sync adapters
"sync_data1 TEXT);");
db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (calendar_id);");
}
private void createCalendarsTable303(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
"_id INTEGER PRIMARY KEY," +
"account_name TEXT," +
"account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"dirty INTEGER," +
"name TEXT," +
"displayName TEXT," +
"calendar_color INTEGER," +
"access_level INTEGER," +
"visible INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"calendar_location TEXT," +
"calendar_timezone TEXT," +
"ownerAccount TEXT, " +
"canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
"canModifyTimeZone INTEGER DEFAULT 1," +
"maxReminders INTEGER DEFAULT 5," +
"allowedReminders TEXT DEFAULT '0,1'," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"cal_sync1 TEXT," +
"cal_sync2 TEXT," +
"cal_sync3 TEXT," +
"cal_sync4 TEXT," +
"cal_sync5 TEXT," +
"cal_sync6 TEXT" +
");");
// Trigger to remove a calendar's events when we delete the calendar
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
"BEGIN " +
CALENDAR_CLEANUP_TRIGGER_SQL +
"END");
}
private void createColorsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Tables.COLORS + " (" +
CalendarContract.Colors._ID + " INTEGER PRIMARY KEY," +
CalendarContract.Colors.ACCOUNT_NAME + " TEXT NOT NULL," +
CalendarContract.Colors.ACCOUNT_TYPE + " TEXT NOT NULL," +
CalendarContract.Colors.DATA + " TEXT," +
CalendarContract.Colors.COLOR_TYPE + " INTEGER NOT NULL," +
CalendarContract.Colors.COLOR_KEY + " TEXT NOT NULL," +
CalendarContract.Colors.COLOR + " INTEGER NOT NULL" +
");");
}
public void createColorsTriggers(SQLiteDatabase db) {
db.execSQL(CREATE_EVENT_COLOR_UPDATE_TRIGGER);
db.execSQL(CREATE_CALENDAR_COLOR_UPDATE_TRIGGER);
}
private void createCalendarsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
CalendarContract.Calendars._ID + " INTEGER PRIMARY KEY," +
CalendarContract.Calendars.ACCOUNT_NAME + " TEXT," +
CalendarContract.Calendars.ACCOUNT_TYPE + " TEXT," +
CalendarContract.Calendars._SYNC_ID + " TEXT," +
CalendarContract.Calendars.DIRTY + " INTEGER," +
CalendarContract.Calendars.NAME + " TEXT," +
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME + " TEXT," +
CalendarContract.Calendars.CALENDAR_COLOR + " INTEGER," +
CalendarContract.Calendars.CALENDAR_COLOR_KEY + " TEXT," +
CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL + " INTEGER," +
CalendarContract.Calendars.VISIBLE + " INTEGER NOT NULL DEFAULT 1," +
CalendarContract.Calendars.SYNC_EVENTS + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Calendars.CALENDAR_LOCATION + " TEXT," +
CalendarContract.Calendars.CALENDAR_TIME_ZONE + " TEXT," +
CalendarContract.Calendars.OWNER_ACCOUNT + " TEXT, " +
CalendarContract.Calendars.CAN_ORGANIZER_RESPOND + " INTEGER NOT NULL DEFAULT 1," +
CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE + " INTEGER DEFAULT 1," +
CalendarContract.Calendars.CAN_PARTIALLY_UPDATE + " INTEGER DEFAULT 0," +
CalendarContract.Calendars.MAX_REMINDERS + " INTEGER DEFAULT 5," +
CalendarContract.Calendars.ALLOWED_REMINDERS + " TEXT DEFAULT '0,1'," +
CalendarContract.Calendars.ALLOWED_AVAILABILITY + " TEXT DEFAULT '0,1'," +
CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES + " TEXT DEFAULT '0,1,2'," +
CalendarContract.Calendars.DELETED + " INTEGER NOT NULL DEFAULT 0," +
CalendarContract.Calendars.CAL_SYNC1 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC2 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC3 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC4 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC5 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC6 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC7 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC8 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC9 + " TEXT," +
CalendarContract.Calendars.CAL_SYNC10 + " TEXT" +
");");
// Trigger to remove a calendar's events when we delete the calendar
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
"BEGIN " +
CALENDAR_CLEANUP_TRIGGER_SQL +
"END");
}
private void createCalendarsTable305(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Calendars (" +
"_id INTEGER PRIMARY KEY," +
"account_name TEXT," +
"account_type TEXT," +
"_sync_id TEXT," +
"dirty INTEGER," +
"name TEXT," +
"calendar_displayName TEXT," +
"calendar_color INTEGER," +
"calendar_access_level INTEGER," +
"visible INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"calendar_location TEXT," +
"calendar_timezone TEXT," +
"ownerAccount TEXT, " +
"canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
"canModifyTimeZone INTEGER DEFAULT 1," +
"canPartiallyUpdate INTEGER DEFAULT 0," +
"maxReminders INTEGER DEFAULT 5," +
"allowedReminders TEXT DEFAULT '0,1'," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"cal_sync1 TEXT," +
"cal_sync2 TEXT," +
"cal_sync3 TEXT," +
"cal_sync4 TEXT," +
"cal_sync5 TEXT," +
"cal_sync6 TEXT," +
"cal_sync7 TEXT," +
"cal_sync8 TEXT," +
"cal_sync9 TEXT," +
"cal_sync10 TEXT" +
");");
// Trigger to remove a calendar's events when we delete the calendar
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
"BEGIN " +
"DELETE FROM Events WHERE calendar_id=old._id;" +
"END");
}
private void createCalendarsTable300(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
"_id INTEGER PRIMARY KEY," +
"account_name TEXT," +
"account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"dirty INTEGER," +
"name TEXT," +
"displayName TEXT," +
"calendar_color INTEGER," +
"access_level INTEGER," +
"visible INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"calendar_location TEXT," +
"calendar_timezone TEXT," +
"ownerAccount TEXT, " +
"canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
"canModifyTimeZone INTEGER DEFAULT 1," +
"maxReminders INTEGER DEFAULT 5," +
"allowedReminders TEXT DEFAULT '0,1,2'," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"sync1 TEXT," +
"sync2 TEXT," +
"sync3 TEXT," +
"sync4 TEXT," +
"sync5 TEXT," +
"sync6 TEXT" +
");");
// Trigger to remove a calendar's events when we delete the calendar
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON " + Tables.CALENDARS + " " +
"BEGIN " +
CALENDAR_CLEANUP_TRIGGER_SQL +
"END");
}
private void createCalendarsTable205(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Calendars (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
"_sync_account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"_sync_dirty INTEGER," +
"name TEXT," +
"displayName TEXT," +
"color INTEGER," +
"access_level INTEGER," +
"visible INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"location TEXT," +
"timezone TEXT," +
"ownerAccount TEXT, " +
"canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
"canModifyTimeZone INTEGER DEFAULT 1, " +
"maxReminders INTEGER DEFAULT 5," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"sync1 TEXT," +
"sync2 TEXT," +
"sync3 TEXT," +
"sync4 TEXT," +
"sync5 TEXT," +
"sync6 TEXT" +
");");
createCalendarsCleanup200(db);
}
private void createCalendarsTable202(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Calendars (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
"_sync_account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"_sync_local_id INTEGER," +
"_sync_dirty INTEGER," +
"_sync_mark INTEGER," + // Used to filter out new rows
"name TEXT," +
"displayName TEXT," +
"color INTEGER," +
"access_level INTEGER," +
"selected INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"location TEXT," +
"timezone TEXT," +
"ownerAccount TEXT, " +
"organizerCanRespond INTEGER NOT NULL DEFAULT 1," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"sync1 TEXT," +
"sync2 TEXT," +
"sync3 TEXT," +
"sync4 TEXT," +
"sync5 TEXT" +
");");
createCalendarsCleanup200(db);
}
private void createCalendarsTable200(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Calendars (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
"_sync_account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"_sync_local_id INTEGER," +
"_sync_dirty INTEGER," +
"_sync_mark INTEGER," + // Used to filter out new rows
"name TEXT," +
"displayName TEXT," +
"hidden INTEGER NOT NULL DEFAULT 0," +
"color INTEGER," +
"access_level INTEGER," +
"selected INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"location TEXT," +
"timezone TEXT," +
"ownerAccount TEXT, " +
"organizerCanRespond INTEGER NOT NULL DEFAULT 1," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"sync1 TEXT," +
"sync2 TEXT," +
"sync3 TEXT" +
");");
createCalendarsCleanup200(db);
}
/** Trigger to remove a calendar's events when we delete the calendar */
private void createCalendarsCleanup200(SQLiteDatabase db) {
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
"BEGIN " +
"DELETE FROM Events WHERE calendar_id=old._id;" +
"END");
}
private void createCalendarMetaDataTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Tables.CALENDAR_META_DATA + " (" +
CalendarContract.CalendarMetaData._ID + " INTEGER PRIMARY KEY," +
CalendarContract.CalendarMetaData.LOCAL_TIMEZONE + " TEXT," +
CalendarContract.CalendarMetaData.MIN_INSTANCE + " INTEGER," + // UTC millis
CalendarContract.CalendarMetaData.MAX_INSTANCE + " INTEGER" + // UTC millis
");");
}
private void createCalendarMetaDataTable59(SQLiteDatabase db) {
db.execSQL("CREATE TABLE CalendarMetaData (" +
"_id INTEGER PRIMARY KEY," +
"localTimezone TEXT," +
"minInstance INTEGER," + // UTC millis
"maxInstance INTEGER" + // UTC millis
");");
}
private void createCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) {
// This is a hack because versioning skipped version number 61 of schema
// TODO after version 70 this can be removed
db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";");
// IF NOT EXISTS should be normal pattern for table creation
db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.CALENDAR_CACHE + " (" +
CalendarCache.COLUMN_NAME_ID + " INTEGER PRIMARY KEY," +
CalendarCache.COLUMN_NAME_KEY + " TEXT NOT NULL," +
CalendarCache.COLUMN_NAME_VALUE + " TEXT" +
");");
initCalendarCacheTable(db, oldTimezoneDbVersion);
updateCalendarCacheTable(db);
}
private void initCalendarCacheTable(SQLiteDatabase db, String oldTimezoneDbVersion) {
String timezoneDbVersion = (oldTimezoneDbVersion != null) ?
oldTimezoneDbVersion : CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION;
// Set the default timezone database version
db.execSQL("INSERT OR REPLACE INTO " + Tables.CALENDAR_CACHE +
" (" + CalendarCache.COLUMN_NAME_ID + ", " +
CalendarCache.COLUMN_NAME_KEY + ", " +
CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
CalendarCache.KEY_TIMEZONE_DATABASE_VERSION.hashCode() + "," +
"'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "'," +
"'" + timezoneDbVersion + "'" +
");");
}
private void updateCalendarCacheTable(SQLiteDatabase db) {
// Define the default timezone type for Instances timezone management
db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE +
" (" + CalendarCache.COLUMN_NAME_ID + ", " +
CalendarCache.COLUMN_NAME_KEY + ", " +
CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
CalendarCache.KEY_TIMEZONE_TYPE.hashCode() + "," +
"'" + CalendarCache.KEY_TIMEZONE_TYPE + "'," +
"'" + CalendarCache.TIMEZONE_TYPE_AUTO + "'" +
");");
String defaultTimezone = TimeZone.getDefault().getID();
// Define the default timezone for Instances
db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE +
" (" + CalendarCache.COLUMN_NAME_ID + ", " +
CalendarCache.COLUMN_NAME_KEY + ", " +
CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
CalendarCache.KEY_TIMEZONE_INSTANCES.hashCode() + "," +
"'" + CalendarCache.KEY_TIMEZONE_INSTANCES + "'," +
"'" + defaultTimezone + "'" +
");");
// Define the default previous timezone for Instances
db.execSQL("INSERT INTO " + Tables.CALENDAR_CACHE +
" (" + CalendarCache.COLUMN_NAME_ID + ", " +
CalendarCache.COLUMN_NAME_KEY + ", " +
CalendarCache.COLUMN_NAME_VALUE + ") VALUES (" +
CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS.hashCode() + "," +
"'" + CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + "'," +
"'" + defaultTimezone + "'" +
");");
}
private void initCalendarCacheTable203(SQLiteDatabase db, String oldTimezoneDbVersion) {
String timezoneDbVersion = (oldTimezoneDbVersion != null) ?
oldTimezoneDbVersion : "2009s";
// Set the default timezone database version
db.execSQL("INSERT OR REPLACE INTO CalendarCache" +
" (_id, " +
"key, " +
"value) VALUES (" +
"timezoneDatabaseVersion".hashCode() + "," +
"'timezoneDatabaseVersion'," +
"'" + timezoneDbVersion + "'" +
");");
}
private void updateCalendarCacheTableTo203(SQLiteDatabase db) {
// Define the default timezone type for Instances timezone management
db.execSQL("INSERT INTO CalendarCache" +
" (_id, key, value) VALUES (" +
"timezoneType".hashCode() + "," +
"'timezoneType'," +
"'auto'" +
");");
String defaultTimezone = TimeZone.getDefault().getID();
// Define the default timezone for Instances
db.execSQL("INSERT INTO CalendarCache" +
" (_id, key, value) VALUES (" +
"timezoneInstances".hashCode() + "," +
"'timezoneInstances'," +
"'" + defaultTimezone + "'" +
");");
// Define the default previous timezone for Instances
db.execSQL("INSERT INTO CalendarCache" +
" (_id, key, value) VALUES (" +
"timezoneInstancesPrevious".hashCode() + "," +
"'timezoneInstancesPrevious'," +
"'" + defaultTimezone + "'" +
");");
}
/**
* Removes orphaned data from the database. Specifically:
* <ul>
* <li>Attendees with an event_id for a nonexistent Event
* <li>Reminders with an event_id for a nonexistent Event
* </ul>
*/
static void removeOrphans(SQLiteDatabase db) {
if (false) { // debug mode
String SELECT_ATTENDEES_ORPHANS = "SELECT " +
Attendees._ID + ", " + Attendees.EVENT_ID + " FROM " + Tables.ATTENDEES +
" WHERE " + WHERE_ATTENDEES_ORPHANS;
Cursor cursor = null;
try {
Log.i(TAG, "Attendees orphans:");
cursor = db.rawQuery(SELECT_ATTENDEES_ORPHANS, null);
DatabaseUtils.dumpCursor(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
String SELECT_REMINDERS_ORPHANS = "SELECT " +
Attendees._ID + ", " + Reminders.EVENT_ID + " FROM " + Tables.REMINDERS +
" WHERE " + WHERE_REMINDERS_ORPHANS;
cursor = null;
try {
Log.i(TAG, "Reminders orphans:");
cursor = db.rawQuery(SELECT_REMINDERS_ORPHANS, null);
DatabaseUtils.dumpCursor(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
return;
}
Log.d(TAG, "Checking for orphaned entries");
int count;
count = db.delete(Tables.ATTENDEES, WHERE_ATTENDEES_ORPHANS, null);
if (count != 0) {
Log.i(TAG, "Deleted " + count + " orphaned Attendees");
}
count = db.delete(Tables.REMINDERS, WHERE_REMINDERS_ORPHANS, null);
if (count != 0) {
Log.i(TAG, "Deleted " + count + " orphaned Reminders");
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "Upgrading DB from version " + oldVersion + " to " + newVersion);
long startWhen = System.nanoTime();
if (oldVersion < 49) {
dropTables(db);
mSyncState.createDatabase(db);
return; // this was lossy
}
// From schema versions 59 to version 66, the CalendarMetaData table definition had lost
// the primary key leading to having the CalendarMetaData with multiple rows instead of
// only one. The Instance table was then corrupted (during Instance expansion we are using
// the localTimezone, minInstance and maxInstance from CalendarMetaData table.
// This boolean helps us tracking the need to recreate the CalendarMetaData table and
// clear the Instance table (and thus force an Instance expansion).
boolean recreateMetaDataAndInstances = (oldVersion >= 59 && oldVersion <= 66);
boolean createEventsView = false;
try {
if (oldVersion < 51) {
upgradeToVersion51(db); // From 50 or 51
oldVersion = 51;
}
if (oldVersion == 51) {
upgradeToVersion52(db);
oldVersion += 1;
}
if (oldVersion == 52) {
upgradeToVersion53(db);
oldVersion += 1;
}
if (oldVersion == 53) {
upgradeToVersion54(db);
oldVersion += 1;
}
if (oldVersion == 54) {
upgradeToVersion55(db);
oldVersion += 1;
}
if (oldVersion == 55 || oldVersion == 56) {
// Both require resync, so just schedule it once
upgradeResync(db);
}
if (oldVersion == 55) {
upgradeToVersion56(db);
oldVersion += 1;
}
if (oldVersion == 56) {
upgradeToVersion57(db);
oldVersion += 1;
}
if (oldVersion == 57) {
// Changes are undone upgrading to 60, so don't do anything.
oldVersion += 1;
}
if (oldVersion == 58) {
upgradeToVersion59(db);
oldVersion += 1;
}
if (oldVersion == 59) {
upgradeToVersion60(db);
createEventsView = true;
oldVersion += 1;
}
if (oldVersion == 60) {
upgradeToVersion61(db);
oldVersion += 1;
}
if (oldVersion == 61) {
upgradeToVersion62(db);
oldVersion += 1;
}
if (oldVersion == 62) {
createEventsView = true;
oldVersion += 1;
}
if (oldVersion == 63) {
upgradeToVersion64(db);
oldVersion += 1;
}
if (oldVersion == 64) {
createEventsView = true;
oldVersion += 1;
}
if (oldVersion == 65) {
upgradeToVersion66(db);
oldVersion += 1;
}
if (oldVersion == 66) {
// Changes are done thru recreateMetaDataAndInstances() method
oldVersion += 1;
}
if (recreateMetaDataAndInstances) {
recreateMetaDataAndInstances67(db);
}
if (oldVersion == 67 || oldVersion == 68) {
upgradeToVersion69(db);
oldVersion = 69;
}
// 69. 70 are for Froyo/old Gingerbread only and 100s are for Gingerbread only
// 70 and 71 have been for Honeycomb but no more used
// 72 and 73 and 74 were for Honeycomb only but are considered as obsolete for enabling
// room for Froyo version numbers
if(oldVersion == 69) {
upgradeToVersion200(db);
createEventsView = true;
oldVersion = 200;
}
if (oldVersion == 70) {
upgradeToVersion200(db);
oldVersion = 200;
}
if (oldVersion == 100) {
// note we skip past v101 and v102
upgradeToVersion200(db);
oldVersion = 200;
}
boolean need203Update = true;
if (oldVersion == 101 || oldVersion == 102) {
// v101 is v100 plus updateCalendarCacheTableTo203().
// v102 is v101 with Event._id changed to autoincrement.
// Upgrade to 200 and skip the 203 update.
upgradeToVersion200(db);
oldVersion = 200;
need203Update = false;
}
if (oldVersion == 200) {
upgradeToVersion201(db);
oldVersion += 1;
}
if (oldVersion == 201) {
upgradeToVersion202(db);
createEventsView = true;
oldVersion += 1;
}
if (oldVersion == 202) {
if (need203Update) {
upgradeToVersion203(db);
}
oldVersion += 1;
}
if (oldVersion == 203) {
createEventsView = true;
oldVersion += 1;
}
if (oldVersion == 206) {
// v206 exists only in HC (change Event._id to autoincrement). Otherwise
// identical to v204, so back it up and let the upgrade path continue.
oldVersion -= 2;
}
if (oldVersion == 204) {
// This is an ICS update, all following use 300+ versions.
upgradeToVersion205(db);
createEventsView = true;
oldVersion += 1;
}
if (oldVersion == 205) {
// Move ICS updates to 300 range
upgradeToVersion300(db);
createEventsView = true;
oldVersion = 300;
}
if (oldVersion == 300) {
upgradeToVersion301(db);
createEventsView = true;
oldVersion++;
}
if (oldVersion == 301) {
upgradeToVersion302(db);
oldVersion++;
}
if (oldVersion == 302) {
upgradeToVersion303(db);
oldVersion++;
createEventsView = true;
}
if (oldVersion == 303) {
upgradeToVersion304(db);
oldVersion++;
createEventsView = true;
}
if (oldVersion == 304) {
upgradeToVersion305(db);
oldVersion++;
createEventsView = true;
}
if (oldVersion == 305) {
upgradeToVersion306(db);
// force a sync to update edit url and etag
scheduleSync(null /* all accounts */, false, null);
oldVersion++;
}
if (oldVersion == 306) {
upgradeToVersion307(db);
oldVersion++;
}
if (oldVersion == 307) {
upgradeToVersion308(db);
oldVersion++;
createEventsView = true;
}
if (createEventsView) {
createEventsView(db);
}
if (oldVersion != DATABASE_VERSION) {
Log.e(TAG, "Need to recreate Calendar schema because of "
+ "unknown Calendar database version: " + oldVersion);
dropTables(db);
bootstrapDB(db);
oldVersion = DATABASE_VERSION;
} else {
removeOrphans(db);
}
} catch (SQLiteException e) {
Log.e(TAG, "onUpgrade: SQLiteException, recreating db. ", e);
Log.e(TAG, "(oldVersion was " + oldVersion + ")");
dropTables(db);
bootstrapDB(db);
return; // this was lossy
}
long endWhen = System.nanoTime();
Log.d(TAG, "Calendar upgrade took " + ((endWhen - startWhen) / 1000000) + "ms");
/**
* db versions < 100 correspond to Froyo and earlier. Gingerbread bumped
* the db versioning to 100. Honeycomb bumped it to 200. ICS will begin
* in 300. At each major release we should jump to the next
* centiversion.
*/
}
/**
* If the user_version of the database if between 59 and 66 (those versions has been deployed
* with no primary key for the CalendarMetaData table)
*/
private void recreateMetaDataAndInstances67(SQLiteDatabase db) {
// Recreate the CalendarMetaData table with correct primary key
db.execSQL("DROP TABLE CalendarMetaData;");
createCalendarMetaDataTable59(db);
// Also clean the Instance table as this table may be corrupted
db.execSQL("DELETE FROM Instances;");
}
private static boolean fixAllDayTime(Time time, String timezone, Long timeInMillis) {
time.set(timeInMillis);
if(time.hour != 0 || time.minute != 0 || time.second != 0) {
time.hour = 0;
time.minute = 0;
time.second = 0;
return true;
}
return false;
}
@VisibleForTesting
void upgradeToVersion308(SQLiteDatabase db) {
/*
* Changes from version 307 to 308:
* - add Colors table to db
* - add eventColor_index to Events table
* - add calendar_color_index to Calendars table
* - add allowedAttendeeTypes to Calendars table
* - add allowedAvailability to Calendars table
*/
createColorsTable(db);
db.execSQL("ALTER TABLE Calendars ADD COLUMN allowedAvailability TEXT DEFAULT '0,1';");
db.execSQL("ALTER TABLE Calendars ADD COLUMN allowedAttendeeTypes TEXT DEFAULT '0,1,2';");
db.execSQL("ALTER TABLE Calendars ADD COLUMN calendar_color_index TEXT;");
db.execSQL("ALTER TABLE Events ADD COLUMN eventColor_index TEXT;");
// Default Exchange calendars to be supporting the 'tentative'
// availability as well
db.execSQL("UPDATE Calendars SET allowedAvailability='0,1,2' WHERE _id IN "
+ "(SELECT _id FROM Calendars WHERE account_type='com.android.exchange');");
// Triggers to update the color stored in an event or a calendar when
// the color_index is changed.
createColorsTriggers(db);
}
@VisibleForTesting
void upgradeToVersion307(SQLiteDatabase db) {
/*
* Changes from version 306 to 307:
* - Changed _id field to AUTOINCREMENT
*/
db.execSQL("ALTER TABLE Events RENAME TO Events_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
db.execSQL("DROP TRIGGER IF EXISTS original_sync_update");
db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
createEventsTable307(db);
String FIELD_LIST =
"_id, " +
"_sync_id, " +
"dirty, " +
"lastSynced," +
"calendar_id, " +
"title, " +
"eventLocation, " +
"description, " +
"eventColor, " +
"eventStatus, " +
"selfAttendeeStatus, " +
"dtstart, " +
"dtend, " +
"eventTimezone, " +
"duration, " +
"allDay, " +
"accessLevel, " +
"availability, " +
"hasAlarm, " +
"hasExtendedProperties, " +
"rrule, " +
"rdate, " +
"exrule, " +
"exdate, " +
"original_id," +
"original_sync_id, " +
"originalInstanceTime, " +
"originalAllDay, " +
"lastDate, " +
"hasAttendeeData, " +
"guestsCanModify, " +
"guestsCanInviteOthers, " +
"guestsCanSeeGuests, " +
"organizer, " +
"deleted, " +
"eventEndTimezone, " +
"sync_data1," +
"sync_data2," +
"sync_data3," +
"sync_data4," +
"sync_data5," +
"sync_data6," +
"sync_data7," +
"sync_data8," +
"sync_data9," +
"sync_data10 ";
// copy fields from old to new
db.execSQL("INSERT INTO Events (" + FIELD_LIST + ") SELECT " + FIELD_LIST +
"FROM Events_Backup;");
db.execSQL("DROP TABLE Events_Backup;");
// Trigger to remove data tied to an event when we delete that event.
db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
"BEGIN " + EVENTS_CLEANUP_TRIGGER_SQL + "END");
// Trigger to update exceptions when an original event updates its
// _sync_id
db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
}
@VisibleForTesting
void upgradeToVersion306(SQLiteDatabase db) {
/*
* The following changes are for google.com accounts only.
*
* Change event id's from ".../private/full/... to .../events/...
* Set Calendars.canPartiallyUpdate to 1 to support partial updates
* Nuke sync state so we re-sync with a fresh etag and edit url
*
* We need to drop the original_sync_update trigger because it fires whenever the
* sync_id field is touched, and dramatically slows this operation.
*/
db.execSQL("DROP TRIGGER IF EXISTS original_sync_update");
db.execSQL("UPDATE Events SET "
+ "_sync_id = REPLACE(_sync_id, '/private/full/', '/events/'), "
+ "original_sync_id = REPLACE(original_sync_id, '/private/full/', '/events/') "
+ "WHERE _id IN (SELECT Events._id FROM Events "
+ "JOIN Calendars ON Events.calendar_id = Calendars._id "
+ "WHERE account_type = 'com.google')"
);
db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
db.execSQL("UPDATE Calendars SET canPartiallyUpdate = 1 WHERE account_type = 'com.google'");
db.execSQL("DELETE FROM _sync_state WHERE account_type = 'com.google'");
}
@VisibleForTesting
void upgradeToVersion305(SQLiteDatabase db) {
/*
* Changes from version 304 to 305:
* -Add CAL_SYNC columns up to 10
* -Rename Calendars.access_level to calendar_access_level
* -Rename calendars _sync_version to cal_sync7
* -Rename calendars _sync_time to cal_sync8
* -Rename displayName to calendar_displayName
* -Rename _sync_local_id to sync_data2
* -Rename htmlUri to sync_data3
* -Rename events _sync_version to sync_data4
* -Rename events _sync_time to sync_data5
* -Rename commentsUri to sync_data6
* -Migrate Events _sync_mark to sync_data8
* -Change sync_data2 from INTEGER to TEXT
* -Change sync_data8 from INTEGER to TEXT
* -Add SYNC_DATA columns up to 10
* -Add EVENT_COLOR to Events table
*/
// rename old table, create new table with updated layout
db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
createCalendarsTable305(db);
// copy fields from old to new
db.execSQL("INSERT INTO Calendars (" +
"_id, " +
"account_name, " +
"account_type, " +
"_sync_id, " +
"cal_sync7, " + // rename from _sync_version
"cal_sync8, " + // rename from _sync_time
"dirty, " +
"name, " +
"calendar_displayName, " + // rename from displayName
"calendar_color, " +
"calendar_access_level, " + // rename from access_level
"visible, " +
"sync_events, " +
"calendar_location, " +
"calendar_timezone, " +
"ownerAccount, " +
"canOrganizerRespond, " +
"canModifyTimeZone, " +
"maxReminders, " +
"allowedReminders, " +
"deleted, " +
"canPartiallyUpdate," +
"cal_sync1, " +
"cal_sync2, " +
"cal_sync3, " +
"cal_sync4, " +
"cal_sync5, " +
"cal_sync6) " +
"SELECT " +
"_id, " +
"account_name, " +
"account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"dirty, " +
"name, " +
"displayName, " +
"calendar_color, " +
"access_level, " +
"visible, " +
"sync_events, " +
"calendar_location, " +
"calendar_timezone, " +
"ownerAccount, " +
"canOrganizerRespond, " +
"canModifyTimeZone, " +
"maxReminders, " +
"allowedReminders, " +
"deleted, " +
"canPartiallyUpdate," +
"cal_sync1, " +
"cal_sync2, " +
"cal_sync3, " +
"cal_sync4, " +
"cal_sync5, " +
"cal_sync6 " +
"FROM Calendars_Backup;");
// drop the old table
db.execSQL("DROP TABLE Calendars_Backup;");
db.execSQL("ALTER TABLE Events RENAME TO Events_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
// 305 and 307 can share the same createEventsTable implementation, because the
// addition of "autoincrement" to _ID doesn't affect the upgrade path. (Note that
// much older databases may also already have autoincrement set because the change
// was back-ported.)
createEventsTable307(db);
// copy fields from old to new
db.execSQL("INSERT INTO Events (" +
"_id, " +
"_sync_id, " +
"sync_data4, " + // renamed from _sync_version
"sync_data5, " + // renamed from _sync_time
"sync_data2, " + // renamed from _sync_local_id
"dirty, " +
"sync_data8, " + // renamed from _sync_mark
"calendar_id, " +
"sync_data3, " + // renamed from htmlUri
"title, " +
"eventLocation, " +
"description, " +
"eventStatus, " +
"selfAttendeeStatus, " +
"sync_data6, " + // renamed from commentsUri
"dtstart, " +
"dtend, " +
"eventTimezone, " +
"eventEndTimezone, " +
"duration, " +
"allDay, " +
"accessLevel, " +
"availability, " +
"hasAlarm, " +
"hasExtendedProperties, " +
"rrule, " +
"rdate, " +
"exrule, " +
"exdate, " +
"original_id," +
"original_sync_id, " +
"originalInstanceTime, " +
"originalAllDay, " +
"lastDate, " +
"hasAttendeeData, " +
"guestsCanModify, " +
"guestsCanInviteOthers, " +
"guestsCanSeeGuests, " +
"organizer, " +
"deleted, " +
"sync_data7," +
"lastSynced," +
"sync_data1) " +
"SELECT " +
"_id, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"dirty, " +
"_sync_mark, " +
"calendar_id, " +
"htmlUri, " +
"title, " +
"eventLocation, " +
"description, " +
"eventStatus, " +
"selfAttendeeStatus, " +
"commentsUri, " +
"dtstart, " +
"dtend, " +
"eventTimezone, " +
"eventEndTimezone, " +
"duration, " +
"allDay, " +
"accessLevel, " +
"availability, " +
"hasAlarm, " +
"hasExtendedProperties, " +
"rrule, " +
"rdate, " +
"exrule, " +
"exdate, " +
"original_id," +
"original_sync_id, " +
"originalInstanceTime, " +
"originalAllDay, " +
"lastDate, " +
"hasAttendeeData, " +
"guestsCanModify, " +
"guestsCanInviteOthers, " +
"guestsCanSeeGuests, " +
"organizer, " +
"deleted, " +
"sync_data7," +
"lastSynced," +
"sync_data1 " +
"FROM Events_Backup;"
);
db.execSQL("DROP TABLE Events_Backup;");
// Trigger to remove data tied to an event when we delete that event.
db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
"BEGIN " +
EVENTS_CLEANUP_TRIGGER_SQL +
"END");
// Trigger to update exceptions when an original event updates its
// _sync_id
db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
}
@VisibleForTesting
void upgradeToVersion304(SQLiteDatabase db) {
/*
* Changes from version 303 to 304:
* - add canPartiallyUpdate to Calendars table
* - add sync_data7 to Calendars to Events table
* - add lastSynced to Calendars to Events table
*/
db.execSQL("ALTER TABLE Calendars ADD COLUMN canPartiallyUpdate INTEGER DEFAULT 0;");
db.execSQL("ALTER TABLE Events ADD COLUMN sync_data7 TEXT;");
db.execSQL("ALTER TABLE Events ADD COLUMN lastSynced INTEGER DEFAULT 0;");
}
@VisibleForTesting
void upgradeToVersion303(SQLiteDatabase db) {
/*
* Changes from version 302 to 303:
* - change SYNCx columns to CAL_SYNCx
*/
// rename old table, create new table with updated layout
db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
createCalendarsTable303(db);
// copy fields from old to new
db.execSQL("INSERT INTO Calendars (" +
"_id, " +
"account_name, " +
"account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"dirty, " +
"name, " +
"displayName, " +
"calendar_color, " +
"access_level, " +
"visible, " +
"sync_events, " +
"calendar_location, " +
"calendar_timezone, " +
"ownerAccount, " +
"canOrganizerRespond, " +
"canModifyTimeZone, " +
"maxReminders, " +
"allowedReminders, " +
"deleted, " +
"cal_sync1, " + // rename from sync1
"cal_sync2, " + // rename from sync2
"cal_sync3, " + // rename from sync3
"cal_sync4, " + // rename from sync4
"cal_sync5, " + // rename from sync5
"cal_sync6) " + // rename from sync6
"SELECT " +
"_id, " +
"account_name, " +
"account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"dirty, " +
"name, " +
"displayName, " +
"calendar_color, " +
"access_level, " +
"visible, " +
"sync_events, " +
"calendar_location, " +
"calendar_timezone, " +
"ownerAccount, " +
"canOrganizerRespond, " +
"canModifyTimeZone, " +
"maxReminders, " +
"allowedReminders," +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4," +
"sync5," +
"sync6 " +
"FROM Calendars_Backup;"
);
// drop the old table
db.execSQL("DROP TABLE Calendars_Backup;");
}
@VisibleForTesting
void upgradeToVersion302(SQLiteDatabase db) {
/*
* Changes from version 301 to 302
* - Move Exchange eventEndTimezone values to SYNC_DATA1
*/
db.execSQL("UPDATE Events SET sync_data1=eventEndTimezone WHERE calendar_id IN "
+ "(SELECT _id FROM Calendars WHERE account_type='com.android.exchange');");
db.execSQL("UPDATE Events SET eventEndTimezone=NULL WHERE calendar_id IN "
+ "(SELECT _id FROM Calendars WHERE account_type='com.android.exchange');");
}
@VisibleForTesting
void upgradeToVersion301(SQLiteDatabase db) {
/*
* Changes from version 300 to 301
* - Added original_id column to Events table
* - Added triggers to keep original_id and original_sync_id in sync
*/
db.execSQL("DROP TRIGGER IF EXISTS " + SYNC_ID_UPDATE_TRIGGER_NAME + ";");
db.execSQL("ALTER TABLE Events ADD COLUMN original_id INTEGER;");
// Fill in the original_id for all events that have an original_sync_id
db.execSQL("UPDATE Events set original_id=" +
"(SELECT Events2._id FROM Events AS Events2 " +
"WHERE Events2._sync_id=Events.original_sync_id) " +
"WHERE Events.original_sync_id NOT NULL");
// Trigger to update exceptions when an original event updates its
// _sync_id
db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
}
@VisibleForTesting
void upgradeToVersion300(SQLiteDatabase db) {
/*
* Changes from version 205 to 300:
* - rename _sync_account to account_name in Calendars table
* - remove _sync_account from Events table
* - rename _sync_account_type to account_type in Calendars table
* - remove _sync_account_type from Events table
* - rename _sync_dirty to dirty in Calendars/Events table
* - rename color to calendar_color in Calendars table
* - rename location to calendar_location in Calendars table
* - rename timezone to calendar_timezone in Calendars table
* - add allowedReminders in Calendars table
* - rename visibility to accessLevel in Events table
* - rename transparency to availability in Events table
* - rename originalEvent to original_sync_id in Events table
* - remove dtstart2 and dtend2 from Events table
* - rename syncAdapterData to sync_data1 in Events table
*/
// rename old table, create new table with updated layout
db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup;");
createCalendarsTable300(db);
// copy fields from old to new
db.execSQL("INSERT INTO Calendars (" +
"_id, " +
"account_name, " + // rename from _sync_account
"account_type, " + // rename from _sync_account_type
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"dirty, " + // rename from _sync_dirty
"name, " +
"displayName, " +
"calendar_color, " + // rename from color
"access_level, " +
"visible, " +
"sync_events, " +
"calendar_location, " + // rename from location
"calendar_timezone, " + // rename from timezone
"ownerAccount, " +
"canOrganizerRespond, " +
"canModifyTimeZone, " +
"maxReminders, " +
"allowedReminders," +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4," +
"sync5," +
"sync6) " +
"SELECT " +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_dirty, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"visible, " +
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"canOrganizerRespond, " +
"canModifyTimeZone, " +
"maxReminders, " +
"'0,1,2,3'," +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4, " +
"sync5, " +
"sync6 " +
"FROM Calendars_Backup;"
);
/* expand the set of allowed reminders for Google calendars to include email */
db.execSQL("UPDATE Calendars SET allowedReminders = '0,1,2' " +
"WHERE account_type = 'com.google'");
// drop the old table
db.execSQL("DROP TABLE Calendars_Backup;");
db.execSQL("ALTER TABLE Events RENAME TO Events_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS events_insert");
db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
db.execSQL("DROP INDEX IF EXISTS eventSyncAccountAndIdIndex");
db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
createEventsTable300(db);
// copy fields from old to new
db.execSQL("INSERT INTO Events (" +
"_id, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"dirty, " + // renamed from _sync_dirty
"_sync_mark, " +
"calendar_id, " +
"htmlUri, " +
"title, " +
"eventLocation, " +
"description, " +
"eventStatus, " +
"selfAttendeeStatus, " +
"commentsUri, " +
"dtstart, " +
"dtend, " +
"eventTimezone, " +
"eventEndTimezone, " + // renamed from eventTimezone2
"duration, " +
"allDay, " +
"accessLevel, " + // renamed from visibility
"availability, " + // renamed from transparency
"hasAlarm, " +
"hasExtendedProperties, " +
"rrule, " +
"rdate, " +
"exrule, " +
"exdate, " +
"original_sync_id, " + // renamed from originalEvent
"originalInstanceTime, " +
"originalAllDay, " +
"lastDate, " +
"hasAttendeeData, " +
"guestsCanModify, " +
"guestsCanInviteOthers, " +
"guestsCanSeeGuests, " +
"organizer, " +
"deleted, " +
"sync_data1) " + // renamed from syncAdapterData
"SELECT " +
"_id, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"_sync_dirty, " +
"_sync_mark, " +
"calendar_id, " +
"htmlUri, " +
"title, " +
"eventLocation, " +
"description, " +
"eventStatus, " +
"selfAttendeeStatus, " +
"commentsUri, " +
"dtstart, " +
"dtend, " +
"eventTimezone, " +
"eventTimezone2, " +
"duration, " +
"allDay, " +
"visibility, " +
"transparency, " +
"hasAlarm, " +
"hasExtendedProperties, " +
"rrule, " +
"rdate, " +
"exrule, " +
"exdate, " +
"originalEvent, " +
"originalInstanceTime, " +
"originalAllDay, " +
"lastDate, " +
"hasAttendeeData, " +
"guestsCanModify, " +
"guestsCanInviteOthers, " +
"guestsCanSeeGuests, " +
"organizer, " +
"deleted, " +
"syncAdapterData " +
"FROM Events_Backup;"
);
db.execSQL("DROP TABLE Events_Backup;");
// Trigger to remove data tied to an event when we delete that event.
db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON " + Tables.EVENTS + " " +
"BEGIN " +
EVENTS_CLEANUP_TRIGGER_SQL +
"END");
}
@VisibleForTesting
void upgradeToVersion205(SQLiteDatabase db) {
/*
* Changes from version 204 to 205:
* - rename+reorder "_sync_mark" to "sync6" (and change type from INTEGER to TEXT)
* - rename "selected" to "visible"
* - rename "organizerCanRespond" to "canOrganizerRespond"
* - add "canModifyTimeZone"
* - add "maxReminders"
* - remove "_sync_local_id" (a/k/a _SYNC_DATA)
*/
// rename old table, create new table with updated layout
db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
createCalendarsTable205(db);
// copy fields from old to new
db.execSQL("INSERT INTO Calendars (" +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_dirty, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"visible, " + // rename from "selected"
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"canOrganizerRespond, " + // rename from "organizerCanRespond"
"canModifyTimeZone, " +
"maxReminders, " +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4," +
"sync5," +
"sync6) " + // rename/reorder from _sync_mark
"SELECT " +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_dirty, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"selected, " +
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"organizerCanRespond, " +
"1, " +
"5, " +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4, " +
"sync5, " +
"_sync_mark " +
"FROM Calendars_Backup;"
);
// set these fields appropriately for Exchange events
db.execSQL("UPDATE Calendars SET canModifyTimeZone=0, maxReminders=1 " +
"WHERE _sync_account_type='com.android.exchange'");
// drop the old table
db.execSQL("DROP TABLE Calendars_Backup;");
}
@VisibleForTesting
void upgradeToVersion203(SQLiteDatabase db) {
// Same as Gingerbread version 100
Cursor cursor = db.rawQuery("SELECT value FROM CalendarCache WHERE key=?",
new String[] {"timezoneDatabaseVersion"});
String oldTimezoneDbVersion = null;
if (cursor != null && cursor.moveToNext()) {
try {
oldTimezoneDbVersion = cursor.getString(0);
} finally {
cursor.close();
}
// Also clean the CalendarCache table
db.execSQL("DELETE FROM CalendarCache;");
}
initCalendarCacheTable203(db, oldTimezoneDbVersion);
// Same as Gingerbread version 101
updateCalendarCacheTableTo203(db);
}
@VisibleForTesting
void upgradeToVersion202(SQLiteDatabase db) {
// We will drop the "hidden" column from the calendar schema and add the "sync5" column
db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
createCalendarsTable202(db);
// Populate the new Calendars table and put into the "sync5" column the value of the
// old "hidden" column
db.execSQL("INSERT INTO Calendars (" +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"_sync_dirty, " +
"_sync_mark, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"selected, " +
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"organizerCanRespond, " +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4," +
"sync5) " +
"SELECT " +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"_sync_dirty, " +
"_sync_mark, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"selected, " +
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"organizerCanRespond, " +
"deleted, " +
"sync1, " +
"sync2, " +
"sync3, " +
"sync4, " +
"hidden " +
"FROM Calendars_Backup;"
);
// Drop the backup table
db.execSQL("DROP TABLE Calendars_Backup;");
}
@VisibleForTesting
void upgradeToVersion201(SQLiteDatabase db) {
db.execSQL("ALTER TABLE Calendars ADD COLUMN sync4 TEXT;");
}
@VisibleForTesting
void upgradeToVersion200(SQLiteDatabase db) {
// we cannot use here a Calendar.Calendars,URL constant for "url" as we are trying to make
// it disappear so we are keeping the hardcoded name "url" in all the SQLs
db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
createCalendarsTable200(db);
// Populate the new Calendars table except the SYNC2 / SYNC3 columns
db.execSQL("INSERT INTO Calendars (" +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"_sync_dirty, " +
"_sync_mark, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"selected, " +
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"organizerCanRespond, " +
"deleted, " +
"sync1) " +
"SELECT " +
"_id, " +
"_sync_account, " +
"_sync_account_type, " +
"_sync_id, " +
"_sync_version, " +
"_sync_time, " +
"_sync_local_id, " +
"_sync_dirty, " +
"_sync_mark, " +
"name, " +
"displayName, " +
"color, " +
"access_level, " +
"selected, " +
"sync_events, " +
"location, " +
"timezone, " +
"ownerAccount, " +
"organizerCanRespond, " +
"0, " +
"url " +
"FROM Calendars_Backup;"
);
// Populate SYNC2 and SYNC3 columns - SYNC1 represent the old "url" column
// We will need to iterate over all the "com.google" type of calendars
String selectSql = "SELECT _id, url" +
" FROM Calendars_Backup" +
" WHERE _sync_account_type='com.google'" +
" AND url IS NOT NULL;";
String updateSql = "UPDATE Calendars SET " +
"sync2=?, " + // edit Url
"sync3=? " + // self Url
"WHERE _id=?;";
Cursor cursor = db.rawQuery(selectSql, null /* selection args */);
if (cursor != null && cursor.getCount() > 0) {
try {
Object[] bindArgs = new Object[3];
while (cursor.moveToNext()) {
Long id = cursor.getLong(0);
String url = cursor.getString(1);
String selfUrl = getSelfUrlFromEventsUrl(url);
String editUrl = getEditUrlFromEventsUrl(url);
bindArgs[0] = editUrl;
bindArgs[1] = selfUrl;
bindArgs[2] = id;
db.execSQL(updateSql, bindArgs);
}
} finally {
cursor.close();
}
}
// Drop the backup table
db.execSQL("DROP TABLE Calendars_Backup;");
}
@VisibleForTesting
static void upgradeToVersion69(SQLiteDatabase db) {
// Clean up allDay events which could be in an invalid state from an earlier version
// Some allDay events had hour, min, sec not set to zero, which throws elsewhere. This
// will go through the allDay events and make sure they have proper values and are in the
// correct timezone. Verifies that dtstart and dtend are in UTC and at midnight, that
// eventTimezone is set to UTC, tries to make sure duration is in days, and that dtstart2
// and dtend2 are at midnight in their timezone.
final String sql = "SELECT _id, " +
"dtstart, " +
"dtend, " +
"duration, " +
"dtstart2, " +
"dtend2, " +
"eventTimezone, " +
"eventTimezone2, " +
"rrule " +
"FROM Events " +
"WHERE allDay=?";
Cursor cursor = db.rawQuery(sql, new String[] {"1"});
if (cursor != null) {
try {
String timezone;
String timezone2;
String duration;
Long dtstart;
Long dtstart2;
Long dtend;
Long dtend2;
Time time = new Time();
Long id;
// some things need to be in utc so we call this frequently, cache to make faster
final String utc = Time.TIMEZONE_UTC;
while (cursor.moveToNext()) {
String rrule = cursor.getString(8);
id = cursor.getLong(0);
dtstart = cursor.getLong(1);
dtstart2 = null;
timezone = cursor.getString(6);
timezone2 = cursor.getString(7);
duration = cursor.getString(3);
if (TextUtils.isEmpty(rrule)) {
// For non-recurring events dtstart and dtend should both have values
// and duration should be null.
dtend = cursor.getLong(2);
dtend2 = null;
// Since we made all three of these at the same time if timezone2 exists
// so should dtstart2 and dtend2.
if(!TextUtils.isEmpty(timezone2)) {
dtstart2 = cursor.getLong(4);
dtend2 = cursor.getLong(5);
}
boolean update = false;
if (!TextUtils.equals(timezone, utc)) {
update = true;
timezone = utc;
}
time.clear(timezone);
update |= fixAllDayTime(time, timezone, dtstart);
dtstart = time.normalize(false);
time.clear(timezone);
update |= fixAllDayTime(time, timezone, dtend);
dtend = time.normalize(false);
if (dtstart2 != null) {
time.clear(timezone2);
update |= fixAllDayTime(time, timezone2, dtstart2);
dtstart2 = time.normalize(false);
}
if (dtend2 != null) {
time.clear(timezone2);
update |= fixAllDayTime(time, timezone2, dtend2);
dtend2 = time.normalize(false);
}
if (!TextUtils.isEmpty(duration)) {
update = true;
}
if (update) {
// enforce duration being null
db.execSQL("UPDATE Events SET " +
"dtstart=?, " +
"dtend=?, " +
"dtstart2=?, " +
"dtend2=?, " +
"duration=?, " +
"eventTimezone=?, " +
"eventTimezone2=? " +
"WHERE _id=?",
new Object[] {
dtstart,
dtend,
dtstart2,
dtend2,
null,
timezone,
timezone2,
id}
);
}
} else {
// For recurring events only dtstart and duration should be used.
// We ignore dtend since it will be overwritten if the event changes to a
// non-recurring event and won't be used otherwise.
if(!TextUtils.isEmpty(timezone2)) {
dtstart2 = cursor.getLong(4);
}
boolean update = false;
if (!TextUtils.equals(timezone, utc)) {
update = true;
timezone = utc;
}
time.clear(timezone);
update |= fixAllDayTime(time, timezone, dtstart);
dtstart = time.normalize(false);
if (dtstart2 != null) {
time.clear(timezone2);
update |= fixAllDayTime(time, timezone2, dtstart2);
dtstart2 = time.normalize(false);
}
if (TextUtils.isEmpty(duration)) {
// If duration was missing assume a 1 day duration
duration = "P1D";
update = true;
} else {
int len = duration.length();
// TODO fix durations in other formats as well
if (duration.charAt(0) == 'P' &&
duration.charAt(len - 1) == 'S') {
int seconds = Integer.parseInt(duration.substring(1, len - 1));
int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
duration = "P" + days + "D";
update = true;
}
}
if (update) {
// If there were other problems also enforce dtend being null
db.execSQL("UPDATE Events SET " +
"dtstart=?, " +
"dtend=?, " +
"dtstart2=?, " +
"dtend2=?, " +
"duration=?," +
"eventTimezone=?, " +
"eventTimezone2=? " +
"WHERE _id=?",
new Object[] {
dtstart,
null,
dtstart2,
null,
duration,
timezone,
timezone2,
id}
);
}
}
}
} finally {
cursor.close();
}
}
}
private void upgradeToVersion66(SQLiteDatabase db) {
// Add a column to indicate whether the event organizer can respond to his own events
// The UI should not show attendee status for events in calendars with this column = 0
db.execSQL("ALTER TABLE Calendars" +
" ADD COLUMN organizerCanRespond INTEGER NOT NULL DEFAULT 1;");
}
private void upgradeToVersion64(SQLiteDatabase db) {
// Add a column that may be used by sync adapters
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN syncAdapterData TEXT;");
}
private void upgradeToVersion62(SQLiteDatabase db) {
// New columns are to transition to having allDay events in the local timezone
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN dtstart2 INTEGER;");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN dtend2 INTEGER;");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN eventTimezone2 TEXT;");
String[] allDayBit = new String[] {"0"};
// Copy over all the data that isn't an all day event.
db.execSQL("UPDATE Events SET " +
"dtstart2=dtstart," +
"dtend2=dtend," +
"eventTimezone2=eventTimezone " +
"WHERE allDay=?;",
allDayBit /* selection args */);
// "cursor" iterates over all the calendars
allDayBit[0] = "1";
Cursor cursor = db.rawQuery("SELECT Events._id," +
"dtstart," +
"dtend," +
"eventTimezone," +
"timezone " +
"FROM Events INNER JOIN Calendars " +
"WHERE Events.calendar_id=Calendars._id" +
" AND allDay=?",
allDayBit /* selection args */);
Time oldTime = new Time();
Time newTime = new Time();
// Update the allday events in the new columns
if (cursor != null) {
try {
String[] newData = new String[4];
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
long id = cursor.getLong(0); // Order from query above
long dtstart = cursor.getLong(1);
long dtend = cursor.getLong(2);
String eTz = cursor.getString(3); // current event timezone
String tz = cursor.getString(4); // Calendar timezone
//If there's no timezone for some reason use UTC by default.
if(eTz == null) {
eTz = Time.TIMEZONE_UTC;
}
// Convert start time for all day events into the timezone of their calendar
oldTime.clear(eTz);
oldTime.set(dtstart);
newTime.clear(tz);
newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
newTime.normalize(false);
dtstart = newTime.toMillis(false /*ignoreDst*/);
// Convert end time for all day events into the timezone of their calendar
oldTime.clear(eTz);
oldTime.set(dtend);
newTime.clear(tz);
newTime.set(oldTime.monthDay, oldTime.month, oldTime.year);
newTime.normalize(false);
dtend = newTime.toMillis(false /*ignoreDst*/);
newData[0] = String.valueOf(dtstart);
newData[1] = String.valueOf(dtend);
newData[2] = tz;
newData[3] = String.valueOf(id);
db.execSQL("UPDATE Events SET " +
"dtstart2=?, " +
"dtend2=?, " +
"eventTimezone2=? " +
"WHERE _id=?",
newData);
}
} finally {
cursor.close();
}
}
}
private void upgradeToVersion61(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
// IF NOT EXISTS should be normal pattern for table creation
db.execSQL("CREATE TABLE IF NOT EXISTS CalendarCache (" +
"_id INTEGER PRIMARY KEY," +
"key TEXT NOT NULL," +
"value TEXT" +
");");
db.execSQL("INSERT INTO CalendarCache (" +
"key, " +
"value) VALUES (" +
"'timezoneDatabaseVersion'," +
"'2009s'" +
");");
}
private void upgradeToVersion60(SQLiteDatabase db) {
// Switch to CalendarProvider2
upgradeSyncState(db);
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
"BEGIN " +
("DELETE FROM Events" +
" WHERE calendar_id=old._id;") +
"END");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;");
db.execSQL("DROP TRIGGER IF EXISTS events_insert");
// Trigger to set event's sync_account
db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
"BEGIN " +
"UPDATE Events" +
" SET _sync_account=" +
" (SELECT _sync_account FROM Calendars" +
" WHERE Calendars._id=new.calendar_id)," +
"_sync_account_type=" +
" (SELECT _sync_account_type FROM Calendars" +
" WHERE Calendars._id=new.calendar_id) " +
"WHERE Events._id=new._id;" +
"END");
db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
// Trigger to remove data tied to an event when we delete that event.
db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
"BEGIN " +
("DELETE FROM Instances" +
" WHERE event_id=old._id;" +
"DELETE FROM EventsRawTimes" +
" WHERE event_id=old._id;" +
"DELETE FROM Attendees" +
" WHERE event_id=old._id;" +
"DELETE FROM Reminders" +
" WHERE event_id=old._id;" +
"DELETE FROM CalendarAlerts" +
" WHERE event_id=old._id;" +
"DELETE FROM ExtendedProperties" +
" WHERE event_id=old._id;") +
"END");
db.execSQL("DROP TRIGGER IF EXISTS attendees_update");
db.execSQL("DROP TRIGGER IF EXISTS attendees_insert");
db.execSQL("DROP TRIGGER IF EXISTS attendees_delete");
db.execSQL("DROP TRIGGER IF EXISTS reminders_update");
db.execSQL("DROP TRIGGER IF EXISTS reminders_insert");
db.execSQL("DROP TRIGGER IF EXISTS reminders_delete");
db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update");
db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert");
db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete");
}
private void upgradeToVersion59(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS BusyBits;");
db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup(" +
"_id," +
"localTimezone," +
"minInstance," +
"maxInstance" +
");");
db.execSQL("INSERT INTO CalendarMetaData_Backup " +
"SELECT " +
"_id," +
"localTimezone," +
"minInstance," +
"maxInstance" +
" FROM CalendarMetaData;");
db.execSQL("DROP TABLE CalendarMetaData;");
createCalendarMetaDataTable59(db);
db.execSQL("INSERT INTO CalendarMetaData " +
"SELECT " +
"_id," +
"localTimezone," +
"minInstance," +
"maxInstance" +
" FROM CalendarMetaData_Backup;");
db.execSQL("DROP TABLE CalendarMetaData_Backup;");
}
private void upgradeToVersion57(SQLiteDatabase db) {
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN guestsCanModify" +
" INTEGER NOT NULL DEFAULT 0;");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN guestsCanInviteOthers" +
" INTEGER NOT NULL DEFAULT 1;");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN guestsCanSeeGuests" +
" INTEGER NOT NULL DEFAULT 1;");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN organizer" +
" STRING;");
db.execSQL("UPDATE Events SET organizer=" +
"(SELECT attendeeEmail" +
" FROM Attendees" +
" WHERE " +
"Attendees.event_id=" +
"Events._id" +
" AND " +
"Attendees.attendeeRelationship=2);");
}
private void upgradeToVersion56(SQLiteDatabase db) {
db.execSQL("ALTER TABLE Calendars" +
" ADD COLUMN ownerAccount TEXT;");
db.execSQL("ALTER TABLE Events" +
" ADD COLUMN hasAttendeeData INTEGER NOT NULL DEFAULT 0;");
// Clear _sync_dirty to avoid a client-to-server sync that could blow away
// server attendees.
// Clear _sync_version to pull down the server's event (with attendees)
// Change the URLs from full-selfattendance to full
db.execSQL("UPDATE Events"
+ " SET _sync_dirty=0, "
+ "_sync_version=NULL, "
+ "_sync_id="
+ "REPLACE(_sync_id, " +
"'/private/full-selfattendance', '/private/full'),"
+ "commentsUri="
+ "REPLACE(commentsUri, " +
"'/private/full-selfattendance', '/private/full');");
db.execSQL("UPDATE Calendars"
+ " SET url="
+ "REPLACE(url, '/private/full-selfattendance', '/private/full');");
// "cursor" iterates over all the calendars
Cursor cursor = db.rawQuery("SELECT _id, " +
"url FROM Calendars",
null /* selection args */);
// Add the owner column.
if (cursor != null) {
try {
final String updateSql = "UPDATE Calendars" +
" SET ownerAccount=?" +
" WHERE _id=?";
while (cursor.moveToNext()) {
Long id = cursor.getLong(0);
String url = cursor.getString(1);
String owner = calendarEmailAddressFromFeedUrl(url);
db.execSQL(updateSql, new Object[] {owner, id});
}
} finally {
cursor.close();
}
}
}
private void upgradeResync(SQLiteDatabase db) {
// Delete sync state, so all records will be re-synced.
db.execSQL("DELETE FROM _sync_state;");
// "cursor" iterates over all the calendars
Cursor cursor = db.rawQuery("SELECT _sync_account," +
"_sync_account_type,url FROM Calendars",
null /* selection args */);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
String accountName = cursor.getString(0);
String accountType = cursor.getString(1);
final Account account = new Account(accountName, accountType);
String calendarUrl = cursor.getString(2);
scheduleSync(account, false /* two-way sync */, calendarUrl);
}
} finally {
cursor.close();
}
}
}
private void upgradeToVersion55(SQLiteDatabase db) {
db.execSQL("ALTER TABLE Calendars ADD COLUMN " +
"_sync_account_type TEXT;");
db.execSQL("ALTER TABLE Events ADD COLUMN " +
"_sync_account_type TEXT;");
db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
db.execSQL("UPDATE Calendars"
+ " SET _sync_account_type='com.google'"
+ " WHERE _sync_account IS NOT NULL");
db.execSQL("UPDATE Events"
+ " SET _sync_account_type='com.google'"
+ " WHERE _sync_account IS NOT NULL");
db.execSQL("UPDATE DeletedEvents"
+ " SET _sync_account_type='com.google'"
+ " WHERE _sync_account IS NOT NULL");
Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ "_sync_account_type, "
+ "_sync_account, "
+ "_sync_id);");
}
private void upgradeToVersion54(SQLiteDatabase db) {
Log.w(TAG, "adding eventSyncAccountAndIdIndex");
db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ "_sync_account, _sync_id);");
}
private void upgradeToVersion53(SQLiteDatabase db) {
Log.w(TAG, "Upgrading CalendarAlerts table");
db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN " +
"creationTime INTEGER NOT NULL DEFAULT 0;");
db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN " +
"receivedTime INTEGER NOT NULL DEFAULT 0;");
db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN " +
"notifyTime INTEGER NOT NULL DEFAULT 0;");
}
private void upgradeToVersion52(SQLiteDatabase db) {
// We added "originalAllDay" to the Events table to keep track of
// the allDay status of the original recurring event for entries
// that are exceptions to that recurring event. We need this so
// that we can format the date correctly for the "originalInstanceTime"
// column when we make a change to the recurrence exception and
// send it to the server.
db.execSQL("ALTER TABLE Events ADD COLUMN " +
"originalAllDay INTEGER;");
// Iterate through the Events table and for each recurrence
// exception, fill in the correct value for "originalAllDay",
// if possible. The only times where this might not be possible
// are (1) the original recurring event no longer exists, or
// (2) the original recurring event does not yet have a _sync_id
// because it was created on the phone and hasn't been synced to the
// server yet. In both cases the originalAllDay field will be set
// to null. In the first case we don't care because the recurrence
// exception will not be displayed and we won't be able to make
// any changes to it (and even if we did, the server should ignore
// them, right?). In the second case, the calendar client already
// disallows making changes to an instance of a recurring event
// until the recurring event has been synced to the server so the
// second case should never occur.
// "cursor" iterates over all the recurrences exceptions.
Cursor cursor = db.rawQuery("SELECT _id," +
"originalEvent" +
" FROM Events" +
" WHERE originalEvent IS NOT NULL",
null /* selection args */);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String originalEvent = cursor.getString(1);
// Find the original recurring event (if it exists)
Cursor recur = db.rawQuery("SELECT allDay" +
" FROM Events" +
" WHERE _sync_id=?",
new String[] {originalEvent});
if (recur == null) {
continue;
}
try {
// Fill in the "originalAllDay" field of the
// recurrence exception with the "allDay" value
// from the recurring event.
if (recur.moveToNext()) {
int allDay = recur.getInt(0);
db.execSQL("UPDATE Events" +
" SET originalAllDay=" + allDay +
" WHERE _id="+id);
}
} finally {
recur.close();
}
}
} finally {
cursor.close();
}
}
}
private void upgradeToVersion51(SQLiteDatabase db) {
Log.w(TAG, "Upgrading DeletedEvents table");
// We don't have enough information to fill in the correct
// value of the calendar_id for old rows in the DeletedEvents
// table, but rows in that table are transient so it is unlikely
// that there are any rows. Plus, the calendar_id is used only
// when deleting a calendar, which is a rare event. All new rows
// will have the correct calendar_id.
db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
// Trigger to remove a calendar's events when we delete the calendar
db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
"BEGIN " +
"DELETE FROM Events WHERE calendar_id=" +
"old._id;" +
"DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
"END");
db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
}
private void dropTables(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + Tables.COLORS + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDARS + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS_RAW_TIMES + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.INSTANCES + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_META_DATA + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_CACHE + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.ATTENDEES + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.REMINDERS + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDAR_ALERTS + ";");
db.execSQL("DROP TABLE IF EXISTS " + Tables.EXTENDED_PROPERTIES + ";");
}
@Override
public synchronized SQLiteDatabase getWritableDatabase() {
SQLiteDatabase db = super.getWritableDatabase();
return db;
}
public SyncStateContentProviderHelper getSyncState() {
return mSyncState;
}
/**
* Schedule a calendar sync for the account.
* @param account the account for which to schedule a sync
* @param uploadChangesOnly if set, specify that the sync should only send
* up local changes. This is typically used for a local sync, a user override of
* too many deletions, or a sync after a calendar is unselected.
* @param url the url feed for the calendar to sync (may be null, in which case a poll of
* all feeds is done.)
*/
void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
Bundle extras = new Bundle();
if (uploadChangesOnly) {
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
}
if (url != null) {
extras.putString("feed", url);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
}
ContentResolver.requestSync(account, CalendarContract.Calendars.CONTENT_URI.getAuthority(),
extras);
}
private static void createEventsView(SQLiteDatabase db) {
db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";");
String eventsSelect = "SELECT "
+ Tables.EVENTS + "." + CalendarContract.Events._ID
+ " AS " + CalendarContract.Events._ID + ","
+ CalendarContract.Events.TITLE + ","
+ CalendarContract.Events.DESCRIPTION + ","
+ CalendarContract.Events.EVENT_LOCATION + ","
+ CalendarContract.Events.EVENT_COLOR + ","
+ CalendarContract.Events.EVENT_COLOR_KEY + ","
+ CalendarContract.Events.STATUS + ","
+ CalendarContract.Events.SELF_ATTENDEE_STATUS + ","
+ CalendarContract.Events.DTSTART + ","
+ CalendarContract.Events.DTEND + ","
+ CalendarContract.Events.DURATION + ","
+ CalendarContract.Events.EVENT_TIMEZONE + ","
+ CalendarContract.Events.EVENT_END_TIMEZONE + ","
+ CalendarContract.Events.ALL_DAY + ","
+ CalendarContract.Events.ACCESS_LEVEL + ","
+ CalendarContract.Events.AVAILABILITY + ","
+ CalendarContract.Events.HAS_ALARM + ","
+ CalendarContract.Events.HAS_EXTENDED_PROPERTIES + ","
+ CalendarContract.Events.RRULE + ","
+ CalendarContract.Events.RDATE + ","
+ CalendarContract.Events.EXRULE + ","
+ CalendarContract.Events.EXDATE + ","
+ CalendarContract.Events.ORIGINAL_SYNC_ID + ","
+ CalendarContract.Events.ORIGINAL_ID + ","
+ CalendarContract.Events.ORIGINAL_INSTANCE_TIME + ","
+ CalendarContract.Events.ORIGINAL_ALL_DAY + ","
+ CalendarContract.Events.LAST_DATE + ","
+ CalendarContract.Events.HAS_ATTENDEE_DATA + ","
+ CalendarContract.Events.CALENDAR_ID + ","
+ CalendarContract.Events.GUESTS_CAN_INVITE_OTHERS + ","
+ CalendarContract.Events.GUESTS_CAN_MODIFY + ","
+ CalendarContract.Events.GUESTS_CAN_SEE_GUESTS + ","
+ CalendarContract.Events.ORGANIZER + ","
+ CalendarContract.Events.SYNC_DATA1 + ","
+ CalendarContract.Events.SYNC_DATA2 + ","
+ CalendarContract.Events.SYNC_DATA3 + ","
+ CalendarContract.Events.SYNC_DATA4 + ","
+ CalendarContract.Events.SYNC_DATA5 + ","
+ CalendarContract.Events.SYNC_DATA6 + ","
+ CalendarContract.Events.SYNC_DATA7 + ","
+ CalendarContract.Events.SYNC_DATA8 + ","
+ CalendarContract.Events.SYNC_DATA9 + ","
+ CalendarContract.Events.SYNC_DATA10 + ","
+ Tables.EVENTS + "." + CalendarContract.Events.DELETED
+ " AS " + CalendarContract.Events.DELETED + ","
+ Tables.EVENTS + "." + CalendarContract.Events._SYNC_ID
+ " AS " + CalendarContract.Events._SYNC_ID + ","
+ Tables.EVENTS + "." + CalendarContract.Events.DIRTY
+ " AS " + CalendarContract.Events.DIRTY + ","
+ CalendarContract.Events.LAST_SYNCED + ","
+ Tables.CALENDARS + "." + CalendarContract.Calendars.ACCOUNT_NAME
+ " AS " + CalendarContract.Events.ACCOUNT_NAME + ","
+ Tables.CALENDARS + "." + CalendarContract.Calendars.ACCOUNT_TYPE
+ " AS " + CalendarContract.Events.ACCOUNT_TYPE + ","
+ CalendarContract.Calendars.CALENDAR_TIME_ZONE + ","
+ CalendarContract.Calendars.CALENDAR_DISPLAY_NAME + ","
+ CalendarContract.Calendars.CALENDAR_LOCATION + ","
+ CalendarContract.Calendars.VISIBLE + ","
+ CalendarContract.Calendars.CALENDAR_COLOR + ","
+ CalendarContract.Calendars.CALENDAR_COLOR_KEY + ","
+ CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL + ","
+ CalendarContract.Calendars.MAX_REMINDERS + ","
+ CalendarContract.Calendars.ALLOWED_REMINDERS + ","
+ CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES + ","
+ CalendarContract.Calendars.ALLOWED_AVAILABILITY + ","
+ CalendarContract.Calendars.CAN_ORGANIZER_RESPOND + ","
+ CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE + ","
+ CalendarContract.Calendars.CAN_PARTIALLY_UPDATE + ","
+ CalendarContract.Calendars.CAL_SYNC1 + ","
+ CalendarContract.Calendars.CAL_SYNC2 + ","
+ CalendarContract.Calendars.CAL_SYNC3 + ","
+ CalendarContract.Calendars.CAL_SYNC4 + ","
+ CalendarContract.Calendars.CAL_SYNC5 + ","
+ CalendarContract.Calendars.CAL_SYNC6 + ","
+ CalendarContract.Calendars.CAL_SYNC7 + ","
+ CalendarContract.Calendars.CAL_SYNC8 + ","
+ CalendarContract.Calendars.CAL_SYNC9 + ","
+ CalendarContract.Calendars.CAL_SYNC10 + ","
+ CalendarContract.Calendars.OWNER_ACCOUNT + ","
+ CalendarContract.Calendars.SYNC_EVENTS
+ " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS
+ " ON (" + Tables.EVENTS + "." + CalendarContract.Events.CALENDAR_ID
+ "=" + Tables.CALENDARS + "." + CalendarContract.Calendars._ID
+ ")";
db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect);
}
/**
* Extracts the calendar email from a calendar feed url.
* @param feed the calendar feed url
* @return the calendar email that is in the feed url or null if it can't
* find the email address.
* TODO: this is duplicated in CalendarSyncAdapter; move to a library
*/
public static String calendarEmailAddressFromFeedUrl(String feed) {
// Example feed url:
// https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
String[] pathComponents = feed.split("/");
if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
try {
return URLDecoder.decode(pathComponents[5], "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "unable to url decode the email address in calendar " + feed);
return null;
}
}
Log.e(TAG, "unable to find the email address in calendar " + feed);
return null;
}
/**
* Get a "allcalendars" url from a "private/full" or "private/free-busy" url
* @param url
* @return the rewritten Url
*
* For example:
*
* http://www.google.com/calendar/feeds/joe%40joe.com/private/full
* http://www.google.com/calendar/feeds/joe%40joe.com/private/free-busy
*
* will be rewriten into:
*
* http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com
* http://www.google.com/calendar/feeds/default/allcalendars/full/joe%40joe.com
*/
@VisibleForTesting
private static String getAllCalendarsUrlFromEventsUrl(String url) {
if (url == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot get AllCalendars url from a NULL url");
}
return null;
}
if (url.contains("/private/full")) {
return url.replace("/private/full", "").
replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full");
}
if (url.contains("/private/free-busy")) {
return url.replace("/private/free-busy", "").
replace("/calendar/feeds", "/calendar/feeds/default/allcalendars/full");
}
// Just log as we dont recognize the provided Url
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot get AllCalendars url from the following url: " + url);
}
return null;
}
/**
* Get "selfUrl" from "events url"
* @param url the Events url (either "private/full" or "private/free-busy"
* @return the corresponding allcalendar url
*/
private static String getSelfUrlFromEventsUrl(String url) {
return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url));
}
/**
* Get "editUrl" from "events url"
* @param url the Events url (either "private/full" or "private/free-busy"
* @return the corresponding allcalendar url
*/
private static String getEditUrlFromEventsUrl(String url) {
return rewriteUrlFromHttpToHttps(getAllCalendarsUrlFromEventsUrl(url));
}
/**
* Rewrite the url from "http" to "https" scheme
* @param url the url to rewrite
* @return the rewritten URL
*/
private static String rewriteUrlFromHttpToHttps(String url) {
if (url == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot rewrite a NULL url");
}
return null;
}
if (url.startsWith(SCHEMA_HTTPS)) {
return url;
}
if (!url.startsWith(SCHEMA_HTTP)) {
throw new IllegalArgumentException("invalid url parameter, unknown scheme: " + url);
}
return SCHEMA_HTTPS + url.substring(SCHEMA_HTTP.length());
}
/**
* Duplicates an event and its associated tables (Attendees, Reminders, ExtendedProperties).
* <p>
* Does not create a duplicate if the Calendar's "canPartiallyUpdate" is 0 or the Event's
* "dirty" is 1 (so we don't create more than one duplicate).
*
* @param id The _id of the event to duplicate.
*/
protected void duplicateEvent(final long id) {
final SQLiteDatabase db = getWritableDatabase();
final long canPartiallyUpdate = DatabaseUtils.longForQuery(db, "SELECT "
+ CalendarContract.Calendars.CAN_PARTIALLY_UPDATE + " FROM " + Views.EVENTS
+ " WHERE " + Events._ID + " = ?", new String[] {
String.valueOf(id)
});
if (canPartiallyUpdate == 0) {
return;
}
db.execSQL("INSERT INTO " + CalendarDatabaseHelper.Tables.EVENTS
+ " (" + LAST_SYNCED_EVENT_COLUMNS + ","
+ Events.DIRTY + "," + Events.LAST_SYNCED + ")"
+ " SELECT " + LAST_SYNCED_EVENT_COLUMNS + ", 0, 1"
+ " FROM " + Tables.EVENTS
+ " WHERE " + Events._ID + " = ? AND " + Events.DIRTY + " = ?",
new Object[]{
id,
0, // Events.DIRTY
});
final long newId = DatabaseUtils.longForQuery(
db, "SELECT CASE changes() WHEN 0 THEN -1 ELSE last_insert_rowid() END", null);
if (newId < 0) {
return;
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Duplicating event " + id + " into new event " + newId);
}
copyEventRelatedTables(db, newId, id);
}
/**
* Makes a copy of the Attendees, Reminders, and ExtendedProperties rows associated with
* a specific event.
*
* @param db The database.
* @param newId The ID of the new event.
* @param id The ID of the old event.
*/
static void copyEventRelatedTables(SQLiteDatabase db, long newId, long id) {
db.execSQL("INSERT INTO " + Tables.REMINDERS
+ " ( " + CalendarContract.Reminders.EVENT_ID + ", "
+ LAST_SYNCED_REMINDER_COLUMNS + ") "
+ "SELECT ?," + LAST_SYNCED_REMINDER_COLUMNS
+ " FROM " + Tables.REMINDERS
+ " WHERE " + CalendarContract.Reminders.EVENT_ID + " = ?",
new Object[] {newId, id});
db.execSQL("INSERT INTO "
+ Tables.ATTENDEES
+ " (" + CalendarContract.Attendees.EVENT_ID + ","
+ LAST_SYNCED_ATTENDEE_COLUMNS + ") "
+ "SELECT ?," + LAST_SYNCED_ATTENDEE_COLUMNS + " FROM " + Tables.ATTENDEES
+ " WHERE " + CalendarContract.Attendees.EVENT_ID + " = ?",
new Object[] {newId, id});
db.execSQL("INSERT INTO " + Tables.EXTENDED_PROPERTIES
+ " (" + CalendarContract.ExtendedProperties.EVENT_ID + ","
+ LAST_SYNCED_EXTENDED_PROPERTY_COLUMNS + ") "
+ "SELECT ?, " + LAST_SYNCED_EXTENDED_PROPERTY_COLUMNS
+ " FROM " + Tables.EXTENDED_PROPERTIES
+ " WHERE " + CalendarContract.ExtendedProperties.EVENT_ID + " = ?",
new Object[]{newId, id});
}
protected void removeDuplicateEvent(final long id) {
final SQLiteDatabase db = getWritableDatabase();
final Cursor cursor = db.rawQuery("SELECT " + Events._ID + " FROM " + Tables.EVENTS
+ " WHERE " + Events._SYNC_ID
+ " = (SELECT " + Events._SYNC_ID
+ " FROM " + Tables.EVENTS
+ " WHERE " + Events._ID + " = ?) "
+ "AND " + Events.LAST_SYNCED + " = ?",
new String[]{
String.valueOf(id),
"1", // Events.LAST_SYNCED
});
try {
// there should only be at most one but this can't hurt
if (cursor.moveToNext()) {
final long dupId = cursor.getLong(0);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Removing duplicate event " + dupId + " of original event " + id);
}
// triggers will clean up related tables.
db.execSQL("DELETE FROM Events WHERE " + Events._ID + " = ?", new Object[]{dupId});
}
} finally {
cursor.close();
}
}
}