Add MoveItems operation.

Change-Id: I39d7aeb2de4a01ec9237f574b752b57662829eb4
diff --git a/Android.mk b/Android.mk
index 0551fe8..2cbb478 100644
--- a/Android.mk
+++ b/Android.mk
@@ -34,6 +34,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon com.android.emailsync
 LOCAL_STATIC_JAVA_LIBRARIES += calendar-common
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
 
 LOCAL_PACKAGE_NAME := Exchange2
 LOCAL_OVERRIDES_PACKAGES := Exchange
diff --git a/src/com/android/exchange/adapter/MoveItemsParser.java b/src/com/android/exchange/adapter/MoveItemsParser.java
index a654c76..be11727 100644
--- a/src/com/android/exchange/adapter/MoveItemsParser.java
+++ b/src/com/android/exchange/adapter/MoveItemsParser.java
@@ -27,6 +27,7 @@
     private static final String TAG = "MoveItemsParser";
     private int mStatusCode = 0;
     private String mNewServerId;
+    private String mSourceServerId;
 
     // These are the EAS status codes for MoveItems
     private static final int STATUS_NO_SOURCE_FOLDER = 1;
@@ -54,6 +55,10 @@
         return mNewServerId;
     }
 
+    public String getSourceServerId() {
+        return mSourceServerId;
+    }
+
     public void parseResponse() throws IOException {
         while (nextTag(Tags.MOVE_RESPONSE) != END) {
             if (tag == Tags.MOVE_STATUS) {
@@ -87,6 +92,9 @@
             } else if (tag == Tags.MOVE_DSTMSGID) {
                 mNewServerId = getValue();
                 LogUtils.i(TAG, "Moved message id is now: %s", mNewServerId);
+            } else if (tag == Tags.MOVE_SRCMSGID) {
+                mSourceServerId = getValue();
+                LogUtils.i(TAG, "Source message id is: %s", mNewServerId);
             } else {
                 skipTag();
             }
diff --git a/src/com/android/exchange/eas/EasMoveItems.java b/src/com/android/exchange/eas/EasMoveItems.java
new file mode 100644
index 0000000..a9c5c3f
--- /dev/null
+++ b/src/com/android/exchange/eas/EasMoveItems.java
@@ -0,0 +1,143 @@
+package com.android.exchange.eas;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SyncResult;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.MessageMove;
+import com.android.exchange.EasResponse;
+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.HttpEntity;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Performs a MoveItems request, which is used to move items between collections.
+ * See http://msdn.microsoft.com/en-us/library/ee160102(v=exchg.80).aspx for more details.
+ * TODO: Investigate how this interacts with ItemOperations.
+ */
+public class EasMoveItems extends EasOperation {
+
+    /** Result code indicating that no moved messages were found for this account. */
+    public final static int RESULT_NO_MESSAGES = 0;
+    public final static int RESULT_OK = 1;
+
+    private static class MoveResponse {
+        public final String sourceMessageId;
+        public final String newMessageId;
+        public final int moveStatus;
+
+        public MoveResponse(final String srcMsgId, final String dstMsgId, final int status) {
+            sourceMessageId = srcMsgId;
+            newMessageId = dstMsgId;
+            moveStatus = status;
+        }
+    }
+
+    private MessageMove mMove;
+    private MoveResponse mResponse;
+
+    public EasMoveItems(final Context context, final Account account) {
+        super(context, account);
+    }
+
+    // TODO: Allow multiple messages in one request. Requires parser changes.
+    public int upsyncMovedMessages(final SyncResult syncResult) {
+        final List<MessageMove> moves = MessageMove.getMoves(mContext, mAccountId);
+        if (moves == null) {
+            return RESULT_NO_MESSAGES;
+        }
+
+        final long[][] messageIds = new long[3][moves.size()];
+        final int[] counts = new int[3];
+
+        for (final MessageMove move : moves) {
+            mMove = move;
+            final int result = performOperation(syncResult);
+            final int status;
+            if (result == RESULT_OK) {
+                processResponse(mMove, mResponse);
+                status = mResponse.moveStatus;
+            } else {
+                // TODO: Perhaps not all errors should be retried?
+                status = MoveItemsParser.STATUS_CODE_RETRY;
+            }
+            final int index = status - 1;
+            messageIds[index][counts[index]] = mMove.getMessageId();
+            ++counts[index];
+        }
+
+        final ContentResolver cr = mContext.getContentResolver();
+        MessageMove.upsyncSuccessful(cr, messageIds[0], counts[0]);
+        MessageMove.upsyncFail(cr, messageIds[1], counts[1]);
+        MessageMove.upsyncRetry(cr, messageIds[2], counts[2]);
+
+        return RESULT_OK;
+    }
+
+    @Override
+    protected String getCommand() {
+        return "MoveItems";
+    }
+
+    @Override
+    protected HttpEntity getRequestEntity() throws IOException {
+        final Serializer s = new Serializer();
+        s.start(Tags.MOVE_MOVE_ITEMS);
+        s.start(Tags.MOVE_MOVE);
+        s.data(Tags.MOVE_SRCMSGID, mMove.getServerId());
+        s.data(Tags.MOVE_SRCFLDID, mMove.getSourceFolderId());
+        s.data(Tags.MOVE_DSTFLDID, mMove.getDestFolderId());
+        s.end();
+        s.end().done();
+        return makeEntity(s);
+    }
+
+    @Override
+    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
+            throws IOException {
+        if (!response.isEmpty()) {
+            final MoveItemsParser parser = new MoveItemsParser(response.getInputStream());
+            parser.parse();
+            final String sourceMessageId = parser.getSourceServerId();
+            final String newMessageId = parser.getNewServerId();
+            final int status = parser.getStatusCode();
+            mResponse = new MoveResponse(sourceMessageId, newMessageId, status);
+        }
+        return RESULT_OK;
+    }
+
+    private void processResponse(final MessageMove request, final MoveResponse response) {
+        // TODO: Eventually this should use a transaction.
+        // TODO: Improve how the parser reports statuses and how we handle them here.
+        if (!response.sourceMessageId.equals(request.getServerId())) {
+            // TODO: This is bad, but I think we need to respect the response anyway.
+            LogUtils.e(LOG_TAG, "Got a response for a message we didn't request");
+        }
+
+        final ContentValues cv = new ContentValues(1);
+        if (response.moveStatus == MoveItemsParser.STATUS_CODE_REVERT) {
+            // Restore the old mailbox id
+            cv.put(EmailContent.MessageColumns.MAILBOX_KEY, request.getSourceFolderKey());
+        } else if (response.moveStatus == MoveItemsParser.STATUS_CODE_SUCCESS) {
+            if (response.newMessageId != null
+                    && !response.newMessageId.equals(response.sourceMessageId)) {
+                cv.put(EmailContent.SyncColumns.SERVER_ID, response.newMessageId);
+            }
+        }
+        if (cv.size() != 0) {
+            mContext.getContentResolver().update(
+                    ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
+                            request.getMessageId()), cv, null, null);
+        }
+    }
+}
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index 1485e78..df311cd 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -95,10 +95,10 @@
 
     /**
      * The account id for this operation.
-     * Currently only used for handling provisioning errors. Ideally we should minimize the creep
-     * of how this gets used (i.e. don't let it get to the intertwined state of the past).
+     * NOTE: You will be tempted to add a reference to the {@link Account} here. Resist.
+     * It's too easy for that to lead to creep and stale data.
      */
-    private final long mAccountId;
+    protected final long mAccountId;
     private final EasServerConnection mConnection;
 
     private EasOperation(final Context context, final long accountId,
@@ -190,7 +190,7 @@
                 return RESULT_REQUEST_FAILURE;
             } catch (final IllegalStateException e) {
                 // Subclasses use ISE to signal a hard error when building the request.
-                // TODO: If executeHttpUriRequest can throw an ISE, we may want to tidy this up.
+                // TODO: Switch away from ISEs.
                 LogUtils.e(LOG_TAG, e, "Exception while sending request");
                 if (syncResult != null) {
                     syncResult.databaseError = true;
diff --git a/src/com/android/exchange/eas/EasSync.java b/src/com/android/exchange/eas/EasSync.java
index 34dee03..03c614b 100644
--- a/src/com/android/exchange/eas/EasSync.java
+++ b/src/com/android/exchange/eas/EasSync.java
@@ -56,7 +56,12 @@
 
     @Override
     protected HttpEntity getRequestEntity() throws IOException {
-        return null;
+        final Serializer s = new Serializer();
+        s.start(Tags.SYNC_SYNC);
+        s.start(Tags.SYNC_COLLECTIONS);
+        addOneCollectionToRequest(s, mMailbox);
+        s.end().end().done();
+        return makeEntity(s);
     }
 
 
@@ -80,6 +85,8 @@
         if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
             s.data(Tags.SYNC_CLASS, Eas.getFolderClass(mailbox.mType));
         }
+        s.data(Tags.SYNC_SYNC_KEY, getSyncKeyForMailbox(mailbox));
+        s.data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
         s.end();
     }
 
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index b2a5055..b60cd52 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -43,6 +43,7 @@
 import com.android.exchange.adapter.PingParser;
 import com.android.exchange.adapter.Search;
 import com.android.exchange.eas.EasFolderSync;
+import com.android.exchange.eas.EasMoveItems;
 import com.android.exchange.eas.EasOperation;
 import com.android.exchange.eas.EasPing;
 import com.android.mail.providers.UIProvider.AccountCapabilities;
@@ -528,6 +529,9 @@
             // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
             mSyncHandlerMap.startSync(account.mId);
 
+            EasMoveItems move = new EasMoveItems(context, account);
+            move.upsyncMovedMessages(syncResult);
+
             // TODO: Should we refresh the account here? It may have changed while waiting for any
             // pings to stop. It may not matter since the things that may have been twiddled might
             // not affect syncing.