Merge "Fix confusing setup screen title."
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index f5459b5..8b01f9c 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -23,6 +23,7 @@
 import com.android.exchange.utility.CalendarUtilities;
 import com.android.exchange.utility.Duration;
 
+import android.content.ContentProviderClient;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
@@ -37,12 +38,14 @@
 import android.os.RemoteException;
 import android.pim.DateException;
 import android.provider.Calendar;
+import android.provider.SyncStateContract;
 import android.provider.Calendar.Attendees;
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
 import android.provider.Calendar.EventsEntity;
 import android.provider.Calendar.ExtendedProperties;
 import android.provider.Calendar.Reminders;
+import android.provider.Calendar.SyncState;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 
@@ -61,12 +64,19 @@
     private static final String TAG = "EasCalendarSyncAdapter";
     // Since exceptions will have the same _SYNC_ID as the original event we have to check that
     // there's no original event when finding an item by _SYNC_ID
-    private static final String SERVER_ID_SELECTION = Events._SYNC_ID + "=? AND " +
+    private static final String SERVER_ID = Events._SYNC_ID + "=? AND " +
         Events.ORIGINAL_EVENT + " ISNULL";
+    private static final String DIRTY_TOP_LEVEL =
+        Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " ISNULL";
+    private static final String DIRTY_EXCEPTION =
+        Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " NOTNULL";
+    private static final String DIRTY_IN_CALENDAR =
+        Events._SYNC_DIRTY + "=1 AND " + Events.CALENDAR_ID + "=?";
     private static final String CLIENT_ID_SELECTION = Events._SYNC_LOCAL_ID + "=?";
     private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
         Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
     private static final String[] ID_PROJECTION = new String[] {Events._ID};
+    private static final String[] ORIGINAL_EVENT_PROJECTION = new String[] {Events.ORIGINAL_EVENT};
 
     private static final String CALENDAR_SELECTION =
         Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
@@ -138,19 +148,47 @@
     }
 
     /**
-     * We will eventually get our SyncKey from CalendarProvider.
+     * We get our SyncKey from CalendarProvider.  If there's not one, we set it to "0" (the reset
+     * state) and save that away.
      */
     @Override
     public String getSyncKey() throws IOException {
-        return super.getSyncKey();
+        ContentProviderClient client =
+            mService.mContentResolver.acquireContentProviderClient(Calendar.CONTENT_URI);
+        try {
+            byte[] data = SyncStateContract.Helpers.get(client,
+                    asSyncAdapter(Calendar.SyncState.CONTENT_URI), getAccountManagerAccount());
+            if (data == null || data.length == 0) {
+                // Initialize the SyncKey
+                setSyncKey("0", false);
+                return "0";
+            } else {
+                return new String(data);
+            }
+        } catch (RemoteException e) {
+            throw new IOException("Can't get SyncKey from ContactsProvider");
+        }
     }
 
     /**
-     * We will eventually set our SyncKey in CalendarProvider
+     * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
+     * cases, the SyncKey is set within Calendar
      */
     @Override
     public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
-        super.setSyncKey(syncKey, inCommands);
+        if ("0".equals(syncKey) || !inCommands) {
+            ContentProviderClient client =
+                mService.mContentResolver
+                    .acquireContentProviderClient(Calendar.CONTENT_URI);
+            try {
+                SyncStateContract.Helpers.set(client, asSyncAdapter(Calendar.SyncState.CONTENT_URI),
+                        getAccountManagerAccount(), syncKey.getBytes());
+                userLog("SyncKey set to ", syncKey, " in CalendarProvider");
+           } catch (RemoteException e) {
+                throw new IOException("Can't set SyncKey in CalendarProvider");
+            }
+        }
+        mMailbox.mSyncKey = syncKey;
     }
 
     public android.accounts.Account getAccountManagerAccount() {
@@ -171,7 +209,6 @@
         public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
                 throws IOException {
             super(in, adapter);
-            setDebug(true);
             setLoggingTag("CalendarParser");
             mAccountUri = Events.CONTENT_URI;
         }
@@ -393,7 +430,6 @@
             cv.put(Events.CALENDAR_ID, mCalendarId);
             cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
             cv.put(Events._SYNC_ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE);
-            cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID));
 
             // It appears that these values have to be copied from the parent if they are to appear
             // Note that they can be overridden below
