Fix regular sync, part 1.

- Add support for new IEmailService#sync.
- Don't pass SyncResult through EasOperations. This was
  probably a bad idea to begin with. Instead, callers that
  care should use performOperation()'s return value to set
  the results appropriately.
- Fix upsync operations to signal request/network errors
  ASAP (and mark any unperformed upsyncs as retries).
- In ESAS, bail early on first error.
- In ESAS, set up forwarding to EasService for pushModify.
- Converting mail downsync into an EasOperation. For now,
  not merging it with the EasSync class that already exists.

Change-Id: I4719c5cafbd01a957f267b221cf3276010154176
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
index eef2ce6..d0b9a6c 100644
--- a/src/com/android/exchange/ExchangeService.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -177,12 +177,14 @@
         }
 
         @Override
-        public void sendMail(long accountId) throws RemoteException {
-        }
+        public void sendMail(long accountId) throws RemoteException {}
 
         @Override
-        public void pushModify(long accountId) throws RemoteException {
-        }
+        public void pushModify(long accountId) throws RemoteException {}
+
+        @Override
+        public void sync(final long accountId, final boolean updateFolderList,
+                final int mailboxType, final long[] folders) {}
     };
 
     /**
diff --git a/src/com/android/exchange/eas/EasFolderSync.java b/src/com/android/exchange/eas/EasFolderSync.java
index 48d4070..34da843 100644
--- a/src/com/android/exchange/eas/EasFolderSync.java
+++ b/src/com/android/exchange/eas/EasFolderSync.java
@@ -17,7 +17,6 @@
 package com.android.exchange.eas;
 
 import android.content.Context;
-import android.content.SyncResult;
 import android.os.Bundle;
 
 import com.android.emailcommon.mail.MessagingException;
@@ -99,12 +98,12 @@
     }
 
     @Override
-    public int performOperation(final SyncResult syncResult) {
+    public int performOperation() {
         if (mStatusOnly) {
             return validate();
         } else {
             LogUtils.d(LOG_TAG, "Performing FolderSync for account %d", getAccountId());
-            return super.performOperation(syncResult);
+            return super.performOperation();
         }
     }
 
@@ -119,10 +118,9 @@
     /**
      * Perform a folder sync.
      * TODO: Remove this function when transition to EasService is complete.
-     * @param syncResult The {@link SyncResult} object for this sync operation.
      * @return A result code, either from above or from the base class.
      */
