Fix upsync for email.

Calendar & Contact upsync are still stubs.

Change-Id: I62c168ce7e7aef449d6ff5a89919aed1fd6ce5c8
diff --git a/src/com/android/exchange/EasAuthenticationException.java b/src/com/android/exchange/EasAuthenticationException.java
index f5b14b9..2e7847f 100644
--- a/src/com/android/exchange/EasAuthenticationException.java
+++ b/src/com/android/exchange/EasAuthenticationException.java
@@ -25,7 +25,7 @@
 public class EasAuthenticationException extends IOException {
     private static final long serialVersionUID = 1L;
 
-    EasAuthenticationException() {
+    public EasAuthenticationException() {
         super();
     }
 }
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index e729809..8a50e86 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -1076,7 +1076,7 @@
             if (status == HttpStatus.SC_OK) {
                 if (!resp.isEmpty()) {
                     InputStream is = resp.getInputStream();
-                    MoveItemsParser p = new MoveItemsParser(is, this);
+                    MoveItemsParser p = new MoveItemsParser(is);
                     p.parse();
                     int statusCode = p.getStatusCode();
                     ContentValues cv = new ContentValues();
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index 09b908a..b364d91 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -161,8 +161,8 @@
                         // we sync folders"...
                         throw new IOException();
                     } else if (status == 7) {
-                        // TODO: Fix this.
-                        //mService.mUpsyncFailed = true;
+                        // TODO: Fix this. The handling here used to be pretty bogus, and it's not
+                        // obvious that simply forcing another resync makes sense here.
                         moreAvailable = true;
                     } else {
                         // Access, provisioning, transient, etc.
diff --git a/src/com/android/exchange/adapter/MoveItemsParser.java b/src/com/android/exchange/adapter/MoveItemsParser.java
index 542331d..a654c76 100644
--- a/src/com/android/exchange/adapter/MoveItemsParser.java
+++ b/src/com/android/exchange/adapter/MoveItemsParser.java
@@ -15,7 +15,7 @@
 
 package com.android.exchange.adapter;
 
-import com.android.exchange.EasSyncService;
+import com.android.mail.utils.LogUtils;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -24,7 +24,7 @@
  * Parse the result of a MoveItems command.
  */
 public class MoveItemsParser extends Parser {
-    private final EasSyncService mService;
+    private static final String TAG = "MoveItemsParser";
     private int mStatusCode = 0;
     private String mNewServerId;
 
@@ -42,9 +42,8 @@
     public static final int STATUS_CODE_REVERT = 2;
     public static final int STATUS_CODE_RETRY = 3;
 
-    public MoveItemsParser(InputStream in, EasSyncService service) throws IOException {
+    public MoveItemsParser(InputStream in) throws IOException {
         super(in);
-        mService = service;
     }
 
     public int getStatusCode() {
@@ -83,11 +82,11 @@
                 }
                 if (status != STATUS_SUCCESS) {
                     // There's not much to be done if this fails
-                    mService.userLog("Error in MoveItems: " + status);
+                    LogUtils.i(TAG, "Error in MoveItems: %d", status);
                 }
             } else if (tag == Tags.MOVE_DSTMSGID) {
                 mNewServerId = getValue();
-                mService.userLog("Moved message id is now: " + mNewServerId);
+                LogUtils.i(TAG, "Moved message id is now: %s", mNewServerId);
             } else {
                 skipTag();
             }
diff --git a/src/com/android/exchange/service/EasCalendarSyncHandler.java b/src/com/android/exchange/service/EasCalendarSyncHandler.java
index da37e6e..44c262f 100644
--- a/src/com/android/exchange/service/EasCalendarSyncHandler.java
+++ b/src/com/android/exchange/service/EasCalendarSyncHandler.java
@@ -24,7 +24,7 @@
 import java.io.InputStream;
 
 /**
- *
+ * Performs an Exchange Sync for a Calendar collection.
  */
 public class EasCalendarSyncHandler extends EasSyncHandler {
 
@@ -121,4 +121,8 @@
 
     }
 
+    @Override
+    protected void cleanup(final int syncResult) {
+        // Nothing to do.
+    }
 }
diff --git a/src/com/android/exchange/service/EasContactsSyncHandler.java b/src/com/android/exchange/service/EasContactsSyncHandler.java
index 6eeac87..773aee4 100644
--- a/src/com/android/exchange/service/EasContactsSyncHandler.java
+++ b/src/com/android/exchange/service/EasContactsSyncHandler.java
@@ -160,4 +160,10 @@
     protected void setUpsyncCommands(final Serializer s) throws IOException {
 
     }
+
+
+    @Override
+    protected void cleanup(final int syncResult) {
+
+    }
 }
diff --git a/src/com/android/exchange/service/EasMailboxSyncHandler.java b/src/com/android/exchange/service/EasMailboxSyncHandler.java
index 0798946..60f3316 100644
--- a/src/com/android/exchange/service/EasMailboxSyncHandler.java
+++ b/src/com/android/exchange/service/EasMailboxSyncHandler.java
@@ -1,10 +1,16 @@
 package com.android.exchange.service;
 
+import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
+import android.content.OperationApplicationException;
 import android.content.SyncResult;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.os.RemoteException;
+import android.text.format.DateUtils;
 
 import com.android.emailcommon.TrafficFlags;
 import com.android.emailcommon.provider.Account;
@@ -15,19 +21,118 @@
 import com.android.emailcommon.provider.Mailbox;
 import com.android.emailcommon.service.SyncWindow;
 import com.android.exchange.Eas;
+import com.android.exchange.EasAuthenticationException;
+import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.AbstractSyncParser;
 import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.exchange.adapter.MoveItemsParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
+import com.android.mail.utils.LogUtils;
+
+import org.apache.http.HttpStatus;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
 
 /**
  * Performs an Exchange mailbox sync for "normal" mailboxes.
  */
 public class EasMailboxSyncHandler extends EasSyncHandler {
+    private static final String TAG = "EasMailboxSyncHandler";
+
+    /**
+     * The projection used for building the fetch request list.
+     */
+    private static final String[] FETCH_REQUEST_PROJECTION = { SyncColumns.SERVER_ID };
+    private static final int FETCH_REQUEST_SERVER_ID = 0;
+
+    /**
+     * The projection used for querying message tables for the purpose of determining what needs
+     * to be set as a sync update.
+     */
+    private static final String[] UPDATES_PROJECTION = { MessageColumns.ID,
+            MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
+            MessageColumns.FLAGS, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE };
+    private static final int UPDATES_ID_COLUMN = 0;
+    private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
+    private static final int UPDATES_SERVER_ID_COLUMN = 2;
+    private static final int UPDATES_FLAG_COLUMN = 3;
+    private static final int UPDATES_READ_COLUMN = 4;
+    private static final int UPDATES_FAVORITE_COLUMN = 5;
+
+    /**
+     * Message flags value to signify that the message has been moved, and eventually needs to be
+     * deleted.
+     */
+    public static final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
+
+    /**
+     * The selection for moved messages that get deleted after a successful sync.
+     */
+    private static final String WHERE_MAILBOX_KEY_AND_MOVED =
+            MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
+            MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
+
+    private static final String EMAIL_WINDOW_SIZE = "5";
+
+    private static final String WHERE_BODY_SOURCE_MESSAGE_KEY =
+            EmailContent.Body.SOURCE_MESSAGE_KEY + "=?";
+
+    // State needed across multiple functions during a Sync.
+    // TODO: We should perhaps invert the meaning of mDeletedMessages & mUpdatedMessages and
+    // store the values we want to *retain* after a successful upsync.
+
+    /**
+     * List of message ids that were read from Message_Deletes and were sent in the Commands section
+     * of the current sync.
+     */
+    private final ArrayList<Long> mDeletedMessages = new ArrayList<Long>();
+
+    /**
+     * List of message ids that were read from Message_Updates and were sent in the Commands section
+     * of the current sync.
+     */
+    private final ArrayList<Long> mUpdatedMessages = new ArrayList<Long>();
+
+    /**
+     * List of server ids for messages to fetch from the server.
+     */
+    private final ArrayList<String> mMessagesToFetch = new ArrayList<String>();
+
+    /**
+     * Holds all the data needed to process a MoveItems request.
+     */
+    private static class MoveRequest {
+        public final long messageId;
+        public final String messageServerId;
+        public final int messageFlags;
+        public final long sourceFolderId;
+        public final String sourceFolderServerId;
+        public final String destFolderServerId;
+
+        public MoveRequest(final long _messageId, final String _messageServerId,
+                final int _messageFlags,
+                final long _sourceFolderId, final String _sourceFolderServerId,
+                final String _destFolderServerId) {
+            messageId = _messageId;
+            messageServerId = _messageServerId;
+            messageFlags = _messageFlags;
+            sourceFolderId = _sourceFolderId;
+            sourceFolderServerId = _sourceFolderServerId;
+            destFolderServerId = _destFolderServerId;
+        }
+    }
+
+    /**
+     * List of all MoveRequests, i.e. all messages which have different mailboxes than they used to.
+     */
+    private final ArrayList<MoveRequest> mMessagesToMove = new ArrayList<MoveRequest>();
 
     public EasMailboxSyncHandler(final Context context, final ContentResolver contentResolver,
             final Account account, final Mailbox mailbox, final Bundle syncExtras,
@@ -35,26 +140,6 @@
         super(context, contentResolver, account, mailbox, syncExtras, syncResult);
     }
 
-    private static final String[] FETCH_REQUEST_PROJECTION =
-            new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID};
-    private static final int FETCH_REQUEST_RECORD_ID = 0;
-    private static final int FETCH_REQUEST_SERVER_ID = 1;
-
-    private static final String EMAIL_WINDOW_SIZE = "5";
-
-    /**
-     * Holder for fetch request information (record id and server id)
-     */
-    private static class FetchRequest {
-        final long messageId;
-        final String serverId;
-
-        FetchRequest(final long _messageId, final String _serverId) {
-            messageId = _messageId;
-            serverId = _serverId;
-        }
-    }
-
     private String getEmailFilter() {
         int syncLookback = mMailbox.mSyncLookback;
         if (syncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN
@@ -81,26 +166,23 @@
         }
     }
 
-    private ArrayList<FetchRequest> createFetchRequestList(final boolean initialSync) {
-        final ArrayList<FetchRequest> fetchRequestList = new ArrayList<FetchRequest>();
-        if (!initialSync) {
-            // Find partially loaded messages; this should typically be a rare occurrence
-            final Cursor c = mContentResolver.query(Message.CONTENT_URI, FETCH_REQUEST_PROJECTION,
-                    MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
-                    MessageColumns.MAILBOX_KEY + "=?", new String[] {Long.toString(mMailbox.mId)},
-                    null);
-
+    /**
+     * Find partially loaded messages and add their server ids to {@link #mMessagesToFetch}.
+     */
+    private void addToFetchRequestList() {
+        final Cursor c = mContentResolver.query(Message.CONTENT_URI, FETCH_REQUEST_PROJECTION,
+                MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
+                MessageColumns.MAILBOX_KEY + "=?", new String[] {Long.toString(mMailbox.mId)},
+                null);
+        if (c != null) {
             try {
-                // Put all of these messages into a list; we'll need both id and server id
                 while (c.moveToNext()) {
-                    fetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID),
-                            c.getString(FETCH_REQUEST_SERVER_ID)));
+                    mMessagesToFetch.add(c.getString(FETCH_REQUEST_SERVER_ID));
                 }
             } finally {
                 c.close();
             }
         }
-        return fetchRequestList;
     }
 
     @Override
@@ -137,15 +219,15 @@
 
     @Override
     protected void setNonInitialSyncOptions(final Serializer s) throws IOException {
+        // Check for messages that aren't fully loaded.
+        addToFetchRequestList();
         // The "empty" case is typical; we send a request for changes, and also specify a sync
         // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
         // truncation
         // If there are fetch requests, we only want the fetches (i.e. no changes from the server)
         // so we turn MIME support off.  Note that we are always using EAS 2.5 if there are fetch
         // requests
-        // TODO: Fix.
-        final boolean hasFetchRequests = false;
-        if (!hasFetchRequests) {
+        if (mMessagesToFetch.isEmpty()) {
             // Permanently delete if in trash mailbox
             // In Exchange 2003, deletes-as-moves tag = true; no tag = false
             // In Exchange 2007 and up, deletes-as-moves tag is "0" (false) or "1" (true)
@@ -181,17 +263,434 @@
             }
             s.end();
         } else {
+            // If we have any messages that are not fully loaded, ask for plain text rather than
+            // MIME, to guarantee we'll get usable text body. This also means we should NOT ask for
+            // new messages -- we only want data for the message explicitly fetched.
             s.start(Tags.SYNC_OPTIONS);
-            // Ask for plain text, rather than MIME data.  This guarantees that we'll get a usable
-            // text body
             s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
             s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
             s.end();
         }
     }
 
+    /**
+     * Check whether a message is referenced by another (and therefore must be kept around).
+     * @param messageId The id of the message to check.
+     * @return Whether the message in question is referenced by another message.
+     */
+    private boolean messageReferenced(final long messageId) {
+        final Cursor c = mContentResolver.query(EmailContent.Body.CONTENT_URI,
+                EmailContent.Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
+                new String[] {Long.toString(messageId)}, null);
+        if (c != null) {
+            try {
+                return c.moveToFirst();
+            } finally {
+                c.close();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Write the command to delete a message to the {@link Serializer}.
+     * @param s The {@link Serializer} for this sync command.
+     * @param serverId The server id for the message to delete.
+     * @param firstCommand Whether any sync commands have already been written to s.
+     * @throws IOException
+     */
+    private void addDeleteMessageCommand(final Serializer s, final String serverId,
+            final boolean firstCommand) throws IOException {
+        if (firstCommand) {
+            s.start(Tags.SYNC_COMMANDS);
+        }
+        s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+    }
+
+    /**
+     * Adds a sync delete command for all messages in the Message_Deletes table.
+     * @param s The {@link Serializer} for this sync command.
+     * @param hasCommands Whether any Commands have already been written to s.
+     * @return Whether this function wrote any commands to s.
+     * @throws IOException
+     */
+    private boolean addDeletedCommands(final Serializer s, final boolean hasCommands)
+            throws IOException {
+        boolean wroteCommands = false;
+        final Cursor c = mContentResolver.query(Message.DELETED_CONTENT_URI,
+                Message.ID_COLUMNS_PROJECTION, MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId,
+                null, null);
+        if (c != null) {
+            try {
+                while (c.moveToNext()) {
+                    final String serverId = c.getString(Message.ID_COLUMNS_SYNC_SERVER_ID);
+                    final long messageId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
+                    // Only upsync delete commands for messages that have server ids and are not
+                    // referenced by other messages.
+                    if (serverId != null && !messageReferenced(messageId)) {
+                        addDeleteMessageCommand(s, serverId, wroteCommands || hasCommands);
+                        wroteCommands = true;
+                        mDeletedMessages.add(messageId);
+                    }
+                }
+            } finally {
+                c.close();
+            }
+        }
+
+        return wroteCommands;
+    }
+
+    /**
+     * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
+     * a different format that excludes the punctuation (this is why I'm not putting this in a
+     * parent class)
+     */
+    private static String formatDateTime(final Calendar calendar) {
+        final StringBuilder sb = new StringBuilder();
+        //YYYY-MM-DDTHH:MM:SS.MSSZ
+        sb.append(calendar.get(Calendar.YEAR));
+        sb.append('-');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MONTH) + 1));
+        sb.append('-');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
+        sb.append('T');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
+        sb.append(':');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MINUTE)));
+        sb.append(':');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.SECOND)));
+        sb.append(".000Z");
+        return sb.toString();
+    }
+
+    /**
+     * Get the server id for a mailbox from the content provider.
+     * @param mailboxId The id of the mailbox we're interested in.
+     * @return The server id for the mailbox.
+     */
+    private String getServerIdForMailbox(final long mailboxId) {
+        final Cursor c = mContentResolver.query(
+                ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
+                new String [] { EmailContent.MailboxColumns.SERVER_ID }, null, null, null);
+        if (c != null) {
+            try {
+                if (c.moveToNext()) {
+                    return c.getString(0);
+                }
+            } finally {
+                c.close();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * For a message that's in the Message_Updates table, add a sync command to the
+     * {@link Serializer} if appropriate, and add the message to a list if it should be removed from
+     * Message_Updates.
+     * @param updatedMessageCursor The {@link Cursor} positioned at the message from Message_Updates
+     *                             that we're processing.
+     * @param s The {@link Serializer} for this sync command.
+     * @param hasCommands Whether the {@link Serializer} already has sync commands added to it.
+     * @param trashMailboxId The id for the trash mailbox.
+     * @return Whether we added a sync command to s.
+     * @throws IOException
+     */
+    private boolean handleOneUpdatedMessage(final Cursor updatedMessageCursor, final Serializer s,
+            final boolean hasCommands, final long trashMailboxId) throws IOException {
+        final long messageId = updatedMessageCursor.getLong(UPDATES_ID_COLUMN);
+        // Get the current state of this message (updated table has original state).
+        final Cursor currentCursor = mContentResolver.query(
+                ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
+                UPDATES_PROJECTION, null, null, null);
+        if (currentCursor == null) {
+            // If, somehow, the message isn't still around, we still want to handle it as having
+            // been updated so that it gets removed from the updated table.
+            mUpdatedMessages.add(messageId);
+            return false;
+        }
+
+        try {
+            if (currentCursor.moveToFirst()) {
+                final String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
+                if (serverId == null) {
+                    // No serverId means there's nothing to do, but we should still remove from the
+                    // updated table.
+                    mUpdatedMessages.add(messageId);
+                    return false;
+                }
+
+                final long currentMailboxId = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
+                final int currentFlags = currentCursor.getInt(UPDATES_FLAG_COLUMN);
+
+                // Handle message deletion (i.e. move to trash).
+                if (currentMailboxId == trashMailboxId) {
+                    mUpdatedMessages.add(messageId);
+                    addDeleteMessageCommand(s, serverId, !hasCommands);
+                    // Also mark the message as moved in the DB (so the copy will be deleted if/when
+                    // the server version is synced)
+                    final ContentValues cv = new ContentValues(1);
+                    cv.put(MessageColumns.FLAGS, currentFlags | MESSAGE_FLAG_MOVED_MESSAGE);
+                    mContentResolver.update(
+                            ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
+                            cv, null, null);
+                    return true;
+                }
+
+                // Handle message moved.
+                final long originalMailboxId =
+                        updatedMessageCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
+                if (currentMailboxId != originalMailboxId) {
+                    final String sourceMailboxId = getServerIdForMailbox(originalMailboxId);
+                    final String destMailboxId;
+                    if (sourceMailboxId != null) {
+                        destMailboxId = getServerIdForMailbox(currentMailboxId);
+                    } else {
+                        destMailboxId = null;
+                    }
+                    if (destMailboxId != null) {
+                        mMessagesToMove.add(
+                                new MoveRequest(messageId, serverId, currentFlags,
+                                        originalMailboxId, sourceMailboxId, destMailboxId));
+                        // Since we don't want to remove this message from updated table until it
+                        // downsyncs, we do not add it to updatedIds.
+                    } else {
+                        // TODO: If the message's mailboxes aren't there, handle it better.
+                    }
+                } else {
+                    mUpdatedMessages.add(messageId);
+                }
+
+                final int favorite;
+                final boolean favoriteChanged;
+                // We can only send flag changes to the server in 12.0 or later
+                if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                    favorite = currentCursor.getInt(UPDATES_FAVORITE_COLUMN);
+                    favoriteChanged =
+                            favorite != updatedMessageCursor.getInt(UPDATES_FAVORITE_COLUMN);
+                } else {
+                    favorite = 0;
+                    favoriteChanged = false;
+                }
+
+                final int read = currentCursor.getInt(UPDATES_READ_COLUMN);
+                final boolean readChanged =
+                        read != updatedMessageCursor.getInt(UPDATES_READ_COLUMN);
+
+                if (favoriteChanged || readChanged) {
+                    if (!hasCommands) {
+                        s.start(Tags.SYNC_COMMANDS);
+                    }
+                    s.start(Tags.SYNC_CHANGE);
+                    s.data(Tags.SYNC_SERVER_ID, serverId);
+                    s.start(Tags.SYNC_APPLICATION_DATA);
+                    if (readChanged) {
+                        s.data(Tags.EMAIL_READ, Integer.toString(read));
+                    }
+
+                    // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
+                    // the boolean "favorite" that we think of in Gmail, but it also represents a
+                    // follow up action, which can include a subject, start and due dates, and even
+                    // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
+                    // require that a flag contain a status, a type, and four date fields, two each
+                    // for start date and end (due) date.
+                    if (favoriteChanged) {
+                        if (favorite != 0) {
+                            // Status 2 = set flag
+                            s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
+                            // "FollowUp" is the standard type
+                            s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
+                            final long now = System.currentTimeMillis();
+                            final Calendar calendar =
+                                GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
+                            calendar.setTimeInMillis(now);
+                            // Flags are required to have a start date and end date (duplicated)
+                            // First, we'll set the current date/time in GMT as the start time
+                            String utc = formatDateTime(calendar);
+                            s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
+                            // And then we'll use one week from today for completion date
+                            calendar.setTimeInMillis(now + DateUtils.WEEK_IN_MILLIS);
+                            utc = formatDateTime(calendar);
+                            s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
+                            s.end();
+                        } else {
+                            s.tag(Tags.EMAIL_FLAG);
+                        }
+                    }
+                    s.end().end();  // SYNC_APPLICATION_DATA, SYNC_CHANGE
+                    return true;
+                }
+            }
+        } finally {
+            currentCursor.close();
+        }
+        return false;
+    }
+
+    /**
+     * Send all message move requests and process responses.
+     * TODO: Make this just one request/response, which requires changes to the parser.
+     * @throws IOException
+     */
+    private void performMessageMove() throws IOException {
+
+        for (final MoveRequest req : mMessagesToMove) {
+            final Serializer s = new Serializer();
+            s.start(Tags.MOVE_MOVE_ITEMS);
+            s.start(Tags.MOVE_MOVE);
+            s.data(Tags.MOVE_SRCMSGID, req.messageServerId);
+            s.data(Tags.MOVE_SRCFLDID, req.sourceFolderServerId);
+            s.data(Tags.MOVE_DSTFLDID, req.destFolderServerId);
+            s.end();
+            s.end().done();
+            final EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
+            try {
+                final int status = resp.getStatus();
+                if (status == HttpStatus.SC_OK) {
+                    if (!resp.isEmpty()) {
+                        final MoveItemsParser p = new MoveItemsParser(resp.getInputStream());
+                        p.parse();
+                        final int statusCode = p.getStatusCode();
+                        final ContentValues cv = new ContentValues();
+                        if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+                            // Restore the old mailbox id
+                            cv.put(MessageColumns.MAILBOX_KEY, req.sourceFolderId);
+                            mContentResolver.update(
+                                    ContentUris.withAppendedId(Message.CONTENT_URI, req.messageId),
+                                    cv, null, null);
+                        } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
+                            // Update with the new server id
+                            cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
+                            cv.put(Message.FLAGS, req.messageFlags | MESSAGE_FLAG_MOVED_MESSAGE);
+                            mContentResolver.update(
+                                    ContentUris.withAppendedId(Message.CONTENT_URI, req.messageId),
+                                    cv, null, null);
+                        }
+                        if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
+                                || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+                            // If we revert or succeed, we no longer need the update information
+                            // OR the now-duplicate email (the new copy will be synced down)
+                            mContentResolver.delete(ContentUris.withAppendedId(
+                                    Message.UPDATED_CONTENT_URI, req.messageId), null, null);
+                        } else {
+                            // In this case, we're retrying, so do nothing.  The request will be
+                            // handled next sync
+                        }
+                    }
+                } else if (EasResponse.isAuthError(status)) {
+                    throw new EasAuthenticationException();
+                } else {
+                    LogUtils.i(TAG, "Move items request failed, code: %d", status);
+                    throw new IOException();
+                }
+            } finally {
+                resp.close();
+            }
+        }
+    }
+
+    /**
+     * For each message in Message_Updates, add a sync command if appropriate, and add its id to
+     * our list of processed messages if appropriate.
+     * @param s The {@link Serializer} for this sync request.
+     * @param hasCommands Whether sync commands have already been written to s.
+     * @return Whether this function added any sync commands to s.
+     * @throws IOException
+     */
+    private boolean addUpdatedCommands(final Serializer s, final boolean hasCommands)
+            throws IOException {
+        // Find our trash mailbox, since deletions will have been moved there.
+        final long trashMailboxId =
+                Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
+        final Cursor c = mContentResolver.query(Message.UPDATED_CONTENT_URI, UPDATES_PROJECTION,
+                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+        boolean addedCommands = false;
+        if (c != null) {
+            try {
+                while (c.moveToNext()) {
+                    addedCommands |= handleOneUpdatedMessage(c, s, hasCommands || addedCommands,
+                            trashMailboxId);
+                }
+            } finally {
+                c.close();
+            }
+        }
+
+        // mMessagesToMove is now populated. If it's non-empty, let's send the move request now.
+        if (!mMessagesToMove.isEmpty()) {
+            performMessageMove();
+        }
+
+        return addedCommands;
+    }
+
+    /**
+     * Add FETCH commands for messages that need a body (i.e. we didn't find it during our earlier
+     * sync; this happens only in EAS 2.5 where the body couldn't be found after parsing the
+     * message's MIME data).
+     * @param s The {@link Serializer} for this sync request.
+     * @param hasCommands Whether sync commands have already been written to s.
+     * @return Whether this function added any sync commands to s.
+     * @throws IOException
+     */
+    private boolean addFetchCommands(final Serializer s, final boolean hasCommands)
+            throws IOException {
+        if (!hasCommands && !mMessagesToFetch.isEmpty()) {
+            s.start(Tags.SYNC_COMMANDS);
+        }
+        for (final String serverId : mMessagesToFetch) {
+            s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, serverId).end();
+        }
+
+        return !mMessagesToFetch.isEmpty();
+    }
+
     @Override
     protected void setUpsyncCommands(final Serializer s) throws IOException {
+        boolean addedCommands = addDeletedCommands(s, false);
+        addedCommands = addFetchCommands(s, addedCommands);
+        addedCommands = addUpdatedCommands(s, addedCommands);
+        if (addedCommands) {
+            s.end();
+        }
+    }
 
