Add checks for Event validity before committing
* Enforce CalendarProvider2's requirements for valid Events
* Add unit tests for the new code
Bug: 2658149
Change-Id: I42ad7acbcee3b6b831f805c59436017a32651f3a
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index b7c2d50..28ac479 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -133,6 +133,7 @@
Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
new String[] {Calendars._ID}, CALENDAR_SELECTION,
new String[] {mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
+ if (c == null) return;
try {
if (c.moveToFirst()) {
mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
@@ -231,7 +232,7 @@
}
}
- class EasCalendarSyncParser extends AbstractSyncParser {
+ public class EasCalendarSyncParser extends AbstractSyncParser {
String[] mBindArgument = new String[1];
Uri mAccountUri;
@@ -274,6 +275,56 @@
}
}
+ /**
+ * Set DTEND and/or DURATION, as appropriate, for the Event
+ * The follow rules are enforced by CalendarProvider2:
+ * Events MUST have either 1) a DTEND or 2) a DURATION as defined by Rfc2445
+ * Recurring events (i.e. events with RRULE) must have a DURATION
+ * All-day recurring events MUST have a DURATION that is in the form P<n>D
+ * Other events MAY have a DURATION in any valid form (we use P<n>M)
+ * All-day events MUST have hour, minute, and second = 0; in addition, they must have
+ * the TIMEZONE set to UTC
+ * @param cv the ContentValues for the Event
+ * @param startTime the start time for the Event
+ * @param endTime the end time for the Event
+ * @param allDayEvent whether this is an all day event (1) or not (0)
+ */
+ /*package*/ void setTimes(ContentValues cv, long startTime, long endTime, int allDayEvent) {
+ // Sanity check for illegal values; just return (the event will be rejected later by
+ // isValidEventValues()
+ if (startTime < 0 || endTime < 0) return;
+ // If this is an all-day event, set hour, minute, and second to zero
+ if (allDayEvent != 0) {
+ GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
+ cal.setTimeInMillis(startTime);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
+ cal.set(GregorianCalendar.MINUTE, 0);
+ cal.set(GregorianCalendar.SECOND, 0);
+ startTime = cal.getTimeInMillis();
+ cal.setTimeInMillis(endTime);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
+ cal.set(GregorianCalendar.MINUTE, 0);
+ cal.set(GregorianCalendar.SECOND, 0);
+ endTime = cal.getTimeInMillis();
+ cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID());
+ }
+ // Always set DTSTART
+ cv.put(Events.DTSTART, startTime);
+ // For recurring events, set DURATION. Use P<n>D format for all day events
+ if (cv.containsKey(Events.RRULE)) {
+ if (allDayEvent != 0) {
+ cv.put(Events.DURATION, "P" + ((endTime - startTime) / DAYS) + "D");
+ }
+ else {
+ cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
+ }
+ // For other events, set DTEND and LAST_DATE
+ } else {
+ cv.put(Events.DTEND, endTime);
+ cv.put(Events.LAST_DATE, endTime);
+ }
+ }
+
public void addEvent(CalendarOperations ops, String serverId, boolean update)
throws IOException {
ContentValues cv = new ContentValues();
@@ -370,7 +421,6 @@
break;
case Tags.CALENDAR_START_TIME:
startTime = Utility.parseDateTimeToMillis(getValue());
- cv.put(Events.DTSTART, startTime);
break;
case Tags.CALENDAR_END_TIME:
endTime = Utility.parseDateTimeToMillis(getValue());
@@ -437,6 +487,9 @@
}
}
+ // Set up DTART and DTEND/DURATION
+ setTimes(cv, startTime, endTime, allDayEvent);
+
// If we haven't added the organizer to attendees, do it now
if (!organizerAdded) {
addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
@@ -474,36 +527,6 @@
ops.newExtendedProperty("attendees", sb.toString());
}
- boolean hasRrule = cv.containsKey(Events.RRULE);
- // If there's no recurrence, set DTEND to the end time
- if (!hasRrule) {
- cv.put(Events.DTEND, endTime);
- cv.put(Events.LAST_DATE, endTime);
- }
- // Set the DURATION using rfc2445
- // For all day events, make sure hour, minute, and second are zero for DTSTART/DTEND
- if (allDayEvent != 0) {
- GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
- cal.setTimeInMillis(startTime);
- cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
- cal.set(GregorianCalendar.MINUTE, 0);
- cal.set(GregorianCalendar.SECOND, 0);
- cv.put(Events.DTSTART, cal.getTimeInMillis());
- // Use DURATION w/ recurrences; otherwise DTEND
- if (!hasRrule) {
- cal.setTimeInMillis(endTime);
- cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
- cal.set(GregorianCalendar.MINUTE, 0);
- cal.set(GregorianCalendar.SECOND, 0);
- cv.put(Events.DTEND, cal.getTimeInMillis());
- } else {
- cv.put(Events.DURATION, "P" + ((endTime - startTime) / DAYS) + "D");
- }
- cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID());
- } else {
- cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
- }
-
// Put the real event in the proper place in the ops ArrayList
if (eventOffset >= 0) {
// Store away the DTSTAMP here
@@ -537,11 +560,12 @@
}
}
- private boolean isValidEventValues(ContentValues cv, boolean update) {
- // Do a sanity check on this set of values
+ /*package*/ boolean isValidEventValues(ContentValues cv, boolean update) {
// At the very least, we must get DTSTART and _SYNC_DATA (uid)
- // If it's invalid, log the columns we've got (will help debugging)
- if (!cv.containsKey(Events.DTSTART) || !cv.containsKey(Events._SYNC_DATA)) {
+ // and one of DTEND or DURATION
+ // If the content values are invalid, log the columns (will help debugging)
+ if (!cv.containsKey(Events.DTSTART) || !cv.containsKey(Events._SYNC_DATA) ||
+ (!cv.containsKey(Events.DTEND) && !cv.containsKey(Events.DURATION))) {
userLog(TAG, (update ? "Changed" : "New") + " event invalid; skipping");
StringBuilder sb = new StringBuilder("Columns: ");
for (Entry<String, Object> entry: cv.valueSet()) {
@@ -550,6 +574,16 @@
}
userLog(TAG, sb.toString());
return false;
+ // If we've got an RRULE, we must have a DURATION
+ } else if (cv.containsKey(Events.RRULE)) {
+ String duration = cv.getAsString(Events.DURATION);
+ if (duration == null) return false;
+ if (cv.containsKey(Events.ALL_DAY)) {
+ Integer ade = cv.getAsInteger(Events.ALL_DAY);
+ if (ade != 0 && !duration.endsWith("D")) {
+ return false;
+ }
+ }
}
return true;
}
@@ -618,6 +652,17 @@
cv.put(Events.VISIBILITY, parentCv.getAsString(Events.VISIBILITY));
cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE));
+ long startTime = -1;
+ long endTime = -1;
+ int allDayEvent = 0;
+
+ if (parentCv.containsKey(Events.DTSTART)) {
+ startTime = parentCv.getAsLong(Events.DTSTART);
+ }
+ if (parentCv.containsKey(Events.DTEND)) {
+ endTime = parentCv.getAsLong(Events.DTEND);
+ }
+
// This column is the key that links the exception to the serverId
cv.put(Events.ORIGINAL_EVENT, parentCv.getAsString(Events._SYNC_ID));
@@ -635,7 +680,8 @@
}
break;
case Tags.CALENDAR_ALL_DAY_EVENT:
- cv.put(Events.ALL_DAY, getValueInt());
+ allDayEvent = getValueInt();
+ cv.put(Events.ALL_DAY, allDayEvent);
break;
case Tags.BASE_BODY:
cv.put(Events.DESCRIPTION, bodyParser());
@@ -644,10 +690,10 @@
cv.put(Events.DESCRIPTION, getValue());
break;
case Tags.CALENDAR_START_TIME:
- cv.put(Events.DTSTART, Utility.parseDateTimeToMillis(getValue()));
+ startTime = Utility.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_END_TIME:
- cv.put(Events.DTEND, Utility.parseDateTimeToMillis(getValue()));
+ endTime = Utility.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_LOCATION:
cv.put(Events.EVENT_LOCATION, getValue());
@@ -685,12 +731,11 @@
cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' +
exceptionStartTime);
- if (!cv.containsKey(Events.DTSTART)) {
- cv.put(Events.DTSTART, parentCv.getAsLong(Events.DTSTART));
- }
- if (!cv.containsKey(Events.DTEND)) {
- cv.put(Events.DTEND, parentCv.getAsLong(Events.DTEND));
- }
+ // Set up DTART and DTEND/DURATION
+ setTimes(cv, startTime, endTime, allDayEvent);
+
+ // Don't insert an invalid exception event
+ if (!isValidEventValues(cv, false)) return;
// Add the exception insert
int exceptionStart = ops.mCount;
diff --git a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
index 7f380d5..492e9d6 100644
--- a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -16,9 +16,118 @@
package com.android.exchange.adapter;
-public class CalendarSyncAdapterTests extends SyncAdapterTestCase {
+import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
+
+import android.content.ContentValues;
+import android.provider.Calendar.Events;
+
+import java.io.IOException;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.adapter.CalendarSyncAdapterTests email
+ */
+
+public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
public CalendarSyncAdapterTests() {
super();
}
+
+ public void testSetTimes() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+ ContentValues cv = new ContentValues();
+
+ // Basic, one-time meeting lasting an hour
+ GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
+ Long startTime = startCalendar.getTimeInMillis();
+ GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
+ Long endTime = endCalendar.getTimeInMillis();
+
+ p.setTimes(cv, startTime, endTime, 0);
+ assertNull(cv.getAsInteger(Events.DURATION));
+ assertEquals(startTime, cv.getAsLong(Events.DTSTART));
+ assertEquals(endTime, cv.getAsLong(Events.DTEND));
+ assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
+ assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
+
+ // Recurring meeting lasting an hour
+ cv.clear();
+ cv.put(Events.RRULE, "FREQ=DAILY");
+ p.setTimes(cv, startTime, endTime, 0);
+ assertEquals("P60M", cv.getAsString(Events.DURATION));
+ assertEquals(startTime, cv.getAsLong(Events.DTSTART));
+ assertNull(cv.getAsLong(Events.DTEND));
+ assertNull(cv.getAsLong(Events.LAST_DATE));
+ assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
+
+ // Recurring all-day event lasting one day
+ cv.clear();
+ startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
+ startTime = startCalendar.getTimeInMillis();
+ endCalendar = new GregorianCalendar(2010, 5, 11, 8, 30);
+ endTime = endCalendar.getTimeInMillis();
+ cv.put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO");
+ p.setTimes(cv, startTime, endTime, 1);
+
+ // The start time should have hour/min/sec zero'd out
+ startCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ startCalendar.set(2010, 5, 10, 0, 0, 0);
+ startCalendar.set(GregorianCalendar.MILLISECOND, 0);
+ startTime = startCalendar.getTimeInMillis();
+ assertEquals(startTime, cv.getAsLong(Events.DTSTART));
+
+ // The duration should be in days
+ assertEquals("P1D", cv.getAsString(Events.DURATION));
+ assertNull(cv.getAsLong(Events.DTEND));
+ assertNull(cv.getAsLong(Events.LAST_DATE));
+ // There must be a timezone
+ assertNotNull(cv.getAsString(Events.EVENT_TIMEZONE));
+ }
+
+ public void testIsValidEventValues() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+ long validTime = System.currentTimeMillis();
+ String validData = "foo-bar-bletch";
+ String validDuration = "P30M";
+ String validRrule = "FREQ=DAILY";
+
+ ContentValues cv = new ContentValues();
+
+ cv.put(Events.DTSTART, validTime);
+ // Needs _SYNC_DATA and DTEND/DURATION
+ assertFalse(p.isValidEventValues(cv, false));
+ cv.put(Events._SYNC_DATA, validData);
+ // Needs DTEND/DURATION
+ assertFalse(p.isValidEventValues(cv, false));
+ cv.put(Events.DURATION, validDuration);
+ // Valid (DTSTART, _SYNC_DATA, DURATION)
+ assertTrue(p.isValidEventValues(cv, false));
+ cv.remove(Events.DURATION);
+ cv.put(Events.DTEND, validTime);
+ // Valid (DTSTART, _SYNC_DATA, DTEND)
+ assertTrue(p.isValidEventValues(cv, false));
+ cv.remove(Events.DTSTART);
+ // Needs DTSTART
+ assertFalse(p.isValidEventValues(cv, false));
+ cv.put(Events.DTSTART, validTime);
+ cv.put(Events.RRULE, validRrule);
+ // With RRULE, needs DURATION
+ assertFalse(p.isValidEventValues(cv, false));
+ cv.put(Events.DURATION, "P30M");
+ // Valid (RRULE+DURATION)
+ assertTrue(p.isValidEventValues(cv, false));
+ cv.put(Events.ALL_DAY, "1");
+ // Needs DURATION in the form P<n>D
+ assertFalse(p.isValidEventValues(cv, false));
+ // Valid (RRULE+ALLDAY+DURATION(P<n>D)
+ cv.put(Events.DURATION, "P1D");
+ assertTrue(p.isValidEventValues(cv, false));
+ }
}
diff --git a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
index 6712aa7..7a4ea1b 100644
--- a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -36,7 +36,7 @@
import java.util.GregorianCalendar;
import java.util.TimeZone;
-public class EmailSyncAdapterTests extends SyncAdapterTestCase {
+public class EmailSyncAdapterTests extends SyncAdapterTestCase<EmailSyncAdapter> {
public EmailSyncAdapterTests() {
super();
@@ -73,7 +73,7 @@
}
public void testFormatDateTime() throws IOException {
- EmailSyncAdapter adapter = getTestSyncAdapter();
+ EmailSyncAdapter adapter = getTestSyncAdapter(EmailSyncAdapter.class);
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
// Calendar is odd, months are zero based, so the first 11 below is December...
calendar.set(2008, 11, 11, 18, 19, 20);
diff --git a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
index 24cf596..00b6b23 100644
--- a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
+++ b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -28,9 +28,11 @@
import java.io.ByteArrayInputStream;
import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
-public class SyncAdapterTestCase extends ProviderTestCase2<EmailProvider> {
-
+public class SyncAdapterTestCase<T extends AbstractSyncAdapter>
+ extends ProviderTestCase2<EmailProvider> {
EmailProvider mProvider;
Context mMockContext;
ContentResolver mMockResolver;
@@ -78,13 +80,24 @@
service.mContext = mMockContext;
service.mMailbox = mailbox;
service.mAccount = account;
+ service.mContentResolver = mMockResolver;
return service;
}
- EmailSyncAdapter getTestSyncAdapter() {
+ T getTestSyncAdapter(Class<T> klass) {
EasSyncService service = getTestService();
- EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
- return adapter;
+ Constructor<T> c;
+ try {
+ c = klass.getDeclaredConstructor(new Class[] {Mailbox.class, EasSyncService.class});
+ return c.newInstance(service.mMailbox, service);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ } catch (IllegalArgumentException e) {
+ } catch (InstantiationException e) {
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ }
+ return null;
}
}