DO NOT MERGE - Avoid re-use of orphaned data
Data is being added to Attendees and Reminders that is associated
with delete Events. When the Event ID is re-used, the orphaned
data attaches to the new event. To avoid this, change the Event
ID column to "autoincrement", ensuring that IDs are never re-used.
At database upgrade time, delete any orphaned rows.
Bug 5063315
Change-Id: Ic2519b335d6d6b2c82579c5484ad3b8528ffd682
diff --git a/src/com/android/providers/calendar/CalendarDatabaseHelper.java b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
index 193c2e7..e0fbb35 100644
--- a/src/com/android/providers/calendar/CalendarDatabaseHelper.java
+++ b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
@@ -59,7 +59,7 @@
// Note: if you update the version number, you must also update the code
// in upgradeDatabase() to modify the database (gracefully, if possible).
- static final int DATABASE_VERSION = 101;
+ static final int DATABASE_VERSION = 102;
private static final int PRE_FROYO_SYNC_STATE_VERSION = 3;
@@ -68,6 +68,17 @@
private static final String SYNC_STATE_META_TABLE = "_sync_state_metadata";
private static final String SYNC_STATE_META_VERSION_COLUMN = "version";
+ /** Selects rows from Attendees for which the event_id refers to a nonexistent Event */
+ private static final String WHERE_ATTENDEES_ORPHANS =
+ "event_id IN (SELECT event_id FROM Attendees " +
+ "LEFT OUTER JOIN Events ON event_id=Events._id " +
+ "WHERE 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 =
+ "event_id IN (SELECT event_id FROM Reminders " +
+ "LEFT OUTER JOIN Events ON event_id=Events._id " +
+ "WHERE Events._id IS NULL)";
+
private final Context mContext;
private final SyncStateContentProviderHelper mSyncState;
@@ -241,7 +252,7 @@
// TODO: do we need both dtend and duration?
db.execSQL("CREATE TABLE Events (" +
- "_id INTEGER PRIMARY KEY," +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
ACCOUNT_NAME + " TEXT," +
ACCOUNT_TYPE + " TEXT," +
"_sync_id TEXT," +
@@ -470,6 +481,28 @@
");");
}
+ /**
+ * 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) {
+ Log.d(TAG, "Checking for orphaned entries");
+ int count;
+
+ count = db.delete("Attendees", WHERE_ATTENDEES_ORPHANS, null);
+ if (count != 0) {
+ Log.i(TAG, "Deleted " + count + " orphaned Attendees");
+ }
+
+ count = db.delete("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
@@ -580,6 +613,11 @@
upgradeToVersion101(db);
oldVersion = 101;
}
+ if(oldVersion == 101) {
+ upgradeToVersion102(db);
+ oldVersion = 102;
+ }
+ removeOrphans(db);
} catch (SQLiteException e) {
Log.e(TAG, "onUpgrade: SQLiteException, recreating db. " + e);
dropTables(db);
@@ -613,6 +651,149 @@
}
@VisibleForTesting
+ void upgradeToVersion102(SQLiteDatabase db) {
+ /*
+ * Changes from version 101 to 102:
+ * - 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 events_insert");
+ db.execSQL("DROP INDEX IF EXISTS eventSyncAccountAndIdIndex");
+ db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
+
+ // copy & pasted from bootstrapDB
+ db.execSQL("CREATE TABLE Events (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
+ ACCOUNT_NAME + " TEXT," +
+ ACCOUNT_TYPE + " TEXT," +
+ "_sync_id TEXT," +
+ "_sync_version TEXT," +
+ "_sync_time TEXT," + // UTC
+ "_sync_local_id INTEGER," +
+ "_sync_dirty INTEGER," +
+ "_sync_mark INTEGER," + // To filter out new rows
+ "calendar_id INTEGER NOT NULL," +
+ "htmlUri TEXT," +
+ "title TEXT," +
+ "eventLocation TEXT," +
+ "description TEXT," +
+ "eventStatus INTEGER," +
+ "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
+ "commentsUri TEXT," +
+ "dtstart INTEGER," + // millis since epoch
+ "dtend INTEGER," + // millis since epoch
+ "eventTimezone TEXT," + // timezone for event
+ "duration TEXT," +
+ "allDay INTEGER NOT NULL DEFAULT 0," +
+ "visibility INTEGER NOT NULL DEFAULT 0," +
+ "transparency 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 TEXT," + // _sync_id of recurring event
+ "originalInstanceTime INTEGER," + // millis since epoch
+ "originalAllDay INTEGER," +
+ "lastDate INTEGER," + // millis since epoch
+ "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," +
+ "dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone
+ "dtend2 INTEGER," + //millis since epoch, allDay events in local timezone
+ "eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone
+ "syncAdapterData TEXT" + //available for use by sync adapters
+ ");");
+
+ db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " + Calendar.Events._SYNC_ACCOUNT + ", "
+ + Calendar.Events._SYNC_ID + ");");
+
+ db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
+ Calendar.Events.CALENDAR_ID +
+ ");");
+
+ String FIELD_LIST =
+ "_id, " +
+ "_sync_account, " +
+ "_sync_account_type, " +
+ "_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, " +
+ "duration, " +
+ "allDay, " +
+ "visibility, " +
+ "transparency, " +
+ "hasAlarm, " +
+ "hasExtendedProperties, " +
+ "rrule, " +
+ "rdate, " +
+ "exrule, " +
+ "exdate, " +
+ "originalEvent, " +
+ "originalInstanceTime, " +
+ "originalAllDay, " +
+ "lastDate, " +
+ "hasAttendeeData, " +
+ "guestsCanModify, " +
+ "guestsCanInviteOthers, " +
+ "guestsCanSeeGuests, " +
+ "organizer, " +
+ "deleted, " +
+ "dtstart2, " +
+ "dtend2, " +
+ "eventTimezone2, " +
+ "syncAdapterData";
+
+ // 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 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");
+
+ // 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");
+ }
+
+
+ @VisibleForTesting
void upgradeToVersion101(SQLiteDatabase db) {
updateCalendarCacheTableTo101(db);
}