+    @Override
+    protected void cleanup(final int syncResult) {
+        // After a successful sync, we have things to delete from the DB.
+        if (syncResult != SYNC_RESULT_FAILED) {
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            // Delete any moved messages (since we've just synced the mailbox, and no longer need
+            // the placeholder message); this prevents duplicates from appearing in the mailbox.
+            // TODO: Verify this still makes sense.
+            ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
+                    .withSelection(WHERE_MAILBOX_KEY_AND_MOVED,
+                            new String[] {Long.toString(mMailbox.mId)}).build());
+            // Delete any entries in Message_Updates and Message_Deletes that were upsynced.
+            for (final long id: mDeletedMessages) {
+                ops.add(ContentProviderOperation.newDelete(
+                        ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
+            }
+            for (final long id: mUpdatedMessages) {
+                ops.add(ContentProviderOperation.newDelete(
+                        ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
+            }
+            try {
+                mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+            } catch (final RemoteException e) {
+                // TODO: Improve handling.
+            } catch (final OperationApplicationException e) {
+                // TODO: Improve handing.
+            }
+        }
+
+        if (syncResult == SYNC_RESULT_MORE_AVAILABLE) {
+            // Prepare our member variables for another sync request.
+            mDeletedMessages.clear();
+            mUpdatedMessages.clear();
+            mMessagesToFetch.clear();
+            mMessagesToMove.clear();
+        }
     }
 }
diff --git a/src/com/android/exchange/service/EasSyncHandler.java b/src/com/android/exchange/service/EasSyncHandler.java
index e595e10..149740f 100644
--- a/src/com/android/exchange/service/EasSyncHandler.java
+++ b/src/com/android/exchange/service/EasSyncHandler.java
@@ -27,12 +27,37 @@
 import java.io.InputStream;
 
 /**
- * Base class for performing a sync from an Exchange server (i.e. retrieving collection data from
- * the server). A single sync request from the app will result in one or more Sync POST messages to
- * the server, but that's all encapsulated in this class as a single "sync".
+ * Base class for syncing a single collection from an Exchange server. A "collection" is a single
+ * mailbox, or contacts for an account, or calendar for an account. (Tasks is part of the protocol
+ * but not implemented.)
+ * A single {@link ContentResolver#requestSync} for a single collection corresponds to a single
+ * object (of the appropriate subclass) being created and {@link #performSync} being called on it.
+ * This in turn will result in one or more Sync POST requests being sent to the Exchange server;
+ * from the client's point of view, these multiple Exchange Sync requests are all part of the same
+ * "sync" (i.e. the fact that there are multiple requests to the server is a detail of the Exchange
+ * protocol).
  * Different collection types (e.g. mail, contacts, calendar) should subclass this class and
  * implement the various abstract functions. The majority of how the sync flow is common to all,
  * aside from a few details and the {@link Parser} used.
+ * Details on how this class (and Exchange Sync) works:
+ * - Overview MSDN link: http://msdn.microsoft.com/en-us/library/ee159766(v=exchg.80).aspx
+ * - Sync MSDN link: http://msdn.microsoft.com/en-us/library/gg675638(v=exchg.80).aspx
+ * - The very first time, the client sends a Sync request with SyncKey = 0 and no other parameters.
+ *   This initial Sync request simply gets us a real SyncKey.
+ *   TODO: We should add the initial Sync to {@link EasAccountSyncHandler}.
+ * - Non-initial Sync requests can be for one or more collections; this implementation does one at
+ *   a time. TODO: allow sync for multiple collections to be aggregated?
+ * - For each collection, we send SyncKey, ServerId, other modifiers, Options, and Commands. The
+ *   protocol has a specific order in which these elements must appear in the request.
+ * - {@link #buildEasRequest} forms the XML for the request, using {@link #setInitialSyncOptions},
+ *   {@link #setNonInitialSyncOptions}, and {@link #setUpsyncCommands} to fill in the details
+ *   specific for each collection type.
+ * - The Sync response may specify that there's more data available on the server, in which case
+ *   we keep sending Sync requests to get that data.
+ * - The ordering constraints and other details may require subclasses to have member variables to
+ *   store state between the various calls while performing a single Sync request. These may need
+ *   to be reset between Sync requests to the Exchange server. Additionally, there are possibly
+ *   other necessary cleanups after parsing a Sync response. These are handled in {@link #cleanup}.
  */
 public abstract class EasSyncHandler extends EasServerConnection {
     private static final String TAG = "EasSyncHandler";
@@ -41,9 +66,9 @@
     protected static final String PIM_WINDOW_SIZE = "4";
 
     // TODO: For each type of failure, provide info about why.
-    private static final int SYNC_RESULT_FAILED = -1;
-    private static final int SYNC_RESULT_DONE = 0;
-    private static final int SYNC_RESULT_MORE_AVAILABLE = 1;
+    protected static final int SYNC_RESULT_FAILED = -1;
+    protected static final int SYNC_RESULT_DONE = 0;
+    protected static final int SYNC_RESULT_MORE_AVAILABLE = 1;
 
     /** Maximum number of Sync requests we'll send to the Exchange server in one sync attempt. */
     private static final int MAX_LOOPING_COUNT = 100;
@@ -132,23 +157,36 @@
     protected abstract AbstractSyncParser getParser(final InputStream is) throws IOException;
 
     /**
-     * Add sync options to the {@link Serializer} for this sync, if it's the first sync on this
-     * mailbox.
+     * Add to the {@link Serializer} for this sync the child elements of a Collection needed for an
+     * initial sync for this collection.
      * @param s The {@link Serializer} for this sync.
      * @throws IOException
      */
     protected abstract void setInitialSyncOptions(final Serializer s) throws IOException;
 
     /**
-     * Add sync options to the {@link Serializer} for this sync, if it's not the first sync on this
-     * mailbox.
+     * Add to the {@link Serializer} for this sync the child elements of a Collection needed for a
+     * non-initial sync for this collection, OTHER THAN Commands (which are written by
+     * {@link #setUpsyncCommands}.
      * @param s The {@link Serializer} for this sync.
      * @throws IOException
      */
     protected abstract void setNonInitialSyncOptions(final Serializer s) throws IOException;
 
+    /**
+     * Add all Commands to the {@link Serializer} for this Sync request. Strictly speaking, it's
+     * not all Upsync requests since Fetch is also a command, but largely that's what this section
+     * is used for.
+     * @param s The {@link Serializer} for this sync.
+     * @throws IOException
+     */
     protected abstract void setUpsyncCommands(final Serializer s) throws IOException;
 
+    /**
+     * Perform any necessary cleanup after processing a Sync response.
+     */
+    protected abstract void cleanup(final int syncResult);
+
     // End of abstract functions.
 
     /**
@@ -207,7 +245,6 @@
             setInitialSyncOptions(s);
         } else {
             setNonInitialSyncOptions(s);
-            // TODO: handle when previous iteration's upsync failed.
             setUpsyncCommands(s);
         }
         s.end().end().end().done();
@@ -223,7 +260,9 @@
      */
     private int parse(final EasResponse resp) {
         try {
-            if (getParser(resp.getInputStream()).parse()) {
+            final AbstractSyncParser parser = getParser(resp.getInputStream());
+            final boolean moreAvailable = parser.parse();
+            if (moreAvailable) {
                 return SYNC_RESULT_MORE_AVAILABLE;
             }
         } catch (final Parser.EmptyStreamException e) {
@@ -280,9 +319,7 @@
             resp.close();
         }
 
-        if (result == SYNC_RESULT_DONE) {
-            // TODO: target.cleanup() or equivalent
-        }
+        cleanup(result);
 
         if (initialSync && result != SYNC_RESULT_FAILED) {
             // TODO: Handle Automatic Lookback