am 6c1de36d: AI 149721: Import Portuguese translations.
Merge commit '6c1de36d4021ed6986bdbc8cc664fa08f6bc0ee6' into donut
* commit '6c1de36d4021ed6986bdbc8cc664fa08f6bc0ee6':
AI 149721: Import Portuguese translations.
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 296d4c5..a32038c 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -15,11 +15,11 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="gadget_title">"Kalender"</string>
+ <!-- no translation found for gadget_title (3673051912300198724) -->
+ <skip />
<string name="no_title_label">"(Mangler emne)"</string>
- <plurals name="gadget_more_events">
- <item quantity="one">"én hendelse til"</item>
- <item quantity="other">"<xliff:g id="NUMBER">%d</xliff:g> hendelser til"</item>
- </plurals>
- <string name="gadget_no_events">"Ingen framtidige kalenderhendelser"</string>
+ <!-- no translation found for gadget_more_events:one (4209472169696459059) -->
+ <!-- no translation found for gadget_more_events:other (2850138968496439596) -->
+ <!-- no translation found for gadget_no_events (5219179980474526479) -->
+ <skip />
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 69c3549..ba167da 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -19,7 +19,7 @@
<string name="no_title_label">"(Geen onderwerp)"</string>
<plurals name="gadget_more_events">
<item quantity="one">"Nog 1 afspraak"</item>
- <item quantity="other">"Nog <xliff:g id="NUMBER">%d</xliff:g> andere afspraken"</item>
+ <item quantity="other">"Nog <xliff:g id="NUMBER">%d</xliff:g>andere afspraken"</item>
</plurals>
<string name="gadget_no_events">"Geen lopende agenda-afspraken"</string>
</resources>
diff --git a/src/com/android/providers/calendar/CalendarAppWidgetService.java b/src/com/android/providers/calendar/CalendarAppWidgetService.java
index e69a154..a6418cd 100644
--- a/src/com/android/providers/calendar/CalendarAppWidgetService.java
+++ b/src/com/android/providers/calendar/CalendarAppWidgetService.java
@@ -271,16 +271,20 @@
}
return result;
}
-
+
/**
* Calculate flipping point for the given event; when we should hide this
* event and show the next one. This is usually half-way through the event.
+ * <p>
+ * Events with duration longer than one day as treated as all-day events
+ * when computing the flipping point.
*
* @param start Event start time in local timezone.
* @param end Event end time in local timezone.
*/
private long getEventFlip(Cursor cursor, long start, long end, boolean allDay) {
- if (allDay) {
+ long duration = end - start;
+ if (allDay || duration > DateUtils.DAY_IN_MILLIS) {
return start;
} else {
return (start + end) / 2;
diff --git a/src/com/android/providers/calendar/CalendarProvider.java b/src/com/android/providers/calendar/CalendarProvider.java
index ead3d12..59cc896 100644
--- a/src/com/android/providers/calendar/CalendarProvider.java
+++ b/src/com/android/providers/calendar/CalendarProvider.java
@@ -145,6 +145,11 @@
0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff,
};
+ // To determine if a recurrence exception originally overlapped the
+ // window, we need to assume a maximum duration, since we only know
+ // the original start time.
+ private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000;
+
public static final class TimeRange {
public long begin;
public long end;
@@ -1633,82 +1638,122 @@
Debug.startMethodTracing("expandInstanceRangeLocked");
}
- final SQLiteDatabase db = getDatabase();
- Cursor entries = null;
-
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Expanding events between " + begin + " and " + end);
}
+ Cursor entries = getEntries(begin, end);
try {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
- qb.setProjectionMap(sEventsProjectionMap);
-
- String beginString = String.valueOf(begin);
- String endString = String.valueOf(end);
-
- qb.appendWhere("(dtstart <= ");
- qb.appendWhere(endString);
- qb.appendWhere(" AND ");
- qb.appendWhere("(lastDate IS NULL OR lastDate >= ");
- qb.appendWhere(beginString);
- qb.appendWhere(")) OR (");
- // grab recurrence exceptions that fall outside our expansion window but modify
- // recurrences that do fall within our window. we won't insert these into the output
- // set of instances, but instead will just add them to our cancellations list, so we
- // can cancel the correct recurrence expansion instances.
- qb.appendWhere("originalInstanceTime IS NOT NULL ");
- qb.appendWhere("AND originalInstanceTime <= ");
- qb.appendWhere(endString);
- qb.appendWhere(" AND ");
- // we don't have originalInstanceDuration or end time. for now, assume the original
- // instance lasts no longer than 1 week.
- // TODO: compute the originalInstanceEndTime or get this from the server.
- qb.appendWhere("originalInstanceTime >= ");
- qb.appendWhere(String.valueOf(begin - 7*24*60*60*1000 /* 1 week */));
- qb.appendWhere(")");
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Retrieving events to expand: " + qb.toString());
+ performInstanceExpansion(begin, end, localTimezone, entries);
+ } finally {
+ if (entries != null) {
+ entries.close();
}
+ }
+ if (PROFILE) {
+ Debug.stopMethodTracing();
+ }
+ }
- entries = qb.query(db, EXPAND_COLUMNS, null, null, null, null, null);
+ /**
+ * Get all entries affecting the given window.
+ * @param begin Window start (ms).
+ * @param end Window end (ms).
+ * @return Cursor for the entries; caller must close it.
+ */
+ private Cursor getEntries(long begin, long end) {
+ final SQLiteDatabase db = getDatabase();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
+ qb.setProjectionMap(sEventsProjectionMap);
- RecurrenceProcessor rp = new RecurrenceProcessor();
+ String beginString = String.valueOf(begin);
+ String endString = String.valueOf(end);
- int statusColumn = entries.getColumnIndex(Events.STATUS);
- int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
- int dtendColumn = entries.getColumnIndex(Events.DTEND);
- int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
- int durationColumn = entries.getColumnIndex(Events.DURATION);
- int rruleColumn = entries.getColumnIndex(Events.RRULE);
- int rdateColumn = entries.getColumnIndex(Events.RDATE);
- int exruleColumn = entries.getColumnIndex(Events.EXRULE);
- int exdateColumn = entries.getColumnIndex(Events.EXDATE);
- int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
- int idColumn = entries.getColumnIndex(Events._ID);
- int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
- int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
- int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
+ qb.appendWhere("(dtstart <= ");
+ qb.appendWhere(endString);
+ qb.appendWhere(" AND ");
+ qb.appendWhere("(lastDate IS NULL OR lastDate >= ");
+ qb.appendWhere(beginString);
+ qb.appendWhere(")) OR (");
+ // grab recurrence exceptions that fall outside our expansion window but modify
+ // recurrences that do fall within our window. we won't insert these into the output
+ // set of instances, but instead will just add them to our cancellations list, so we
+ // can cancel the correct recurrence expansion instances.
+ qb.appendWhere("originalInstanceTime IS NOT NULL ");
+ qb.appendWhere("AND originalInstanceTime <= ");
+ qb.appendWhere(endString);
+ qb.appendWhere(" AND ");
+ // we don't have originalInstanceDuration or end time. for now, assume the original
+ // instance lasts no longer than 1 week.
+ // TODO: compute the originalInstanceEndTime or get this from the server.
+ qb.appendWhere("originalInstanceTime >= ");
+ qb.appendWhere(String.valueOf(begin - MAX_ASSUMED_DURATION));
+ qb.appendWhere(")");
- ContentValues initialValues;
- EventInstancesMap instancesMap = new EventInstancesMap();
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Retrieving events to expand: " + qb.toString());
+ }
- Duration duration = new Duration();
- Time eventTime = new Time();
+ return qb.query(db, EXPAND_COLUMNS, null, null, null, null, null);
+ }
- while (entries.moveToNext()) {
+ /**
+ * Perform instance expansion on the given entries.
+ * @param begin Window start (ms).
+ * @param end Window end (ms).
+ * @param localTimezone
+ * @param entries The entries to process.
+ */
+ private void performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries) {
+ RecurrenceProcessor rp = new RecurrenceProcessor();
+
+ int statusColumn = entries.getColumnIndex(Events.STATUS);
+ int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
+ int dtendColumn = entries.getColumnIndex(Events.DTEND);
+ int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
+ int durationColumn = entries.getColumnIndex(Events.DURATION);
+ int rruleColumn = entries.getColumnIndex(Events.RRULE);
+ int rdateColumn = entries.getColumnIndex(Events.RDATE);
+ int exruleColumn = entries.getColumnIndex(Events.EXRULE);
+ int exdateColumn = entries.getColumnIndex(Events.EXDATE);
+ int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
+ int idColumn = entries.getColumnIndex(Events._ID);
+ int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
+ int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
+ int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
+
+ ContentValues initialValues;
+ EventInstancesMap instancesMap = new EventInstancesMap();
+
+ Duration duration = new Duration();
+ Time eventTime = new Time();
+
+ // Invariant: entries contains all events that affect the current
+ // window. It consists of:
+ // a) Individual events that fall in the window. These will be
+ // displayed.
+ // b) Recurrences that included the window. These will be displayed
+ // if not canceled.
+ // c) Recurrence exceptions that fall in the window. These will be
+ // displayed if not cancellations.
+ // d) Recurrence exceptions that modify an instance inside the
+ // window (subject to 1 week assumption above), but are outside
+ // the window. These will not be displayed. Cases c and d are
+ // distingushed by the start / end time.
+
+ while (entries.moveToNext()) {
+ try {
initialValues = null;
boolean allDay = entries.getInt(allDayColumn) != 0;
String eventTimezone = entries.getString(eventTimezoneColumn);
if (allDay || TextUtils.isEmpty(eventTimezone)) {
- // in the events table, allDay events start at midnight.
- // this forces them to stay at midnight for all day events
- // TODO: check that this actually does the right thing.
- eventTimezone = Time.TIMEZONE_UTC;
+ // in the events table, allDay events start at midnight.
+ // this forces them to stay at midnight for all day events
+ // TODO: check that this actually does the right thing.
+ eventTimezone = Time.TIMEZONE_UTC;
}
long dtstartMillis = entries.getLong(dtstartColumn);
@@ -1754,7 +1799,7 @@
if (status == Events.STATUS_CANCELED) {
// should not happen!
Log.e(TAG, "Found canceled recurring event in "
- + "Events table. Ignoring.");
+ + "Events table. Ignoring.");
continue;
}
@@ -1796,15 +1841,7 @@
}
long[] dates;
- try {
- dates = rp.expand(eventTime, recur, begin, end);
- } catch (DateException e) {
- Log.w(TAG, "RecurrenceProcessor.expand skipping " + recur, e);
- continue;
- } catch (TimeFormatException e) {
- Log.w(TAG, "RecurrenceProcessor.expand skipping " + recur, e);
- continue;
- }
+ dates = rp.expand(eventTime, recur, begin, end);
// Initialize the "eventTime" timezone outside the loop.
// This is used in computeTimezoneDependentFields().
@@ -1857,7 +1894,12 @@
// add events to the instances map if they don't actually fall within our
// expansion window.
if ((dtendMillis < begin) || (dtstartMillis > end)) {
- initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
+ if (originalEvent != null && originalInstanceTimeMillis != -1) {
+ initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
+ } else {
+ Log.w(TAG, "Unexpected event outside window: " + syncId);
+ continue;
+ }
}
initialValues.put(Instances.EVENT_ID, eventId);
@@ -1875,99 +1917,104 @@
instancesMap.add(syncId, initialValues);
}
+ } catch (DateException e) {
+ Log.w(TAG, "RecurrenceProcessor error ", e);
+ } catch (TimeFormatException e) {
+ Log.w(TAG, "RecurrenceProcessor error ", e);
}
+ }
- // First, delete the original instances corresponding to recurrence
- // exceptions. We do this by iterating over the list and for each
- // recurrence exception, we search the list for an instance with a
- // matching "original instance time". If we find such an instance,
- // we remove it from the list. If we don't find such an instance
- // then we cancel the recurrence exception.
- Set<String> keys = instancesMap.keySet();
- for (String syncId : keys) {
- InstancesList list = instancesMap.get(syncId);
- for (ContentValues values : list) {
+ // Invariant: instancesMap contains all instances that affect the
+ // window, indexed by original sync id. It consists of:
+ // a) Individual events that fall in the window. They have:
+ // EVENT_ID, BEGIN, END
+ // b) Instances of recurrences that fall in the window. They may
+ // be subject to exceptions. They have:
+ // EVENT_ID, BEGIN, END
+ // c) Exceptions that fall in the window. They have:
+ // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS (since they can
+ // be a modification or cancellation), EVENT_ID, BEGIN, END
+ // d) Recurrence exceptions that modify an instance inside the
+ // window but fall outside the window. They have:
+ // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS =
+ // STATUS_CANCELED, EVENT_ID, BEGIN, END
- // If this instance is not a recurrence exception, then
- // skip it.
- if (!values.containsKey(Events.ORIGINAL_EVENT)) {
- continue;
- }
+ // First, delete the original instances corresponding to recurrence
+ // exceptions. We do this by iterating over the list and for each
+ // recurrence exception, we search the list for an instance with a
+ // matching "original instance time". If we find such an instance,
+ // we remove it from the list. If we don't find such an instance
+ // then we cancel the recurrence exception.
+ Set<String> keys = instancesMap.keySet();
+ for (String syncId : keys) {
+ InstancesList list = instancesMap.get(syncId);
+ for (ContentValues values : list) {
- String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
- long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- InstancesList originalList = instancesMap.get(originalEvent);
- if (originalList == null) {
- // The original recurrence is not present, so don't try canceling it.
- continue;
- }
+ // If this instance is not a recurrence exception, then
+ // skip it.
+ if (!values.containsKey(Events.ORIGINAL_EVENT)) {
+ continue;
+ }
- // Search the original event for a matching original
- // instance time. If there is a matching one, then remove
- // the original one. We do this both for exceptions that
- // change the original instance as well as for exceptions
- // that delete the original instance.
- boolean found = false;
- for (int num = originalList.size() - 1; num >= 0; num--) {
- ContentValues originalValues = originalList.get(num);
- long beginTime = originalValues.getAsLong(Instances.BEGIN);
- if (beginTime == originalTime) {
- // We found the original instance, so remove it.
- found = true;
- originalList.remove(num);
- }
- }
+ String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
+ long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ InstancesList originalList = instancesMap.get(originalEvent);
+ if (originalList == null) {
+ // The original recurrence is not present, so don't try canceling it.
+ continue;
+ }
- // If we didn't find a matching instance time then cancel
- // this recurrence exception.
- if (!found) {
- values.put(Events.STATUS, Events.STATUS_CANCELED);
+ // Search the original event for a matching original
+ // instance time. If there is a matching one, then remove
+ // the original one. We do this both for exceptions that
+ // change the original instance as well as for exceptions
+ // that delete the original instance.
+ for (int num = originalList.size() - 1; num >= 0; num--) {
+ ContentValues originalValues = originalList.get(num);
+ long beginTime = originalValues.getAsLong(Instances.BEGIN);
+ if (beginTime == originalTime) {
+ // We found the original instance, so remove it.
+ originalList.remove(num);
}
}
}
+ }
- // Now do the inserts. Since the db lock is held when this method is executed,
- // this will be done in a transaction.
- // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
- // while the calendar app is trying to query the db (expanding instances)), we will
- // not be "polite" and yield the lock until we're done. This will favor local query
- // operations over sync/write operations.
- for (String syncId : keys) {
- InstancesList list = instancesMap.get(syncId);
- for (ContentValues values : list) {
+ // Invariant: instancesMap contains filtered instances.
+ // It consists of:
+ // a) Individual events that fall in the window.
+ // b) Instances of recurrences that fall in the window and have not
+ // been subject to exceptions.
+ // c) Exceptions that fall in the window. They will have
+ // STATUS_CANCELED if they are cancellations.
+ // d) Recurrence exceptions that modify an instance inside the
+ // window but fall outside the window. These are STATUS_CANCELED.
- // If this instance was cancelled then don't create a new
- // instance.
- Integer status = values.getAsInteger(Events.STATUS);
- if (status != null && status == Events.STATUS_CANCELED) {
- continue;
- }
+ // Now do the inserts. Since the db lock is held when this method is executed,
+ // this will be done in a transaction.
+ // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
+ // while the calendar app is trying to query the db (expanding instances)), we will
+ // not be "polite" and yield the lock until we're done. This will favor local query
+ // operations over sync/write operations.
+ for (String syncId : keys) {
+ InstancesList list = instancesMap.get(syncId);
+ for (ContentValues values : list) {
- // Remove these fields before inserting a new instance
- values.remove(Events.ORIGINAL_EVENT);
- values.remove(Events.ORIGINAL_INSTANCE_TIME);
- values.remove(Events.STATUS);
-
- mInstancesInserter.replace(values);
- if (false) {
- // yield the lock if anyone else is trying to
- // perform a db operation here.
- db.yieldIfContended();
- }
+ // If this instance was cancelled then don't create a new
+ // instance.
+ Integer status = values.getAsInteger(Events.STATUS);
+ if (status != null && status == Events.STATUS_CANCELED) {
+ continue;
}
- }
- } catch (TimeFormatException e) {
- Log.w(TAG, "Exception in instance query preparation", e);
- }
- finally {
- if (entries != null) {
- entries.close();
+
+ // Remove these fields before inserting a new instance
+ values.remove(Events.ORIGINAL_EVENT);
+ values.remove(Events.ORIGINAL_INSTANCE_TIME);
+ values.remove(Events.STATUS);
+
+ mInstancesInserter.replace(values);
}
}
- if (PROFILE) {
- Debug.stopMethodTracing();
- }
- //System.out.println("EXIT insertInstanceRange begin=" + begin + " end=" + end);
}
/**
@@ -2681,15 +2728,17 @@
db.update("Events", values, "_id="+eventId, null);
}
+ /**
+ * Updates the instances table when an event is added or updated.
+ * @param values The new values of the event.
+ * @param rowId The database row id of the event.
+ * @param newEvent true if the event is new.
+ * @param db The database
+ */
private void updateInstancesLocked(ContentValues values,
- long rowId,
- boolean newEvent,
- SQLiteDatabase db) {
- if (isRecurrenceEvent(values)) {
- // TODO: insert the new recurrence into the instances table.
- mMetaData.clearInstanceRange();
- return;
- }
+ long rowId,
+ boolean newEvent,
+ SQLiteDatabase db) {
// If there are no expanded Instances, then return.
MetaData.Fields fields = mMetaData.getFieldsLocked();
@@ -2697,13 +2746,7 @@
return;
}
- // if the event is in the expanded range, insert
- // into the instances table.
- // TODO: deal with durations. currently, durations are only used in
- // recurrences.
-
Long dtstartMillis = values.getAsLong(Events.DTSTART);
-
if (dtstartMillis == null) {
if (newEvent) {
// must be present for a new event.
@@ -2714,15 +2757,47 @@
return;
}
+ Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
+ Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+
if (!newEvent) {
+ // Want to do this for regular event, recurrence, or exception.
+ // For recurrence or exception, more deletion may happen below if we
+ // do an instance expansion. This deletion will suffice if the exception
+ // is moved outside the window, for instance.
db.delete("Instances", "event_id=" + rowId, null /* selectionArgs */);
}
+ if (isRecurrenceEvent(values)) {
+ // The recurrence or exception needs to be (re-)expanded if:
+ // a) Exception or recurrence that falls inside window
+ boolean insideWindow = dtstartMillis <= fields.maxInstance &&
+ (lastDateMillis == null || lastDateMillis >= fields.minInstance);
+ // b) Exception that affects instance inside window
+ // These conditions match the query in getEntries
+ // See getEntries comment for explanation of subtracting 1 week.
+ boolean affectsWindow = originalInstanceTime != null &&
+ originalInstanceTime <= fields.maxInstance &&
+ originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
+ if (insideWindow || affectsWindow) {
+ updateRecurrenceInstancesLocked(values, rowId, db);
+ }
+ // TODO: an exception creation or update could be optimized by
+ // updating just the affected instances, instead of regenerating
+ // the recurrence.
+ return;
+ }
+
Long dtendMillis = values.getAsLong(Events.DTEND);
if (dtendMillis == null) {
dtendMillis = dtstartMillis;
}
+ // if the event is in the expanded range, insert
+ // into the instances table.
+ // TODO: deal with durations. currently, durations are only used in
+ // recurrences.
+
if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
ContentValues instanceValues = new ContentValues();
instanceValues.put(Instances.EVENT_ID, rowId);
@@ -2748,6 +2823,102 @@
}
}
+ /**
+ * Determines the recurrence entries associated with a particular recurrence.
+ * This set is the base recurrence and any exception.
+ *
+ * Normally the entries are indicated by the sync id of the base recurrence
+ * (which is the originalEvent in the exceptions).
+ * However, a complication is that a recurrence may not yet have a sync id.
+ * In that case, the recurrence is specified by the rowId.
+ *
+ * @param recurrenceSyncId The sync id of the base recurrence, or null.
+ * @param rowId The row id of the base recurrence.
+ * @return the relevant entries.
+ */
+ private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
+ final SQLiteDatabase db = getDatabase();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
+ qb.setProjectionMap(sEventsProjectionMap);
+ if (recurrenceSyncId == null) {
+ String where = "Events._id = " + rowId;
+ qb.appendWhere(where);
+ } else {
+ String where = "Events._sync_id = \"" + recurrenceSyncId + "\""
+ + " OR Events.originalEvent = \"" + recurrenceSyncId + "\"";
+ qb.appendWhere(where);
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Retrieving events to expand: " + qb.toString());
+ }
+
+ return qb.query(db, EXPAND_COLUMNS, null /* selection */, null /* selectionArgs */, null /* groupBy */, null /* having */, null /* sortOrder */);
+ }
+
+ /**
+ * Do incremental Instances update of a recurrence or recurrence exception.
+ *
+ * This method does performInstanceExpansion on just the modified recurrence,
+ * to avoid the overhead of recomputing the entire instance table.
+ *
+ * @param values The new values of the event.
+ * @param rowId The database row id of the event.
+ * @param db The database
+ */
+ private void updateRecurrenceInstancesLocked(ContentValues values,
+ long rowId,
+ SQLiteDatabase db) {
+ MetaData.Fields fields = mMetaData.getFieldsLocked();
+ String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
+ String recurrenceSyncId = null;
+ if (originalEvent != null) {
+ recurrenceSyncId = originalEvent;
+ } else {
+ // Get the recurrence's sync id from the database
+ recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events"
+ + " WHERE _id = " + rowId, null /* selection args */);
+ }
+ // recurrenceSyncId is the _sync_id of the underlying recurrence
+ // If the recurrence hasn't gone to the server, it will be null.
+
+ // Need to clear out old instances
+ if (recurrenceSyncId == null) {
+ // Creating updating a recurrence that hasn't gone to the server.
+ // Need to delete based on row id
+ String where = "_id IN (SELECT Instances._id as _id"
+ + " FROM Instances INNER JOIN Events"
+ + " ON (Events._id = Instances.event_id)"
+ + " WHERE Events._id =?)";
+ db.delete("Instances", where, new String[]{"" + rowId});
+ } else {
+ // Creating or modifying a recurrence or exception.
+ // Delete instances for recurrence (_sync_id = recurrenceSyncId)
+ // and all exceptions (originalEvent = recurrenceSyncId)
+ String where = "_id IN (SELECT Instances._id as _id"
+ + " FROM Instances INNER JOIN Events"
+ + " ON (Events._id = Instances.event_id)"
+ + " WHERE Events._sync_id =?"
+ + " OR Events.originalEvent =?)";
+ db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId});
+ }
+
+ // Now do instance expansion
+ Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
+ try {
+ performInstanceExpansion(fields.minInstance, fields.maxInstance, fields.timezone, entries);
+ } finally {
+ if (entries != null) {
+ entries.close();
+ }
+ }
+
+ // Clear busy bits
+ mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
+ 0 /* startDay */, 0 /* endDay */);
+ }
+
long calculateLastDate(ContentValues values)
throws DateException {
// Allow updates to some event fields like the title or hasAlarm
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapter.java b/src/com/android/providers/calendar/CalendarSyncAdapter.java
index c84a4ff..0c53c6a 100644
--- a/src/com/android/providers/calendar/CalendarSyncAdapter.java
+++ b/src/com/android/providers/calendar/CalendarSyncAdapter.java
@@ -62,6 +62,7 @@
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
+import java.util.TimeZone;
import java.util.Vector;
/**
@@ -302,7 +303,11 @@
// in order to format the "originalStartTime" correctly.
boolean originalAllDay = c.getInt(c.getColumnIndex(Events.ORIGINAL_ALL_DAY)) != 0;
- Time originalTime = new Time(c.getString(c.getColumnIndex(Events.EVENT_TIMEZONE)));
+ String timezone = c.getString(c.getColumnIndex(Events.EVENT_TIMEZONE));
+ if (TextUtils.isEmpty(timezone)) {
+ timezone = TimeZone.getDefault().getID();
+ }
+ Time originalTime = new Time(timezone);
originalTime.set(originalStartTime);
utc.set(originalStartTime);
diff --git a/tests/src/com/android/providers/calendar/CalendarProviderTest.java b/tests/src/com/android/providers/calendar/CalendarProviderTest.java
index aaf7699..7aa3cb2 100644
--- a/tests/src/com/android/providers/calendar/CalendarProviderTest.java
+++ b/tests/src/com/android/providers/calendar/CalendarProviderTest.java
@@ -54,10 +54,12 @@
private MockContentResolver mResolver;
private Uri mEventsUri = Uri.parse("content://calendar/events");
private int mCalendarId;
+
+ protected boolean mWipe = 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.
- private int mGlobalSyncId = 1;
+ private int mGlobalSyncId = 1000;
/**
* KeyValue is a simple class that stores a pair of strings representing
@@ -136,13 +138,22 @@
this.eventName = eventName;
this.pairs = pairs;
}
-
+
public void execute() {
Log.i(TAG, "update " + eventName);
+ if (mWipe) {
+ // Wipe instance table so it will be regenerated
+ mMetaData.clearInstanceRange();
+ }
ContentValues map = new ContentValues();
for (KeyValue pair : pairs) {
String value = pair.value;
- map.put(pair.key, value);
+ if (Calendar.EventsColumns.STATUS.equals(pair.key)) {
+ // Do type conversion for STATUS
+ map.put(pair.key, Integer.parseInt(value));
+ } else {
+ map.put(pair.key, value);
+ }
}
updateMatchingEvents(eventName, map);
}
@@ -261,15 +272,18 @@
this.instances[index++] = time.toMillis(false /* use isDst */);
}
}
-
+
public void execute() {
Cursor cursor = queryInstances(begin, end);
int len = 0;
if (instances != null) {
len = instances.length;
}
+ if (len != cursor.getCount()) {
+ dumpCursor(cursor);
+ }
assertEquals("number of instances don't match", len, cursor.getCount());
-
+
if (instances == null) {
return;
}
@@ -306,6 +320,9 @@
title, date);
Log.e(TAG, mesg);
}
+ if (!found) {
+ dumpCursor(cursor);
+ }
assertTrue(found);
}
cursor.close();
@@ -388,7 +405,7 @@
String mOriginalTitle;
long mOriginalInstance;
int mSyncId;
-
+
// Constructor for normal events, using the default timezone
public EventInfo(String title, String startDate, String endDate,
boolean allDay) {
@@ -577,6 +594,12 @@
new EventInfo("daily0", "2008-05-02T00:00:00",
"except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008",
"2008-01-02T00:00:00", "2008-01-02T01:00:00", false),
+ new EventInfo("weekly0", "2008-05-13T13:00:00",
+ "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm",
+ "2008-12-11T13:00:00", "2008-12-11T14:00:00", false),
+ new EventInfo("weekly0", "2008-05-13T13:00:00",
+ "cancel0", "weekly0 exception for 5/13/2008 1pm",
+ "2008-05-13T13:00:00", "2008-05-13T14:00:00", false),
new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm",
"2008-05-01T13:00:00", "2008-05-01T14:00:00",
"FREQ=YEARLY;WKST=SU", false),
@@ -821,13 +844,46 @@
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}),
};
+
+ /**
+ * This sequence of commands deletes (cancels) one instance of a recurrence.
+ */
+ private Command[] mCancelInstance = {
+ new Insert("weekly0"),
+ new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
+ new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00",
+ "2008-05-20T13:00:00", }),
+ new Insert("cancel0"),
+ new Update("cancel0", new KeyValue[] {
+ new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED),
+ }),
+ new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
+ new String[] {"2008-05-06T13:00:00",
+ "2008-05-20T13:00:00", }),
+ };
+ /**
+ * This sequence of commands creates a recurring event with a recurrence
+ * exception that moves an event from outside the expansion window into the
+ * expansion window.
+ */
+ private Command[] mExceptionWithMovedRecurrence2 = {
+ new Insert("weekly0"),
+ new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
+ new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
+ "2008-12-16T13:00:00", }),
+ new Insert("except3"),
+ new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
+ new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
+ "2008-12-11T13:00:00", "2008-12-16T13:00:00", }),
+ };
/**
* This sequence of commands creates a recurring event with a recurrence
* exception and then changes the end time of the recurring event. It then
* checks that the recurrence exception does not occur in the Instances
* database table.
*/
- private Command[] mExceptionWithTruncatedRecurrence = {
+ private Command[]
+ mExceptionWithTruncatedRecurrence = {
new Insert("daily0"),
// Verify 4 occurrences of the "daily0" repeating event
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
@@ -857,6 +913,12 @@
new Update("daily0", new KeyValue[] {
new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"),
}),
+ // The server will cancel the out-of-range exception.
+ // It would be nice for the provider to handle this automatically,
+ // but for now simulate the server-side cancel.
+ new Update("except1", new KeyValue[] {
+ new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED),
+ }),
// Verify that the recurrence exception does not appear.
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
@@ -911,6 +973,7 @@
* @param cursor the database cursor
*/
private void dumpCursor(Cursor cursor) {
+ cursor.moveToPosition(-1);
String[] cols = cursor.getColumnNames();
Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
@@ -923,6 +986,7 @@
Log.i(TAG, "}");
index += 1;
}
+ cursor.moveToPosition(-1);
}
private int insertCal(String name, String timezone) {
@@ -938,6 +1002,10 @@
}
private Uri insertEvent(int calId, EventInfo event) {
+ if (mWipe) {
+ // Wipe instance table so it will be regenerated
+ mMetaData.clearInstanceRange();
+ }
ContentValues m = new ContentValues();
m.put(Events.CALENDAR_ID, calId);
m.put(Events.TITLE, event.mTitle);
@@ -959,7 +1027,7 @@
if (event.mTimezone != null) {
m.put(Events.EVENT_TIMEZONE, event.mTimezone);
}
-
+
if (event.mOriginalTitle != null) {
// This is a recurrence exception.
EventInfo recur = findEvent(event.mOriginalTitle);
@@ -1014,6 +1082,7 @@
Events.ALL_DAY,
Events.RRULE,
Events.EVENT_TIMEZONE,
+ Events.ORIGINAL_EVENT,
};
Cursor cursor = mResolver.query(mEventsUri, projection,
"title=?", new String[] { title }, null);
@@ -1026,13 +1095,15 @@
if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
|| values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
|| values.containsKey(Events.RRULE)
- || values.containsKey(Events.EVENT_TIMEZONE)) {
+ || values.containsKey(Events.EVENT_TIMEZONE)
+ || values.containsKey(Calendar.EventsColumns.STATUS)) {
long dtstart = cursor.getLong(1);
long dtend = cursor.getLong(2);
String duration = cursor.getString(3);
boolean allDay = cursor.getInt(4) != 0;
String rrule = cursor.getString(5);
String timezone = cursor.getString(6);
+ String originalEvent = cursor.getString(7);
if (!values.containsKey(Events.DTSTART)) {
values.put(Events.DTSTART, dtstart);
@@ -1053,8 +1124,11 @@
if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
values.put(Events.EVENT_TIMEZONE, timezone);
}
+ if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) {
+ values.put(Events.ORIGINAL_EVENT, originalEvent);
+ }
}
-
+
Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
numRows += mResolver.update(uri, values, null, null);
}
@@ -1288,18 +1362,43 @@
}
}
- public void testCommandSequences() throws Exception {
+ /**
+ * Run commands, wiping instance table at each step.
+ * This tests full instance expansion.
+ * @throws Exception
+ */
+ public void testCommandSequences1() throws Exception {
+ commandSequences(true);
+ }
+
+ /**
+ * Run commands normally.
+ * This tests incremental instance expansion.
+ * @throws Exception
+ */
+ public void testCommandSequences2() throws Exception {
+ commandSequences(false);
+ }
+
+ /**
+ * Run thorough set of command sequences
+ * @param wipe true if instances should be wiped and regenerated
+ * @throws Exception
+ */
+ private void commandSequences(boolean wipe) throws Exception {
Cursor cursor;
Uri url = null;
+ mWipe = wipe; // Set global flag
mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
+ Command[] commands;
Log.i(TAG, "Normal insert/delete");
- Command[] commands = mNormalInsertDelete;
+ commands = mNormalInsertDelete;
for (Command command : commands) {
command.execute();
}
@@ -1338,6 +1437,22 @@
deleteAllEvents();
+ Log.i(TAG, "Exception with cancel");
+ commands = mCancelInstance;
+ for (Command command : commands) {
+ command.execute();
+ }
+
+ deleteAllEvents();
+
+ Log.i(TAG, "Exception with moved recurrence2");
+ commands = mExceptionWithMovedRecurrence2;
+ for (Command command : commands) {
+ command.execute();
+ }
+
+ deleteAllEvents();
+
Log.i(TAG, "Exception with no recurrence");
commands = mExceptionWithNoRecurrence;
for (Command command : commands) {