@@ -637,7 +673,7 @@
 
         private Cursor getServerIdCursor(String serverId) {
             mBindArgument[0] = serverId;
-            return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID,
                     mBindArgument, null);
         }
 
@@ -669,16 +705,6 @@
             }
         }
 
-        class ServerChange {
-            long id;
-            boolean read;
-
-            ServerChange(long _id, boolean _read) {
-                id = _id;
-                read = _read;
-            }
-        }
-
         /**
          * A change is handled as a delete (including all exceptions) and an add
          * This isn't as efficient as attempting to traverse the original and all of its exceptions,
@@ -694,6 +720,7 @@
                         serverId = getValue();
                         break;
                     case Tags.SYNC_APPLICATION_DATA:
+                        userLog("Changing " + serverId);
                         addEvent(ops, serverId, true);
                         break;
                     default:
@@ -722,15 +749,19 @@
         @Override
         public void commit() throws IOException {
             userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
-            // Save the syncKey here, using the Helper provider by Contacts provider
-            //ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
-            //        getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
+            // Save the syncKey here, using the Helper provider by Calendar provider
+            mOps.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+                    getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
 
             // Execute these all at once...
             mOps.execute();
 
             if (mOps.mResults != null) {
-                // Clear dirty flag if necessary...
+                // Clear dirty flags for Events sent to server
+                ContentValues cv = new ContentValues();
+                cv.put(Events._SYNC_DIRTY, 0);
+                mContentResolver.update(sEventsUri, cv, DIRTY_IN_CALENDAR,
+                        new String[] {Long.toString(mCalendarId)});
             }
         }
 
@@ -913,6 +944,194 @@
         return Integer.toString(easVisibility);
     }
 
+    private void sendEvent(Entity entity, String clientId, Serializer s)
+            throws IOException {
+        // Serialize for EAS here
+        // Set uid with the client id we created
+        // 1) Serialize the top-level event
+        // 2) Serialize attendees and reminders from subvalues
+        // 3) Look for exceptions and serialize with the top-level event
+        ContentValues entityValues = entity.getEntityValues();
+        boolean isException = (clientId == null);
+
+        if (entityValues.containsKey(Events.ALL_DAY)) {
+            s.data(Tags.CALENDAR_ALL_DAY_EVENT,
+                    entityValues.getAsInteger(Events.ALL_DAY).toString());
+        }
+
+        long startTime = entityValues.getAsLong(Events.DTSTART);
+        s.data(Tags.CALENDAR_START_TIME,
+                CalendarUtilities.millisToEasDateTime(startTime));
+
+        if (!entityValues.containsKey(Events.DURATION)) {
+            if (entityValues.containsKey(Events.DTEND)) {
+                s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(
+                        entityValues.getAsLong(Events.DTEND)));
+            }
+        } else {
+            // Convert this into millis and add it to DTSTART for DTEND
+            // We'll use 1 hour as a default
+            long durationMillis = HOURS;
+            Duration duration = new Duration();
+            try {
+                duration.parse(entityValues.getAsString(Events.DURATION));
+            } catch (DateException e) {
+                // Can't do much about this; use the default (1 hour)
+            }
+            s.data(Tags.CALENDAR_END_TIME,
+                    CalendarUtilities.millisToEasDateTime(startTime + durationMillis));
+        }
+
+        s.data(Tags.CALENDAR_DTSTAMP,
+                CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+
+        if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+            s.data(Tags.CALENDAR_LOCATION,
+                    entityValues.getAsString(Events.EVENT_LOCATION));
+        }
+        if (entityValues.containsKey(Events.TITLE)) {
+            s.data(Tags.CALENDAR_SUBJECT, entityValues.getAsString(Events.TITLE));
+        }
+
+        if (entityValues.containsKey(Events.VISIBILITY)) {
+            s.data(Tags.CALENDAR_SENSITIVITY,
+                    decodeVisibility(entityValues.getAsInteger(Events.VISIBILITY)));
+        } else {
+            // Private if not set
+            s.data(Tags.CALENDAR_SENSITIVITY, "1");
+        }
+
+        if (!isException) {
+            // A time zone is required in all EAS events; we'll use the default if none
+            // is set.
+            String timeZoneName;
+            if (entityValues.containsKey(Events.EVENT_TIMEZONE)) {
+                timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
+            } else {
+                timeZoneName = TimeZone.getDefault().getID();
+            }
+            String x = CalendarUtilities.timeZoneToTZIString(timeZoneName);
+            s.data(Tags.CALENDAR_TIME_ZONE, x);
+
+            if (entityValues.containsKey(Events.DESCRIPTION)) {
+                String desc = entityValues.getAsString(Events.DESCRIPTION);
+                if (mService.mProtocolVersionDouble >= 12.0) {
+                    s.start(Tags.BASE_BODY);
+                    s.data(Tags.BASE_TYPE, "1");
+                    s.data(Tags.BASE_DATA, desc);
+                    s.end();
+                } else {
+                    s.data(Tags.CALENDAR_BODY, desc);
+                }
+            }
+
+            if (entityValues.containsKey(Events.ORGANIZER)) {
+                s.data(Tags.CALENDAR_ORGANIZER_EMAIL,
+                        entityValues.getAsString(Events.ORGANIZER));
+            }
+
+            if (entityValues.containsKey(Events.RRULE)) {
+                CalendarUtilities.recurrenceFromRrule(
+                        entityValues.getAsString(Events.RRULE), startTime, s);
+            }
+
+            // Handle associated data EXCEPT for attendees, which have to be grouped
+            ArrayList<NamedContentValues> subValues = entity.getSubValues();
+            for (NamedContentValues ncv: subValues) {
+                Uri ncvUri = ncv.uri;
+                ContentValues ncvValues = ncv.values;
+                if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
+                    if (ncvValues.containsKey("uid")) {
+                        clientId = ncvValues.getAsString("uid");
+                        s.data(Tags.CALENDAR_UID, clientId);
+                    }
+                    if (ncvValues.containsKey("dtstamp")) {
+                        s.data(Tags.CALENDAR_DTSTAMP, ncvValues.getAsString("dtstamp"));
+                    }
+                    if (ncvValues.containsKey("categories")) {
+                        // Send all the categories back to the server
+                        // We've saved them as a String of delimited tokens
+                        String categories = ncvValues.getAsString("categories");
+                        StringTokenizer st =
+                            new StringTokenizer(categories, CATEGORY_TOKENIZER_DELIMITER);
+                        if (st.countTokens() > 0) {
+                            s.start(Tags.CALENDAR_CATEGORIES);
+                            while (st.hasMoreTokens()) {
+                                String category = st.nextToken();
+                                s.data(Tags.CALENDAR_CATEGORY, category);
+                            }
+                            s.end();
+                        }
+                    }
+                } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
+                    if (ncvValues.containsKey(Reminders.MINUTES)) {
+                        s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE,
+                                ncvValues.getAsString(Reminders.MINUTES));
+                    }
+                }
+            }
+
+            // We've got to send a UID.  If the event is new, we've generated one; if not,
+            // we should have gotten one from extended properties.
+            s.data(Tags.CALENDAR_UID, clientId);
+
+            // Handle attendee data here; keep track of organizer and stream it afterward
+            boolean hasAttendees = false;
+            String organizerName = null;
+            for (NamedContentValues ncv: subValues) {
+                Uri ncvUri = ncv.uri;
+                ContentValues ncvValues = ncv.values;
+                if (ncvUri.equals(Attendees.CONTENT_URI)) {
+                    if (ncvValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
+                        int relationship =
+                            ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+                        // Organizer isn't among attendees in EAS
+                        if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+                            if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
+                                // Remember this; we can't insert it into the stream in
+                                // the middle of attendees
+                                organizerName =
+                                    ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+                            }
+                            continue;
+                        }
+                        if (!hasAttendees) {
+                            s.start(Tags.CALENDAR_ATTENDEES);
+                            hasAttendees = true;
+                        }
+                        s.start(Tags.CALENDAR_ATTENDEE);
+                        if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
+                            s.data(Tags.CALENDAR_ATTENDEE_NAME,
+                                    ncvValues.getAsString(Attendees.ATTENDEE_NAME));
+                        }
+                        if (ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+                            s.data(Tags.CALENDAR_ATTENDEE_EMAIL,
+                                    ncvValues.getAsString(Attendees.ATTENDEE_EMAIL));
+                        }
+                        s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
+                        s.end(); // Attendee
+                    }
+                    // If there's no relationship, we can't create this for EAS
+                }
+            }
+            if (hasAttendees) {
+                s.end();  // Attendees
+            }
+            if (organizerName != null) {
+                s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
+            }
+        } else {
+            // TODO Add reminders to exceptions (allow them to be specified!)
+            if (entityValues.containsKey(Events.ORIGINAL_INSTANCE_TIME)) {
+                s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+                        CalendarUtilities.millisToEasDateTime(entityValues.getAsLong(
+                                Events.ORIGINAL_INSTANCE_TIME)));
+            } else {
+                // Illegal; what should we do?
+            }
+        }
+    }
+
     @Override
     public boolean sendLocalChanges(Serializer s) throws IOException {
         ContentResolver cr = mService.mContentResolver;
@@ -925,15 +1144,31 @@
         }
 
         try {
-            // TODO This just handles NEW events at the moment
-            // Cheap way to handle changes would be to delete/add
-            EntityIterator ei = EventsEntity.newEntityIterator(
-                    cr.query(uri, null, Events._SYNC_ID + " ISNULL", null, null), cr);
+            // We've got to handle exceptions as part of the parent when changes occur, so we need
+            // to find new/changed exceptions and mark the parent dirty
+            Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION,
+                    null, null);
+            try {
+                ContentValues cv = new ContentValues();
+                cv.put(Events._SYNC_DIRTY, 1);
+                // Mark the parent dirty in this loop
+                while (c.moveToNext()) {
+                    String serverId = c.getString(0);
+                    cr.update(asSyncAdapter(Events.CONTENT_URI), cv, SERVER_ID,
+                             new String[] {serverId});
+                }
+            } finally {
+                c.close();
+            }
+
+            // Now we can go through dirty top-level events and send them back to the server
+            EntityIterator eventIterator = EventsEntity.newEntityIterator(
+                    cr.query(uri, null, DIRTY_TOP_LEVEL, null, null), cr);
             ContentValues cidValues = new ContentValues();
             try {
                 boolean first = true;
-                while (ei.hasNext()) {
-                    Entity entity = ei.next();
+                while (eventIterator.hasNext()) {
+                    Entity entity = eventIterator.next();
                     String clientId = "uid_" + mMailbox.mId + '_' + System.currentTimeMillis();
 
                     // For each of these entities, create the change commands
@@ -949,8 +1184,6 @@
                     // TODO Handle BusyStatus for EAS 2.5
                     // What should it be??
 
-                    // Ignore exceptions (will have Events.ORIGINAL_EVENT)
-
                     if (first) {
                         s.start(Tags.SYNC_COMMANDS);
                         userLog("Sending Calendar changes to the server");
@@ -962,7 +1195,6 @@
                         s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
                         // And save it in the Event as the local id
                         cidValues.put(Events._SYNC_LOCAL_ID, clientId);
-                        // TODO sync adapter!
                         cr.update(ContentUris.
                                 withAppendedId(uri,
                                         entityValues.getAsLong(Events._ID)),
@@ -978,173 +1210,29 @@
                         s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
                     }
                     s.start(Tags.SYNC_APPLICATION_DATA);
+                    sendEvent(entity, clientId, s);
 
-                    // Serialize for EAS here
-                    // Set uid with the client id we created
-                    // 1) Serialize the top-level event
-                    // 2) Serialize attendees and reminders from subvalues
-                    // 3) Look for exceptions and serialize with the top-level event
-                    if (entityValues.containsKey(Events.ALL_DAY)) {
-                        s.data(Tags.CALENDAR_ALL_DAY_EVENT,
-                                entityValues.getAsInteger(Events.ALL_DAY).toString());
-                    }
-
-                    long startTime = entityValues.getAsLong(Events.DTSTART);
-                    s.data(Tags.CALENDAR_START_TIME,
-                            CalendarUtilities.millisToEasDateTime(startTime));
-                    // Convert this into millis and add it to DTSTART for DTEND
-                    // We'll use 1 hour as a default
-                    long durationMillis = HOURS;
-                    Duration duration = new Duration();
-                    try {
-                        duration.parse(entityValues.getAsString(Events.DURATION));
-                    } catch (DateException e) {
-                        // Can't do much about this; use the default (1 hour)
-                    }
-                    s.data(Tags.CALENDAR_END_TIME,
-                            CalendarUtilities.millisToEasDateTime(startTime + durationMillis));
-                    if (entityValues.containsKey(Events.DTEND)) {
-                        // TODO Use this to determine last date; it's NOT the same as EAS DTEND
-                        //long endTime = entityValues.getAsLong(Events.DTEND);
-                        //s.data(Tags.CALENDAR_END_TIME,
-                        //        CalendarUtilities.millisToEasDateTime(endTime));
-                    }
-                    s.data(Tags.CALENDAR_DTSTAMP,
-                            CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
-
-                    // A time zone is required in all EAS events; we'll use the default if none
-                    // is set.
-                    String timeZoneName;
-                    if (entityValues.containsKey(Events.EVENT_TIMEZONE)) {
-                        timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
-                    } else {
-                        timeZoneName = TimeZone.getDefault().getID();
-                    }
-                    String x = CalendarUtilities.timeZoneToTZIString(timeZoneName);
-                    s.data(Tags.CALENDAR_TIME_ZONE, x);
-
-                    if (entityValues.containsKey(Events.EVENT_LOCATION)) {
-                        s.data(Tags.CALENDAR_LOCATION,
-                                entityValues.getAsString(Events.EVENT_LOCATION));
-                    }
-                    if (entityValues.containsKey(Events.TITLE)) {
-                        s.data(Tags.CALENDAR_SUBJECT, entityValues.getAsString(Events.TITLE));
-                    }
-                    if (entityValues.containsKey(Events.DESCRIPTION)) {
-                        String desc = entityValues.getAsString(Events.DESCRIPTION);
-                        if (mService.mProtocolVersionDouble >= 12.0) {
-                            s.start(Tags.BASE_BODY);
-                            s.data(Tags.BASE_TYPE, "1");
-                            s.data(Tags.BASE_DATA, desc);
-                            s.end();
-                        } else {
-                            s.data(Tags.CALENDAR_BODY, desc);
+                    // Now, the hard part; find exceptions for this event
+                    if (serverId != null) {
+                        EntityIterator exceptionIterator = EventsEntity.newEntityIterator(
+                                cr.query(uri, null, Events.ORIGINAL_EVENT + "=?",
+                                        new String[] {serverId}, null), cr);
+                        boolean exFirst = true;
+                        while (exceptionIterator.hasNext()) {
+                            Entity exceptionEntity = exceptionIterator.next();
+                            if (exFirst) {
+                                s.start(Tags.CALENDAR_EXCEPTIONS);
+                                exFirst = false;
+                            }
+                            s.start(Tags.CALENDAR_EXCEPTION);
+                            sendEvent(exceptionEntity, null, s);
+                            s.end(); // EXCEPTION
                         }
-                    }
-                    if (entityValues.containsKey(Events.ORGANIZER)) {
-                        s.data(Tags.CALENDAR_ORGANIZER_EMAIL,
-                                entityValues.getAsString(Events.ORGANIZER));
-                    }
-                    if (entityValues.containsKey(Events.VISIBILITY)) {
-                        s.data(Tags.CALENDAR_SENSITIVITY,
-                                decodeVisibility(entityValues.getAsInteger(Events.VISIBILITY)));
-                    } else {
-                        // Private if not set
-                        s.data(Tags.CALENDAR_SENSITIVITY, "1");
-                    }
-                    if (entityValues.containsKey(Events.RRULE)) {
-                        CalendarUtilities.recurrenceFromRrule(
-                                entityValues.getAsString(Events.RRULE), startTime, s);
-                    }
-
-                    // Handle associated data EXCEPT for attendees, which have to be grouped
-                    ArrayList<NamedContentValues> subValues = entity.getSubValues();
-                    for (NamedContentValues ncv: subValues) {
-                        Uri ncvUri = ncv.uri;
-                        ContentValues ncvValues = ncv.values;
-                        if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
-                            if (ncvValues.containsKey("uid")) {
-                                clientId = ncvValues.getAsString("uid");
-                                s.data(Tags.CALENDAR_UID, clientId);
-                            }
-                            if (ncvValues.containsKey("dtstamp")) {
-                                s.data(Tags.CALENDAR_DTSTAMP, ncvValues.getAsString("dtstamp"));
-                            }
-                            if (ncvValues.containsKey("categories")) {
-                                // Send all the categories back to the server
-                                // We've saved them as a String of delimited tokens
-                                String categories = ncvValues.getAsString("categories");
-                                StringTokenizer st =
-                                    new StringTokenizer(categories, CATEGORY_TOKENIZER_DELIMITER);
-                                if (st.countTokens() > 0) {
-                                    s.start(Tags.CALENDAR_CATEGORIES);
-                                    while (st.hasMoreTokens()) {
-                                        String category = st.nextToken();
-                                        s.data(Tags.CALENDAR_CATEGORY, category);
-                                    }
-                                    s.end();
-                                }
-                            }
-                        } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
-                            if (ncvValues.containsKey(Reminders.MINUTES)) {
-                                s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE,
-                                        ncvValues.getAsString(Reminders.MINUTES));
-                            }
+                        if (!exFirst) {
+                            s.end(); // EXCEPTIONS
                         }
                     }
 
-                    // We've got to send a UID.  If the event is new, we've generated one; if not,
-                    // we should have gotten one from extended properties.
-                    s.data(Tags.CALENDAR_UID, clientId);
-
-                    // Handle attendee data here; keep track of organizer and stream it afterward
-                    boolean hasAttendees = false;
-                    String organizerName = null;
-                    for (NamedContentValues ncv: subValues) {
-                        Uri ncvUri = ncv.uri;
-                        ContentValues ncvValues = ncv.values;
-                        if (ncvUri.equals(Attendees.CONTENT_URI)) {
-                            if (ncvValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
-                                int relationship =
-                                    ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
-                                // Organizer isn't among attendees in EAS
-                                if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
-                                    if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
-                                        // Remember this; we can't insert it into the stream in
-                                        // the middle of attendees
-                                        organizerName =
-                                            ncvValues.getAsString(Attendees.ATTENDEE_NAME);
-                                    }
-                                    continue;
-                                }
-                                if (!hasAttendees) {
-                                    s.start(Tags.CALENDAR_ATTENDEES);
-                                    hasAttendees = true;
-                                }
-                                s.start(Tags.CALENDAR_ATTENDEE);
-                                if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
-                                    s.data(Tags.CALENDAR_ATTENDEE_NAME,
-                                            ncvValues.getAsString(Attendees.ATTENDEE_NAME));
-                                }
-                                if (ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
-                                    s.data(Tags.CALENDAR_ATTENDEE_EMAIL,
-                                            ncvValues.getAsString(Attendees.ATTENDEE_EMAIL));
-                                }
-                                s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
-                                s.end(); // Attendee
-                            }
-                            // If there's no relationship, we can't create this for EAS
-                        }
-                    }
-                    if (hasAttendees) {
-                        s.end();  // Attendees
-                    }
-                    if (organizerName != null) {
-                        s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
-                    }
-//                    case Tags.CALENDAR_EXCEPTIONS:
-//                        exceptionsParser(ops, cv);
-//                        break;
                     s.end().end(); // ApplicationData & Change
                     mUpdatedIdList.add(entityValues.getAsLong(Events._ID));
                 }
@@ -1152,7 +1240,7 @@
                     s.end(); // Commands
                 }
             } finally {
-                ei.close();
+                eventIterator.close();
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Could not read dirty events.");