-    public int doFolderSync(final SyncResult syncResult) {
+    public int doFolderSync() {
         if (mStatusOnly) {
             return RESULT_WRONG_OPERATION;
         }
@@ -130,7 +128,7 @@
         // This intentionally calls super.performOperation -- calling our performOperation
         // will simply end up calling super.performOperation anyway. This is part of the transition
         // to EasService and will go away when this function is deleted.
-        return super.performOperation(syncResult);
+        return super.performOperation();
     }
 
     /**
@@ -155,7 +153,7 @@
 
         if (shouldGetProtocolVersion()) {
             final EasOptions options = new EasOptions(this);
-            final int result = options.getProtocolVersionFromServer(null);
+            final int result = options.getProtocolVersionFromServer();
             if (result != EasOptions.RESULT_OK) {
                 writeResultCode(mValidationResult, result);
                 return result;
@@ -168,7 +166,7 @@
 
         // This is intentionally a call to super.performOperation. This is a helper function for
         // our version of perfomOperation so calling that function would infinite loop.
-        final int result = super.performOperation(null);
+        final int result = super.performOperation();
         writeResultCode(mValidationResult, result);
         return result;
     }
@@ -198,7 +196,7 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
+    protected int handleResponse(final EasResponse response)
             throws IOException, CommandStatusException {
         if (!response.isEmpty()) {
             new FolderSyncParser(mContext, mContext.getContentResolver(),
@@ -213,7 +211,7 @@
     }
 
     @Override
-    protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
+    protected boolean handleProvisionError() {
         if (mStatusOnly) {
             final EasProvision provisionOperation = new EasProvision(this);
             mPolicy = provisionOperation.test();
@@ -221,7 +219,7 @@
             // no need to re-run the operation.
             return false;
         }
-        return super.handleProvisionError(syncResult, accountId);
+        return super.handleProvisionError();
     }
 
     /**
diff --git a/src/com/android/exchange/eas/EasLoadAttachment.java b/src/com/android/exchange/eas/EasLoadAttachment.java
index fe51a96..260d3f8 100644
--- a/src/com/android/exchange/eas/EasLoadAttachment.java
+++ b/src/com/android/exchange/eas/EasLoadAttachment.java
@@ -17,7 +17,6 @@
 package com.android.exchange.eas;
 
 import android.content.Context;
-import android.content.SyncResult;
 import android.os.RemoteException;
 
 import com.android.emailcommon.provider.EmailContent;
@@ -25,7 +24,6 @@
 import com.android.emailcommon.service.EmailServiceStatus;
 import com.android.emailcommon.service.IEmailServiceCallback;
 import com.android.emailcommon.utility.AttachmentUtilities;
-import com.android.exchange.CommandStatusException;
 import com.android.exchange.Eas;
 import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.ItemOperationsParser;
@@ -147,11 +145,10 @@
 
     /**
      * Finish encoding attachment names for Exchange 2003.
-     * @param syncResult The {@link SyncResult} that stores the result of the operation.
      * @return A {@link EmailServiceStatus} code that indicates the result of the operation.
      */
     @Override
-    public int performOperation(final SyncResult syncResult) {
+    public int performOperation() {
         mAttachment = EmailContent.Attachment.restoreAttachmentWithId(mContext, mAttachmentId);
         if (mAttachment == null) {
             LogUtils.e(LOG_TAG, "Could not load attachment %d", mAttachmentId);
@@ -178,7 +175,7 @@
         doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachment.mId,
                 EmailServiceStatus.IN_PROGRESS, 0);
 
-        final int return_value = super.performOperation(syncResult);
+        final int return_value = super.performOperation();
 
         // Last callback to report results.  Note that we are using the status member variable
         // to keep track of the status to be returned as super.performOperation() is not designed
@@ -270,12 +267,10 @@
     /**
      * Read the {@link EasResponse} and extract the attachment data, saving it to the provider.
      * @param response The (successful) {@link EasResponse} containing the attachment data.
-     * @param syncResult The {@link SyncResult} that stores the result of the operation.
      * @return A status code, from {@link EmailServiceStatus}, for this load.
      */
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException, CommandStatusException {
+    protected int handleResponse(final EasResponse response) {
         // Some very basic error checking on the response object first.
         // Our base class should be responsible for checking these errors but if the error
         // checking is done in the override functions, we can be more specific about
diff --git a/src/com/android/exchange/eas/EasMoveItems.java b/src/com/android/exchange/eas/EasMoveItems.java
index ed2ecd7..a57b95d 100644
--- a/src/com/android/exchange/eas/EasMoveItems.java
+++ b/src/com/android/exchange/eas/EasMoveItems.java
@@ -4,7 +4,6 @@
 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;
@@ -52,7 +51,7 @@
     }
 
     // TODO: Allow multiple messages in one request. Requires parser changes.
-    public int upsyncMovedMessages(final SyncResult syncResult) {
+    public int upsyncMovedMessages() {
         final List<MessageMove> moves = MessageMove.getMoves(mContext, getAccountId());
         if (moves == null) {
             return RESULT_NO_MESSAGES;
@@ -60,19 +59,31 @@
 
         final long[][] messageIds = new long[3][moves.size()];
         final int[] counts = new int[3];
+        int result = RESULT_NO_MESSAGES;
 
         for (final MessageMove move : moves) {
             mMove = move;
-            final int result = performOperation(syncResult);
+            if (result >= 0) {
+                // If our previous time through the loop succeeded, keep making server requests.
+                // Otherwise, we carry through the loop for all messages with the last error
+                // response, which will stop trying this iteration and force the rest of the
+                // messages into the retry state.
+                result = performOperation();
+            }
             final int status;
-            if (result == RESULT_OK) {
-                processResponse(mMove, mResponse);
-                status = mResponse.moveStatus;
+            if (result >= 0) {
+                if (result == RESULT_OK) {
+                    processResponse(mMove, mResponse);
+                    status = mResponse.moveStatus;
+                } else {
+                    // TODO: Should this really be a retry?
+                    // We got a 200 response with an empty payload. It's not clear we ought to
+                    // retry, but this is how our implementation has worked in the past.
+                    status = MoveItemsParser.STATUS_CODE_RETRY;
+                }
             } else {
-                // TODO: Perhaps not all errors should be retried?
-                // Notably, if the server returns 200 with an empty response, we retry. This is
-                // how the previous version worked, and I can't find documentation about what this
-                // response state really means.
+                // performOperation returned a negative status code, indicating a failure before the
+                // server actually was able to tell us yea or nay, so we must retry.
                 status = MoveItemsParser.STATUS_CODE_RETRY;
             }
             final int index;
@@ -91,7 +102,10 @@
         MessageMove.upsyncFail(cr, messageIds[1], counts[1]);
         MessageMove.upsyncRetry(cr, messageIds[2], counts[2]);
 
-        return RESULT_OK;
+        if (result >= 0) {
+            return RESULT_OK;
+        }
+        return result;
     }
 
     @Override
@@ -113,8 +127,7 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException {
+    protected int handleResponse(final EasResponse response) throws IOException {
         if (!response.isEmpty()) {
             final MoveItemsParser parser = new MoveItemsParser(response.getInputStream());
             parser.parse();
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index 56a21ce..07fa1f9 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -132,8 +132,10 @@
     public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9;
     /** Error code indicating the account could not be loaded from the provider. */
     public static final int RESULT_ACCOUNT_ID_INVALID = -10;
+    /** Error code indicating a hard data layer error. */
+    public static final int RESULT_HARD_DATA_FAILURE = -11;
     /** Error code indicating some other failure. */
-    public static final int RESULT_OTHER_FAILURE = -11;
+    public static final int RESULT_OTHER_FAILURE = -12;
 
     protected final Context mContext;
 
@@ -254,11 +256,9 @@
      * negative result code, which will be handled the same as if it had been indicated in the HTTP
      * response code.
      *
-     * @param syncResult If this operation is a sync, the {@link SyncResult} object that should
-     *                   be written to for this sync; otherwise null.
      * @return A result code for the outcome of this operation, as described above.
      */
-    public int performOperation(final SyncResult syncResult) {
+    public int performOperation() {
         // Make sure the account is loaded if it hasn't already been.
         if (!loadAccount(false)) {
             LogUtils.i(LOG_TAG, "Failed to load account %d before sending request for operation %s",
@@ -290,26 +290,16 @@
                     message = "(no message)";
                 }
                 LogUtils.i(LOG_TAG, "IOException while sending request: %s", message);
-                if (syncResult != null) {
-                    ++syncResult.stats.numIoExceptions;
-                }
                 return RESULT_REQUEST_FAILURE;
             } catch (final CertificateException e) {
                 LogUtils.i(LOG_TAG, "CertificateException while sending request: %s",
                         e.getMessage());
-                if (syncResult != null) {
-                    // TODO: Is this the best stat to increment?
-                    ++syncResult.stats.numAuthExceptions;
-                }
                 return RESULT_CLIENT_CERTIFICATE_REQUIRED;
             } catch (final IllegalStateException e) {
                 // Subclasses use ISE to signal a hard error when building the request.
                 // TODO: Switch away from ISEs.
                 LogUtils.e(LOG_TAG, e, "Exception while sending request");
-                if (syncResult != null) {
-                    syncResult.databaseError = true;
-                }
-                return RESULT_OTHER_FAILURE;
+                return RESULT_HARD_DATA_FAILURE;
             }
 
             // The POST completed, so process the response.
@@ -319,12 +309,9 @@
                 if (response.isSuccess()) {
                     int responseResult;
                     try {
-                        responseResult = handleResponse(response, syncResult);
+                        responseResult = handleResponse(response);
                     } catch (final IOException e) {
                         LogUtils.e(LOG_TAG, e, "Exception while handling response");
-                        if (syncResult != null) {
-                            ++syncResult.stats.numIoExceptions;
-                        }
                         return RESULT_REQUEST_FAILURE;
                     } catch (final CommandStatusException e) {
                         // For some operations (notably Sync & FolderSync), errors are signaled in
@@ -356,36 +343,24 @@
                 // If this operation has distinct handling for 403 errors, do that.
                 if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) {
                     LogUtils.e(LOG_TAG, "Forbidden response");
-                    if (syncResult != null) {
-                        // TODO: Is this the best stat to increment?
-                        ++syncResult.stats.numAuthExceptions;
-                    }
                     return RESULT_FORBIDDEN;
                 }
 
                 // Handle provisioning errors.
                 if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) {
-                    if (handleProvisionError(syncResult, getAccountId())) {
+                    if (handleProvisionError()) {
                         // The provisioning error has been taken care of, so we should re-do this
                         // request.
                         LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying",
                                 getCommand());
                         continue;
                     }
-                    if (syncResult != null) {
-                        LogUtils.e(LOG_TAG, "Issue with provisioning");
-                        // TODO: Is this the best stat to increment?
-                        ++syncResult.stats.numAuthExceptions;
-                    }
                     return RESULT_PROVISIONING_ERROR;
                 }
 
                 // Handle authentication errors.
                 if (response.isAuthError()) {
                     LogUtils.e(LOG_TAG, "Authentication error");
-                    if (syncResult != null) {
-                        ++syncResult.stats.numAuthExceptions;
-                    }
                     if (response.isMissingCertificate()) {
                         return RESULT_CLIENT_CERTIFICATE_REQUIRED;
                     }
@@ -401,10 +376,6 @@
                     // All other errors.
                     LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d",
                             getCommand(), response.getStatus(), result);
-                    if (syncResult != null) {
-                        // TODO: Is this the best stat to increment?
-                        ++syncResult.stats.numIoExceptions;
-                    }
                     return RESULT_OTHER_FAILURE;
                 }
             } finally {
@@ -415,9 +386,6 @@
         // Non-redirects return immediately after handling, so the only way to reach here is if we
         // looped too many times.
         LogUtils.e(LOG_TAG, "Too many redirects");
-        if (syncResult != null) {
-           syncResult.tooManyRetries = true;
-        }
         return RESULT_TOO_MANY_REDIRECTS;
     }
 
@@ -487,15 +455,13 @@
     /**
      * Parse the response from the Exchange perform whatever actions are dictated by that.
      * @param response The {@link EasResponse} to our request.
-     * @param syncResult The {@link SyncResult} object for this operation, or null if we're not
-     *                   handling a sync.
      * @return A result code. Non-negative values are returned directly to the caller; negative
      *         values
      *
      * that is returned to the caller of {@link #performOperation}.
      * @throws IOException
      */
-    protected abstract int handleResponse(final EasResponse response, final SyncResult syncResult)
+    protected abstract int handleResponse(final EasResponse response)
             throws IOException, CommandStatusException;
 
     /**
@@ -545,13 +511,11 @@
     /**
      * Handle a provisioning error. Subclasses may override this to do something different, e.g.
      * to validate rather than actually do the provisioning.
-     * @param syncResult
-     * @param accountId
      * @return
      */
-    protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
+    protected boolean handleProvisionError() {
         final EasProvision provisionOperation = new EasProvision(this);
-        return provisionOperation.provision(syncResult, accountId);
+        return provisionOperation.provision();
     }
 
     /**
@@ -734,4 +698,32 @@
         LogUtils.d(LOG_TAG, "requestSync EasOperation requestNoOpSync %s, %s",
                 amAccount.toString(), extras.toString());
     }
+
+    public static void writeResultToSyncResult(final int result, final SyncResult syncResult) {
+        switch (result) {
+            case RESULT_TOO_MANY_REDIRECTS:
+                syncResult.tooManyRetries = true;
+                break;
+            case RESULT_REQUEST_FAILURE:
+                syncResult.stats.numIoExceptions = 1;
+                break;
+            case RESULT_FORBIDDEN:
+            case RESULT_PROVISIONING_ERROR:
+            case RESULT_AUTHENTICATION_ERROR:
+            case RESULT_CLIENT_CERTIFICATE_REQUIRED:
+                syncResult.stats.numAuthExceptions = 1;
+                break;
+            case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
+                // Only used in validate, so there's never a syncResult to write to here.
+                break;
+            case RESULT_ACCOUNT_ID_INVALID:
+            case RESULT_HARD_DATA_FAILURE:
+                syncResult.databaseError = true;
+                break;
+            case RESULT_OTHER_FAILURE:
+                // TODO: Is this correct?
+                syncResult.stats.numIoExceptions = 1;
+                break;
+        }
+    }
 }
diff --git a/src/com/android/exchange/eas/EasOptions.java b/src/com/android/exchange/eas/EasOptions.java
index 32d2c75..131c391 100644
--- a/src/com/android/exchange/eas/EasOptions.java
+++ b/src/com/android/exchange/eas/EasOptions.java
@@ -16,8 +16,6 @@
 
 package com.android.exchange.eas;
 
-import android.content.SyncResult;
-
 import com.android.exchange.Eas;
 import com.android.exchange.EasResponse;
 import com.android.mail.utils.LogUtils;
@@ -53,11 +51,10 @@
     /**
      * Perform the server request. If successful, callers should use
      * {@link #getProtocolVersionString} to get the actual protocol version value.
-     * @param syncResult The {@link SyncResult} to use for this operation.
      * @return A result code; {@link #RESULT_OK} is the only value that indicates success.
      */
-    public int getProtocolVersionFromServer(final SyncResult syncResult) {
-        return performOperation(syncResult);
+    public int getProtocolVersionFromServer() {
+        return performOperation();
     }
 
     /**
@@ -82,7 +79,7 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult) {
+    protected int handleResponse(final EasResponse response) {
         final Header commands = response.getHeader("MS-ASProtocolCommands");
         final Header versions = response.getHeader("ms-asprotocolversions");
         final boolean hasProtocolVersion;
diff --git a/src/com/android/exchange/eas/EasPing.java b/src/com/android/exchange/eas/EasPing.java
index d8e28a4..be1a679 100644
--- a/src/com/android/exchange/eas/EasPing.java
+++ b/src/com/android/exchange/eas/EasPing.java
@@ -19,7 +19,6 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.SyncResult;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.SystemClock;
@@ -102,7 +101,7 @@
 
     public final int doPing() {
         final long startTime = SystemClock.elapsedRealtime();
-        final int result = performOperation(null);
+        final int result = performOperation();
         if (result == RESULT_RESTART) {
             return PingParser.STATUS_EXPIRED;
         } else  if (result == RESULT_REQUEST_FAILURE) {
@@ -176,8 +175,7 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException {
+    protected int handleResponse(final EasResponse response) throws IOException {
         if (response.isEmpty()) {
             // TODO this should probably not be an IOException, maybe something more descriptive?
             throw new IOException("Empty ping response");
diff --git a/src/com/android/exchange/eas/EasProvision.java b/src/com/android/exchange/eas/EasProvision.java
index 7b5bcb9..4ea003d 100644
--- a/src/com/android/exchange/eas/EasProvision.java
+++ b/src/com/android/exchange/eas/EasProvision.java
@@ -17,12 +17,8 @@
 package com.android.exchange.eas;
 
 import android.content.Context;
-import android.content.SyncResult;
-import android.os.Bundle;
-import android.telephony.TelephonyManager;
 
 import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.Policy;
 import com.android.emailcommon.service.PolicyServiceProxy;
 import com.android.exchange.Eas;
@@ -113,20 +109,20 @@
         mPhase = 0;
     }
 
-    private int performInitialRequest(final SyncResult syncResult) {
+    private int performInitialRequest() {
         mPhase = PHASE_INITIAL;
-        return performOperation(syncResult);
+        return performOperation();
     }
 
-    private void performAckRequestForWipe(final SyncResult syncResult) {
+    private void performAckRequestForWipe() {
         mPhase = PHASE_WIPE;
-        performOperation(syncResult);
+        performOperation();
     }
 
-    private int performAckRequest(final SyncResult syncResult, final boolean isPartial) {
+    private int performAckRequest(final boolean isPartial) {
         mPhase = PHASE_ACKNOWLEDGE;
         mStatus = isPartial ? PROVISION_STATUS_PARTIAL : PROVISION_STATUS_OK;
-        return performOperation(syncResult);
+        return performOperation();
     }
 
     /**
@@ -134,10 +130,10 @@
      * @return The {@link Policy} if we support it, or null otherwise.
      */
     public final Policy test() {
-        int result = performInitialRequest(null);
+        int result = performInitialRequest();
         if (result == RESULT_POLICY_UNSUPPORTED) {
             // Check if the server will permit partial policies.
-            result = performAckRequest(null, true);
+            result = performAckRequest(true);
         }
         if (result == RESULT_POLICY_SUPPORTED) {
             // The server is ok with us not supporting everything, so clear the unsupported ones.
@@ -149,19 +145,18 @@
 
     /**
      * Get the required policy from the server and enforce it.
-     * @param syncResult The {@link SyncResult}, if anym for this operation.
-     * @param accountId The id for the account for this request.
      * @return Whether we succeeded in provisioning this account.
      */
-    public final boolean provision(final SyncResult syncResult, final long accountId) {
-        final int result = performInitialRequest(syncResult);
+    public final boolean provision() {
+        final int result = performInitialRequest();
+        final long accountId = getAccountId();
 
         if (result < 0) {
             return false;
         }
 
         if (result == RESULT_REMOTE_WIPE) {
-            performAckRequestForWipe(syncResult);
+            performAckRequestForWipe();
             LogUtils.i(LOG_TAG, "Executing remote wipe");
             PolicyServiceProxy.remoteWipe(mContext);
             return false;
@@ -175,8 +170,7 @@
         }
 
         // Acknowledge to the server and make sure all's well.
-        if (performAckRequest(syncResult, result == RESULT_POLICY_UNSUPPORTED) ==
-                RESULT_POLICY_UNSUPPORTED) {
+        if (performAckRequest(result == RESULT_POLICY_UNSUPPORTED) == RESULT_POLICY_UNSUPPORTED) {
             return false;
         }
 
@@ -190,7 +184,7 @@
         if (version == Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE
                 || version == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
             final EasSettings settingsOperation = new EasSettings(this);
-            if (!settingsOperation.sendDeviceInformation(syncResult)) {
+            if (!settingsOperation.sendDeviceInformation()) {
                 // TODO: Do something more useful when the settings command fails.
                 // The consequence here is that the server will not have device info.
                 // However, this is NOT a provisioning failure.
@@ -266,8 +260,7 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException {
+    protected int handleResponse(final EasResponse response) throws IOException {
         final ProvisionParser pp = new ProvisionParser(mContext, response.getInputStream());
         // If this is the response for a remote wipe ack, it doesn't have anything useful in it.
         // Just go ahead and return now.
@@ -302,7 +295,7 @@
     }
 
     @Override
-    protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
+    protected boolean handleProvisionError() {
         // If we get a provisioning error while doing provisioning, we should not recurse.
         return false;
     }
diff --git a/src/com/android/exchange/eas/EasSettings.java b/src/com/android/exchange/eas/EasSettings.java
index a04fa14..41df356 100644
--- a/src/com/android/exchange/eas/EasSettings.java
+++ b/src/com/android/exchange/eas/EasSettings.java
@@ -16,8 +16,6 @@
 
 package com.android.exchange.eas;
 
-import android.content.SyncResult;
-
 import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.SettingsParser;
@@ -49,8 +47,8 @@
         super(parentOperation);
     }
 
-    public boolean sendDeviceInformation(final SyncResult syncResult) {
-        return performOperation(syncResult) == RESULT_OK;
+    public boolean sendDeviceInformation() {
+        return performOperation() == RESULT_OK;
     }
 
     @Override
@@ -68,8 +66,7 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException {
+    protected int handleResponse(final EasResponse response) throws IOException {
         return new SettingsParser(response.getInputStream()).parse()
                 ? RESULT_OK : RESULT_OTHER_FAILURE;
     }
diff --git a/src/com/android/exchange/eas/EasSync.java b/src/com/android/exchange/eas/EasSync.java
index 084f921..39f38be 100644
--- a/src/com/android/exchange/eas/EasSync.java
+++ b/src/com/android/exchange/eas/EasSync.java
@@ -19,7 +19,6 @@
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
-import android.content.SyncResult;
 import android.database.Cursor;
 import android.support.v4.util.LongSparseArray;
 import android.text.TextUtils;
@@ -55,6 +54,10 @@
  */
 public class EasSync extends EasOperation {
 
+    /** Result code indicating that the mailbox for an upsync is no longer present. */
+    public final static int RESULT_NO_MAILBOX = 0;
+    public final static int RESULT_OK = 1;
+
     // TODO: When we handle downsync, this will become relevant.
     private boolean mInitialSync;
 
@@ -100,10 +103,10 @@
     }
 
     /**
-     * TODO: return value doesn't do what it claims.
-     * @return Number of messages successfully synced, or -1 if we encountered an error.
+     * @return Number of messages successfully synced, or a negative response code from
+     *         {@link EasOperation} if we encountered any errors.
      */
-    public final int upsync(final SyncResult syncResult) {
+    public final int upsync() {
         final List<MessageStateChange> changes = MessageStateChange.getChanges(mContext,
                 getAccountId(), getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE);
         if (changes == null) {
@@ -117,43 +120,65 @@
 
         final long[][] messageIds = new long[2][changes.size()];
         final int[] counts = new int[2];
+        int result = 0;
 
         for (int i = 0; i < allData.size(); ++i) {
             mMailboxId = allData.keyAt(i);
             mStateChanges = allData.valueAt(i);
-            final Cursor mailboxCursor = mContext.getContentResolver().query(
-                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
-                    Mailbox.ProjectionSyncData.PROJECTION, null, null, null);
-            if (mailboxCursor != null) {
-                try {
-                    if (mailboxCursor.moveToFirst()) {
-                        mMailboxServerId = mailboxCursor.getString(
-                                Mailbox.ProjectionSyncData.COLUMN_SERVER_ID);
-                        mMailboxSyncKey = mailboxCursor.getString(
-                                Mailbox.ProjectionSyncData.COLUMN_SYNC_KEY);
-                        final int result;
-                        if (TextUtils.isEmpty(mMailboxSyncKey) || mMailboxSyncKey.equals("0")) {
-                            // For some reason we can get here without a valid mailbox sync key
-                            // b/10797675
-                            // TODO: figure out why and clean this up
-                            LogUtils.d(LOG_TAG,
-                                    "Tried to sync mailbox %d with invalid mailbox sync key",
-                                    mMailboxId);
-                            result = -1;
-                        } else {
-                            result = performOperation(syncResult);
-                        }
-                        if (result == 0) {
-                            handleMessageUpdateStatus(mMessageUpdateStatus, messageIds, counts);
-                        } else {
-                            for (final MessageStateChange msc : mStateChanges) {
-                                messageIds[1][counts[1]] = msc.getMessageId();
-                                ++counts[1];
+            boolean retryMailbox = true;
+            // If we've already encountered a fatal error, don't even try to upsync subsequent
+            // mailboxes.
+            if (result >= 0) {
+                final Cursor mailboxCursor = mContext.getContentResolver().query(
+                        ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
+                        Mailbox.ProjectionSyncData.PROJECTION, null, null, null);
+                if (mailboxCursor != null) {
+                    try {
+                        if (mailboxCursor.moveToFirst()) {
+                            mMailboxServerId = mailboxCursor.getString(
+                                    Mailbox.ProjectionSyncData.COLUMN_SERVER_ID);
+                            mMailboxSyncKey = mailboxCursor.getString(
+                                    Mailbox.ProjectionSyncData.COLUMN_SYNC_KEY);
+                            if (TextUtils.isEmpty(mMailboxSyncKey) || mMailboxSyncKey.equals("0")) {
+                                // For some reason we can get here without a valid mailbox sync key
+                                // b/10797675
+                                // TODO: figure out why and clean this up
+                                LogUtils.d(LOG_TAG,
+                                        "Tried to sync mailbox %d with invalid mailbox sync key",
+                                        mMailboxId);
+                            } else {
+                                result = performOperation();
+                                if (result >= 0) {
+                                    // Our request gave us back a legitimate answer; this is the
+                                    // only case in which we don't retry this mailbox.
+                                    retryMailbox = false;
+                                    if (result == RESULT_OK) {
+                                        handleMessageUpdateStatus(mMessageUpdateStatus, messageIds,
+                                                counts);
+                                    } else if (result == RESULT_NO_MAILBOX) {
+                                        // A retry here is pointless -- the message's mailbox (and
+                                        // therefore the message) is gone, so mark as success so
+                                        // that these entries get wiped from the change list.
+                                        for (final MessageStateChange msc : mStateChanges) {
+                                            messageIds[0][counts[0]] = msc.getMessageId();
+                                            ++counts[0];
+                                        }
+                                    } else {
+                                        LogUtils.wtf(LOG_TAG, "Unrecognized result code: %d",
+                                                result);
+                                    }
+                                }
                             }
                         }
+                    } finally {
+                        mailboxCursor.close();
                     }
-                } finally {
-                    mailboxCursor.close();
+                }
+            }
+            if (retryMailbox) {
+                for (final MessageStateChange msc : mStateChanges) {
+                    messageIds[1][counts[1]] = msc.getMessageId();
+                    ++counts[1];
                 }
             }
         }
@@ -162,7 +187,10 @@
         MessageStateChange.upsyncSuccessful(cr, messageIds[0], counts[0]);
         MessageStateChange.upsyncRetry(cr, messageIds[1], counts[1]);
 
-        return 0;
+        if (result < 0) {
+            return result;
+        }
+        return counts[0];
     }
 
     @Override
@@ -182,11 +210,11 @@
     }
 
     @Override
-    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
+    protected int handleResponse(final EasResponse response)
             throws IOException, CommandStatusException {
         final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
         if (mailbox == null) {
-            return RESULT_OTHER_FAILURE;
+            return RESULT_NO_MAILBOX;
         }
         final EmailSyncParser parser = new EmailSyncParser(mContext, mContext.getContentResolver(),
                 response.getInputStream(), mailbox, mAccount);
@@ -196,7 +224,7 @@
         } catch (final Parser.EmptyStreamException e) {
             // This indicates a compressed response which was empty, which is OK.
         }
-        return 0;
+        return RESULT_OK;
     }
 
     @Override
diff --git a/src/com/android/exchange/eas/EasSyncBase.java b/src/com/android/exchange/eas/EasSyncBase.java
new file mode 100644
index 0000000..90237dd
--- /dev/null
+++ b/src/com/android/exchange/eas/EasSyncBase.java
@@ -0,0 +1,138 @@
+package com.android.exchange.eas;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.exchange.adapter.AbstractSyncParser;
+import com.android.exchange.adapter.Parser;
+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;
+
+/**
+ * Performs an EAS downsync operation for one folder.
+ * TODO: Merge with {@link EasSync}, which currently handles upsync.
+ */
+public class EasSyncBase extends EasOperation {
+
+    private static final String TAG = Eas.LOG_TAG;
+
+    public static final int RESULT_DONE = 0;
+    public static final int RESULT_MORE_AVAILABLE = 1;
+
+    private final boolean mInitialSync;
+    private final Mailbox mMailbox;
+
+    private int mNumWindows;
+
+    // TODO: Convert to accountId when ready to convert to EasService.
+    public EasSyncBase(final Context context, final Account account, final Mailbox mailbox) {
+        super(context, account);
+        // TODO: This works for email, but not necessarily for other types.
+        mInitialSync = EmailContent.isInitialSyncKey(getSyncKey());
+        mMailbox = mailbox;
+    }
+
+    /**
+     * Get the sync key for this mailbox.
+     * @return The sync key for the object being synced. "0" means this is the first sync. If
+     *      there is an error in getting the sync key, this function returns null.
+     */
+    protected String getSyncKey() {
+        if (mMailbox == null) {
+            return null;
+        }
+        if (mMailbox.mSyncKey == null) {
+            mMailbox.mSyncKey = "0";
+        }
+        return mMailbox.mSyncKey;
+    }
+
+    @Override
+    protected String getCommand() {
+        return "Sync";
+    }
+
+    @Override
+    protected HttpEntity getRequestEntity() throws IOException {
+        final String className = Eas.getFolderClass(mMailbox.mType);
+        final String syncKey = getSyncKey();
+        LogUtils.d(TAG, "Syncing account %d mailbox %d (class %s) with syncKey %s", mAccount.mId,
+                mMailbox.mId, className, syncKey);
+
+        final Serializer s = new Serializer();
+        s.start(Tags.SYNC_SYNC);
+        s.start(Tags.SYNC_COLLECTIONS);
+        s.start(Tags.SYNC_COLLECTION);
+        // The "Class" element is removed in EAS 12.1 and later versions
+        if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
+            s.data(Tags.SYNC_CLASS, className);
+        }
+        s.data(Tags.SYNC_SYNC_KEY, syncKey);
+        s.data(Tags.SYNC_COLLECTION_ID, mMailbox.mServerId);
+        if (mInitialSync) {
+            //setInitialSyncOptions(s);
+        } else {
+            //setNonInitialSyncOptions(s, mNumWindows);
+            //setUpsyncCommands(s);
+        }
+        s.end().end().end().done();
+
+        return makeEntity(s);
+    }
+
+    @Override
+    protected int handleResponse(final EasResponse response)
+            throws IOException, CommandStatusException {
+        try {
+            final AbstractSyncParser parser = null;//getParser(response.getInputStream());
+            final boolean moreAvailable = parser.parse();
+            if (moreAvailable) {
+                return RESULT_MORE_AVAILABLE;
+            }
+        } catch (final Parser.EmptyStreamException e) {
+            // This indicates a compressed response which was empty, which is OK.
+        }
+        return RESULT_DONE;
+    }
+
+    @Override
+    public int performOperation() {
+        int result = RESULT_MORE_AVAILABLE;
+        mNumWindows = 1;
+        String key = getSyncKey();
+        while (result == RESULT_MORE_AVAILABLE) {
+            result = super.performOperation();
+            // TODO: Clear pending request queue.
+            final String newKey = getSyncKey();
+            if (result == RESULT_MORE_AVAILABLE && key.equals(newKey)) {
+                LogUtils.e(TAG,
+                        "Server has more data but we have the same key: %s numWindows: %d",
+                        key, mNumWindows);
+                mNumWindows++;
+            } else {
+                mNumWindows = 1;
+            }
+        }
+        return result;
+    }
+
+    @Override
+    protected long getTimeout() {
+        if (mInitialSync) {
+            return 120 * DateUtils.SECOND_IN_MILLIS;
+        }
+        return super.getTimeout();
+    }
+
+}
diff --git a/src/com/android/exchange/service/EasService.java b/src/com/android/exchange/service/EasService.java
index b5081f6..45c93f2 100644
--- a/src/com/android/exchange/service/EasService.java
+++ b/src/com/android/exchange/service/EasService.java
@@ -19,7 +19,6 @@
 import android.app.Service;
 import android.content.ContentResolver;
 import android.content.Intent;
-import android.content.SyncResult;
 import android.database.Cursor;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -79,16 +78,20 @@
             LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
             final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId,
                     attachmentId, callback);
-            doOperation(operation, null, "IEmailService.loadAttachment");
+            doOperation(operation, "IEmailService.loadAttachment");
         }
 
         @Override
         public void updateFolderList(final long accountId) {
             final EasFolderSync operation = new EasFolderSync(EasService.this, accountId);
-            doOperation(operation, null, "IEmailService.updateFolderList");
+            doOperation(operation, "IEmailService.updateFolderList");
         }
 
         @Override
+        public void sync(final long accountId, final boolean updateFolderList,
+                final int mailboxType, final long[] folders) {}
+
+        @Override
         public void pushModify(final long accountId) {
             LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId);
             final Account account = Account.restoreAccountWithId(EasService.this, accountId);
@@ -102,7 +105,7 @@
         @Override
         public Bundle validate(final HostAuth hostAuth) {
             final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth);
-            doOperation(operation, null, "IEmailService.validate");
+            doOperation(operation, "IEmailService.validate");
             return operation.getValidationResult();
         }
 
@@ -227,8 +230,7 @@
         return START_STICKY;
     }
 
-    public int doOperation(final EasOperation operation, final SyncResult syncResult,
-            final String loggingName) {
+    public int doOperation(final EasOperation operation, final String loggingName) {
         final long accountId = operation.getAccountId();
         LogUtils.d(TAG, "%s: %d", loggingName, accountId);
         mSynchronizer.syncStart(accountId);
@@ -238,7 +240,7 @@
         // it in the finally block below.
         // On the other hand, even for SAs, it doesn't hurt to get a wakelock here.
         try {
-            return operation.performOperation(syncResult);
+            return operation.performOperation();
         } finally {
             // TODO: Fix pushEnabled param
             mSynchronizer.syncEnd(accountId, false);
diff --git a/src/com/android/exchange/service/EasSyncHandler.java b/src/com/android/exchange/service/EasSyncHandler.java
index 0b6ee1c..6614399 100644
--- a/src/com/android/exchange/service/EasSyncHandler.java
+++ b/src/com/android/exchange/service/EasSyncHandler.java
@@ -375,7 +375,7 @@
             } else if (resp.isProvisionError()
                     || responseResult == SYNC_RESULT_PROVISIONING_ERROR) {
                 final EasProvision provision = new EasProvision(mContext, mAccount, this);
-                if (provision.provision(syncResult, mAccount.mId)) {
+                if (provision.provision()) {
                     // We handled the provisioning error, so loop.
                     LogUtils.d(TAG, "Provisioning error handled during sync, retrying");
                     result = SYNC_RESULT_MORE_AVAILABLE;
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index 65d9b00..eb17500 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -430,7 +430,7 @@
             // TODO: Prevent this from happening in parallel with a sync?
             final EasLoadAttachment operation = new EasLoadAttachment(EmailSyncAdapterService.this,
                     accountId, attachmentId, callback);
-            operation.performOperation(null);
+            operation.performOperation();
         }
 
         @Override
@@ -469,7 +469,26 @@
         public void sendMail(final long accountId) {}
 
         @Override
-        public void pushModify(final long accountId) {}
+        public void pushModify(final long accountId) {
+            LogUtils.d(TAG, "IEmailService.pushModify");
+            if (mEasService != null) {
+                try {
+                    mEasService.pushModify(accountId);
+                    return;
+                } catch (final RemoteException re) {
+                    LogUtils.e(TAG, re, "While asking EasService to handle pushModify");
+                }
+            }
+            final Account account = Account.restoreAccountWithId(EmailSyncAdapterService.this,
+                    accountId);
+            if (account != null) {
+                mSyncHandlerMap.modifyPing(false, account);
+            }
+        }
+
+        @Override
+        public void sync(final long accountId, final boolean updateFolderList,
+                final int mailboxType, final long[] folders) {}
     };
 
     public EmailSyncAdapterService() {
@@ -674,39 +693,60 @@
 
             // If we're just twiddling the push, we do the lightweight thing and bail early.
             if (pushOnly && !isFolderSync) {
-                mSyncHandlerMap.modifyPing(false, account);
                 LogUtils.d(TAG, "onPerformSync: mailbox push only");
+                if (mEasService != null) {
+                    try {
+                        mEasService.pushModify(account.mId);
+                        return;
+                    } catch (final RemoteException re) {
+                        LogUtils.e(TAG, re, "While trying to pushModify within onPerformSync");
+                    }
+                }
+                mSyncHandlerMap.modifyPing(false, account);
                 return;
             }
 
             // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
             mSyncHandlerMap.startSync(account.mId);
-
-            // Perform a FolderSync if necessary.
-            // TODO: We permit FolderSync even during security hold, because it's necessary to
-            // resolve some holds. Ideally we would only do it for the holds that require it.
-            if (isFolderSync) {
-                final EasFolderSync folderSync = new EasFolderSync(context, account);
-                folderSync.doFolderSync(syncResult);
-            }
-
             boolean lastSyncHadError = false;
 
-            if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) == 0) {
+            try {
+                // Perform a FolderSync if necessary.
+                // TODO: We permit FolderSync even during security hold, because it's necessary to
+                // resolve some holds. Ideally we would only do it for the holds that require it.
+                if (isFolderSync) {
+                    final EasFolderSync folderSync = new EasFolderSync(context, account);
+                    final int result = folderSync.doFolderSync();
+                    if (result < 0) {
+                        EasFolderSync.writeResultToSyncResult(result, syncResult);
+                        return;
+                    }
+                }
+
+                // Do not permit further syncs if we're on security hold.
+                if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
+                    return;
+                }
+
                 // Perform email upsync for this account. Moves first, then state changes.
                 if (!isInitialSync) {
                     EasMoveItems move = new EasMoveItems(context, account);
-                    move.upsyncMovedMessages(syncResult);
+                    final int moveResult = move.upsyncMovedMessages();
+                    if (moveResult < 0) {
+                        EasMoveItems.writeResultToSyncResult(moveResult, syncResult);
+                        return;
+                    }
+
                     // TODO: EasSync should eventually handle both up and down; for now, it's used
                     // purely for upsync.
                     EasSync upsync = new EasSync(context, account);
-                    upsync.upsync(syncResult);
+                    final int upsyncResult = upsync.upsync();
+                    if (upsyncResult < 0) {
+                        EasSync.writeResultToSyncResult(upsyncResult, syncResult);
+                        return;
+                    }
                 }
 
-                // TODO: Should we refresh 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.
-
                 if (mailboxIds != null) {
                     long numIoExceptions = 0;
                     long numAuthExceptions = 0;
@@ -761,15 +801,15 @@
                         }
                     }
                 }
+            } finally {
+                // Clean up the bookkeeping, including restarting ping if necessary.
+                mSyncHandlerMap.syncComplete(lastSyncHadError, account);
+
+                // TODO: It may make sense to have common error handling here. Two possibilities:
+                // 1) performSync return value can signal some useful info.
+                // 2) syncResult can contain useful info.
+                LogUtils.d(TAG, "onPerformSync: finished");
             }
-
-            // Clean up the bookkeeping, including restarting ping if necessary.
-            mSyncHandlerMap.syncComplete(lastSyncHadError, account);
-
-            // TODO: It may make sense to have common error handling here. Two possible mechanisms:
-            // 1) performSync return value can signal some useful info.
-            // 2) syncResult can contain useful info.
-            LogUtils.d(TAG, "onPerformSync: finished");
         }
 
         /**