Make sure values are set correctly when inserting event.

For a recurrence, dtend must be null.  This change enforces that,
as well as some other conditions on the value.
Includes unittest.

bug 2513213

Change-Id: I04a1b7bd2f91e579177dd80741b4487409e903fc
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 1209ed9..ced68df 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -978,13 +978,13 @@
                 "originalInstanceTime >= ?)) AND (sync_events != 0)");
         String selectionArgs[] = new String[] {endString, beginString, endString,
                 String.valueOf(begin - MAX_ASSUMED_DURATION)};
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "Retrieving events to expand: " + qb.toString());
-        }
-
-        return qb.query(mDb, EXPAND_COLUMNS, null /* selection */,
+        Cursor c = qb.query(mDb, EXPAND_COLUMNS, null /* selection */,
                 selectionArgs, null /* groupBy */,
                 null /* having */, null /* sortOrder */);
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "Instance expansion:  got " + c.getCount() + " entries");
+        }
+        return c;
     }
 
     /**
@@ -1538,10 +1538,11 @@
                 if (!values.containsKey(Events.DTSTART)) {
                     throw new RuntimeException("DTSTART field missing from event");
                 }
-                // TODO: avoid the call to updateBundleFromEvent if this is just finding local
-                // changes.
                 // TODO: do we really need to make a copy?
-                ContentValues updatedValues = updateContentValuesFromEvent(values);
+                ContentValues updatedValues = new ContentValues(values);
+                validateEventData(updatedValues);
+                // updateLastDate must be after validation, to ensure proper last date computation
+                updatedValues = updateLastDate(updatedValues);
                 if (updatedValues == null) {
                     throw new RuntimeException("Could not insert event.");
                     // return null;
@@ -1562,7 +1563,6 @@
                     Log.w(TAG, "insertInTransaction: " +
                             "allDay is true but sec, min, hour were not 0.");
                 }
-
                 id = mDbHelper.eventsInsert(updatedValues);
                 if (id != -1) {
                     updateEventRawTimesLocked(id, updatedValues);
@@ -1660,6 +1660,71 @@
         return ContentUris.withAppendedId(uri, id);
     }
 
+    /**
+     * Do some validation on event data before inserting.
+     * In particular make sure dtend, duration, etc make sense for
+     * the type of event (regular, recurrence, exception).  Remove
+     * any unexpected fields.
+     *
+     * @param values the ContentValues to insert
+     */
+    private void validateEventData(ContentValues values) {
+        boolean hasDtend = values.getAsLong(Events.DTEND) != null;
+        boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION));
+        boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE));
+        boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE));
+        boolean hasOriginalEvent = !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT));
+        boolean hasOriginalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME) != null;
+        if (hasRrule || hasRdate) {
+            // Recurrence:
+            // dtstart is start time of first event
+            // dtend is null
+            // duration is the duration of the event
+            // rrule is the recurrence rule
+            // lastDate is the end of the last event or null if it repeats forever
+            // originalEvent is null
+            // originalInstanceTime is null
+            if (hasDtend || !hasDuration || hasOriginalEvent || hasOriginalInstanceTime) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.e(TAG, "Invalid values for recurrence: " + values);
+                }
+                values.remove(Events.DTEND);
+                values.remove(Events.ORIGINAL_EVENT);
+                values.remove(Events.ORIGINAL_INSTANCE_TIME);
+            }
+        } else if (hasOriginalEvent || hasOriginalInstanceTime) {
+            // Recurrence exception
+            // dtstart is start time of exception event
+            // dtend is end time of exception event
+            // duration is null
+            // rrule is null
+            // lastdate is same as dtend
+            // originalEvent is the _sync_id of the recurrence
+            // originalInstanceTime is the start time of the event being replaced
+            if (!hasDtend || hasDuration || !hasOriginalEvent || !hasOriginalInstanceTime) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.e(TAG, "Invalid values for recurrence exception: " + values);
+                }
+                values.remove(Events.DURATION);
+            }
+        } else {
+            // Regular event
+            // dtstart is the start time
+            // dtend is the end time
+            // duration is null
+            // rrule is null
+            // lastDate is the same as dtend
+            // originalEvent is null
+            // originalInstanceTime is null
+            if (!hasDtend || hasDuration) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.e(TAG, "Invalid values for event: " + values);
+                }
+                values.remove(Events.DURATION);
+            }
+        }
+    }
+
     private void setEventDirty(int eventId) {
         mDb.execSQL("UPDATE Events SET _sync_dirty=1 where _id=?", new Integer[] {eventId});
     }
@@ -2075,10 +2140,13 @@
         return lastMillis;
     }
 
-    private ContentValues updateContentValuesFromEvent(ContentValues initialValues) {
+    /**
+     * Add LAST_DATE to values.
+     * @param values the ContentValues (in/out)
+     * @return values on success, null on failure
+     */
+    private ContentValues updateLastDate(ContentValues values) {
         try {
-            ContentValues values = new ContentValues(initialValues);
-
             long last = calculateLastDate(values);
             if (last != -1) {
                 values.put(Events.LAST_DATE, last);
@@ -2541,8 +2609,9 @@
                             + Events.HTML_URI
                             + " in Events table is not allowed.");
                 }
-
-                ContentValues updatedValues = updateContentValuesFromEvent(values);
+                ContentValues updatedValues = new ContentValues(values);
+                // TODO: should extend validateEventData to work with updates and call it here
+                updatedValues = updateLastDate(updatedValues);
                 if (updatedValues == null) {
                     Log.w(TAG, "Could not update event.");
                     return 0;
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
index 683f718..59e1285 100644
--- a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
@@ -53,6 +53,7 @@
     private int mCalendarId;
 
     protected boolean mWipe = false;
+    protected boolean mForceDtend = false;
 
     // We need a unique id to put in the _sync_id field so that we can create
     // recurrence exceptions that refer to recurring events.
@@ -843,6 +844,7 @@
         mDb = helper.getWritableDatabase();
         wipeData(mDb);
         mMetaData = getProvider().mMetaData;
+        mForceDtend = false;
     }
 
 
@@ -921,10 +923,11 @@
         m.put(Events.DTSTART, event.mDtstart);
         m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
 
-        if (event.mRrule == null) {
+        if (event.mRrule == null || mForceDtend) {
             // This is a normal event
             m.put(Events.DTEND, event.mDtend);
-        } else {
+        }
+        if (event.mRrule != null) {
             // This is a repeating event
             m.put(Events.RRULE, event.mRrule);
             m.put(Events.DURATION, event.mDuration);
@@ -1143,6 +1146,12 @@
         cursor.close();
     }
 
+    // Force a dtend value to be set and make sure instance expansion still works
+    public void testInstanceRangeDtend() throws Exception {
+        mForceDtend = true;
+        testInstanceRange();
+    }
+
     public void testInstanceRange() throws Exception {
         Cursor cursor;
         Uri url = null;