Add original_id column + logic. db -> v301

Adds an original_id to the Events table for local reference to
an original event for exceptions. It also adds logic to keep this
value in sync with the original_sync_id so apps don't have to
update for it to continue working.

Change-Id: I0034ecedb6f7b582ba823daab8656b7778781d84
diff --git a/src/com/android/providers/calendar/CalendarDatabaseHelper.java b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
index fa8627a..da1b355 100644
--- a/src/com/android/providers/calendar/CalendarDatabaseHelper.java
+++ b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
@@ -32,6 +32,7 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.os.Bundle;
 import android.provider.Calendar;
+import android.provider.Calendar.Events;
 import android.provider.ContactsContract;
 import android.provider.SyncStateContract;
 import android.text.TextUtils;
@@ -61,7 +62,7 @@
     // 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 = 300;
+    static final int DATABASE_VERSION = 301;
 
     private static final int PRE_FROYO_SYNC_STATE_VERSION = 3;
 
@@ -108,6 +109,21 @@
                 " WHERE " + Calendar.ExtendedProperties.EVENT_ID + "=" +
                     "old." + Calendar.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 " + Calendar.Events.CALENDAR_ID + "=" +
                 "old." + Calendar.Events._ID + ";";
@@ -363,6 +379,10 @@
                 EVENTS_CLEANUP_TRIGGER_SQL +
                 "END");
 
+        // Trigger to update exceptions when an original event updates its
+        // _sync_id
+        db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
+
         ContentResolver.requestSync(null /* all accounts */,
                 ContactsContract.AUTHORITY, new Bundle());
     }
@@ -403,6 +423,65 @@
                 Calendar.Events.RDATE + " TEXT," +
                 Calendar.Events.EXRULE + " TEXT," +
                 Calendar.Events.EXDATE + " TEXT," +
+                Calendar.Events.ORIGINAL_ID + " INTEGER," +
+                // ORIGINAL_SYNC_ID is the _sync_id of recurring event
+                Calendar.Events.ORIGINAL_SYNC_ID + " TEXT," +
+                // originalInstanceTime is in millis since epoch
+                Calendar.Events.ORIGINAL_INSTANCE_TIME + " INTEGER," +
+                Calendar.Events.ORIGINAL_ALL_DAY + " INTEGER," +
+                // lastDate is in millis since epoch
+                Calendar.Events.LAST_DATE + " INTEGER," +
+                Calendar.Events.HAS_ATTENDEE_DATA + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.GUESTS_CAN_MODIFY + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.GUESTS_CAN_INVITE_OTHERS + " INTEGER NOT NULL DEFAULT 1," +
+                Calendar.Events.GUESTS_CAN_SEE_GUESTS + " INTEGER NOT NULL DEFAULT 1," +
+                Calendar.Events.ORGANIZER + " STRING," +
+                Calendar.Events.DELETED + " INTEGER NOT NULL DEFAULT 0," +
+                // timezone for event with allDay events are in local timezone
+                Calendar.Events.EVENT_END_TIMEZONE + " TEXT," +
+                // SYNC_DATA1 is available for use by sync adapters
+                Calendar.Events.SYNC_DATA1 + " TEXT" + ");");
+
+        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON " + Tables.EVENTS + " ("
+                + Calendar.Events.CALENDAR_ID + ");");
+    }
+
+    // TODO Remove this method after merging all ICS upgrades
+    private void createEventsTable300(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + Tables.EVENTS + " (" +
+                Calendar.Events._ID + " INTEGER PRIMARY KEY," +
+                Calendar.Events._SYNC_ID + " TEXT," +
+                Calendar.Events._SYNC_VERSION + " TEXT," +
+                // sync time in UTC
+                Calendar.Events._SYNC_TIME + " TEXT,"  +
+                Calendar.Events._SYNC_DATA + " INTEGER," +
+                Calendar.Events.DIRTY + " INTEGER," +
+                // sync mark to filter out new rows
+                Calendar.Events._SYNC_MARK + " INTEGER," +
+                Calendar.Events.CALENDAR_ID + " INTEGER NOT NULL," +
+                Calendar.Events.HTML_URI + " TEXT," +
+                Calendar.Events.TITLE + " TEXT," +
+                Calendar.Events.EVENT_LOCATION + " TEXT," +
+                Calendar.Events.DESCRIPTION + " TEXT," +
+                Calendar.Events.STATUS + " INTEGER," +
+                Calendar.Events.SELF_ATTENDEE_STATUS + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.COMMENTS_URI + " TEXT," +
+                // dtstart in millis since epoch
+                Calendar.Events.DTSTART + " INTEGER," +
+                // dtend in millis since epoch
+                Calendar.Events.DTEND + " INTEGER," +
+                // timezone for event
+                Calendar.Events.EVENT_TIMEZONE + " TEXT," +
+                Calendar.Events.DURATION + " TEXT," +
+                Calendar.Events.ALL_DAY + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.ACCESS_LEVEL + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.AVAILABILITY + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.HAS_ALARM + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.HAS_EXTENDED_PROPERTIES + " INTEGER NOT NULL DEFAULT 0," +
+                Calendar.Events.RRULE + " TEXT," +
+                Calendar.Events.RDATE + " TEXT," +
+                Calendar.Events.EXRULE + " TEXT," +
+                Calendar.Events.EXDATE + " TEXT," +
                 // originalEvent is the _sync_id of recurring event
                 Calendar.Events.ORIGINAL_SYNC_ID + " TEXT," +
                 // originalInstanceTime is in millis since epoch
@@ -845,6 +924,11 @@
                 createEventsView = true;
                 oldVersion = 300;
             }
