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) {