+            if (oldVersion == 300) {
+                upgradeToVersion301(db);
+                createEventsView = true;
+                oldVersion++;
+            }
             if (createEventsView) {
                 createEventsView(db);
             }
@@ -895,6 +979,28 @@
     }
 
     @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) {
 
         /*
@@ -917,7 +1023,7 @@
 
         // 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");
+        db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup;");
         createCalendarsTable(db);
 
         // copy fields from old to new
@@ -989,7 +1095,7 @@
         db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
         db.execSQL("DROP INDEX IF EXISTS eventSyncAccountAndIdIndex");
         db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
-        createEventsTable(db);
+        createEventsTable300(db);
 
         // copy fields from old to new
         db.execSQL("INSERT INTO Events (" +
@@ -2025,6 +2131,7 @@
                 + Calendar.Events.EXRULE + ","
                 + Calendar.Events.EXDATE + ","
                 + Calendar.Events.ORIGINAL_SYNC_ID + ","
+                + Calendar.Events.ORIGINAL_ID + ","
                 + Calendar.Events.ORIGINAL_INSTANCE_TIME + ","
                 + Calendar.Events.ORIGINAL_ALL_DAY + ","
                 + Calendar.Events.LAST_DATE + ","
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 783632a..5255d4a 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -1431,6 +1431,21 @@
                         updatedValues.put(Events.ORGANIZER, owner);
                     }
                 }
+                if (updatedValues.containsKey(Events.ORIGINAL_SYNC_ID)
+                        && !updatedValues.containsKey(Events.ORIGINAL_ID)) {
+                    long originalId = getOriginalId(updatedValues
+                            .getAsString(Events.ORIGINAL_SYNC_ID));
+                    if (originalId != -1) {
+                        updatedValues.put(Events.ORIGINAL_ID, originalId);
+                    }
+                } else if (!updatedValues.containsKey(Events.ORIGINAL_SYNC_ID)
+                        && updatedValues.containsKey(Events.ORIGINAL_ID)) {
+                    String originalSyncId = getOriginalSyncId(updatedValues
+                            .getAsLong(Events.ORIGINAL_ID));
+                    if (!TextUtils.isEmpty(originalSyncId)) {
+                        updatedValues.put(Events.ORIGINAL_SYNC_ID, originalSyncId);
+                    }
+                }
                 if (fixAllDayTime(uri, updatedValues)) {
                     if (Log.isLoggable(TAG, Log.WARN)) {
                         Log.w(TAG, "insertInTransaction: " +
@@ -1485,6 +1500,20 @@
                             }
                         }
                     }
+                    // If this was a recurring event with a _sync_id update any
+                    // exceptions that may have been added prior to the original
+                    // event
+                    if (values.containsKey(Events._SYNC_ID) && values.containsKey(Events.RRULE)
+                            && !TextUtils.isEmpty(values.getAsString(Events.RRULE))) {
+                        String syncId = values.getAsString(Events._SYNC_ID);
+                        if (TextUtils.isEmpty(syncId)) {
+                            break;
+                        }
+                        ContentValues originalValues = new ContentValues();
+                        originalValues.put(Events.ORIGINAL_ID, id);
+                        mDb.update(Tables.EVENTS, originalValues, Events.ORIGINAL_SYNC_ID + "=?",
+                                new String[] {syncId});
+                    }
                     sendUpdateNotification(id, callerIsSyncAdapter);
                 }
                 break;
@@ -1639,6 +1668,48 @@
         mDb.execSQL(SQL_UPDATE_EVENT_SET_DIRTY, new Integer[] {eventId});
     }
 
+    private long getOriginalId(String originalSyncId) {
+        if (TextUtils.isEmpty(originalSyncId)) {
+            return -1;
+        }
+        // Get the original id for this event
+        long originalId = -1;
+        Cursor c = null;
+        try {
+            c = query(Events.CONTENT_URI, ID_ONLY_PROJECTION,
+                    Events._SYNC_ID + "=?", new String[] {originalSyncId}, null);
+            if (c != null && c.moveToFirst()) {
+                originalId = c.getLong(0);
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return originalId;
+    }
+
+    private String getOriginalSyncId(long originalId) {
+        if (originalId == -1) {
+            return null;
+        }
+        // Get the original id for this event
+        String originalSyncId = null;
+        Cursor c = null;
+        try {
+            c = query(Events.CONTENT_URI, new String[] {Events._SYNC_ID},
+                    Events._ID + "=?", new String[] {Long.toString(originalId)}, null);
+            if (c != null && c.moveToFirst()) {
+                originalSyncId = c.getString(0);
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return originalSyncId;
+    }
+
     /**
      * Gets the calendar's owner for an event.
      * @param calId
@@ -3099,6 +3170,7 @@
         sEventsProjectionMap.put(Events.EXRULE, Events.EXRULE);
         sEventsProjectionMap.put(Events.EXDATE, Events.EXDATE);
         sEventsProjectionMap.put(Events.ORIGINAL_SYNC_ID, Events.ORIGINAL_SYNC_ID);
+        sEventsProjectionMap.put(Events.ORIGINAL_ID, Events.ORIGINAL_ID);
         sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, Events.ORIGINAL_INSTANCE_TIME);
         sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_ALL_DAY);
         sEventsProjectionMap.put(Events.LAST_DATE, Events.LAST_DATE);
@@ -3162,6 +3234,7 @@
         sEventEntitiesProjectionMap.put(Events.EXRULE, Events.EXRULE);
         sEventEntitiesProjectionMap.put(Events.EXDATE, Events.EXDATE);
         sEventEntitiesProjectionMap.put(Events.ORIGINAL_SYNC_ID, Events.ORIGINAL_SYNC_ID);
+        sEventEntitiesProjectionMap.put(Events.ORIGINAL_ID, Events.ORIGINAL_ID);
         sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME,
                 Events.ORIGINAL_INSTANCE_TIME);
         sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_ALL_DAY);
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
index a2fdc89..dafad3a 100644
--- a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
@@ -1913,11 +1913,11 @@
         cursor.close();
     }
 
-
     /**
-     * Test the event's _sync_dirty status and clear it.
+     * Test the event's dirty status and clear it.
+     * 
      * @param eventId event to fetch.
-     * @param wanted the wanted _sync_dirty status
+     * @param wanted the wanted dirty status
      */
     private void testAndClearDirty(long eventId, int wanted) {
         Cursor cursor = mResolver.query(
@@ -1930,7 +1930,7 @@
             assertEquals("dirty flag", wanted, dirty);
             if (dirty == 1) {
                 // Have to access database directly since provider will set dirty again.
-                mDb.execSQL("UPDATE Events SET _sync_dirty=0 WHERE _id=" + eventId);
+                mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId);
             }
         } finally {
             cursor.close();