Handle provisioning errors for all sync operations.

Bug: 11247743
Change-Id: I095022380276141cbf29a6b7855a75d698ff5630
diff --git a/src/com/android/exchange/eas/EasFolderSync.java b/src/com/android/exchange/eas/EasFolderSync.java
index 696a4e7..1be9fe7 100644
--- a/src/com/android/exchange/eas/EasFolderSync.java
+++ b/src/com/android/exchange/eas/EasFolderSync.java
@@ -155,24 +155,11 @@
 
     @Override
     protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException {
+            throws IOException, CommandStatusException {
         if (!response.isEmpty()) {
-            try {
-                new FolderSyncParser(mContext, mContext.getContentResolver(),
-                        response.getInputStream(), mAccount, mStatusOnly).parse();
-            } catch (final CommandStatusException e) {
-                final int status = e.mStatus;
-                LogUtils.e(LOG_TAG, "EasFolderSync.handleResponse status %d", status);
-                if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
-                    return RESULT_PROVISIONING_ERROR;
-                }
-                if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
-                    return RESULT_FORBIDDEN;
-                }
-                return RESULT_OTHER_FAILURE;
-            }
+            new FolderSyncParser(mContext, mContext.getContentResolver(),
+                    response.getInputStream(), mAccount, mStatusOnly).parse();
         }
-
         return RESULT_OK;
     }
 
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index 37b7ead..bf7b013 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -33,6 +33,7 @@
 import com.android.emailcommon.provider.HostAuth;
 import com.android.emailcommon.provider.Mailbox;
 import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException;
 import com.android.exchange.Eas;
 import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.Serializer;
@@ -220,22 +221,42 @@
                 final int result;
                 // First off, the success case.
                 if (response.isSuccess()) {
+                    int responseResult;
                     try {
-                        result = handleResponse(response, syncResult);
-                        if (result >= 0) {
-                            return result;
-                        }
+                        responseResult = handleResponse(response, syncResult);
                     } 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
+                        // the payload of the response. These will have a HTTP 200 response, and the
+                        // error condition is only detected during response parsing.
+                        // The various parsers handle this by throwing a CommandStatusException.
+                        // TODO: Consider having the parsers return the errors instead of throwing.
+                        final int status = e.mStatus;
+                        LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status);
+                        if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
+                            responseResult = RESULT_PROVISIONING_ERROR;
+                        } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
+                            responseResult = RESULT_FORBIDDEN;
+                        } else {
+                            responseResult = RESULT_OTHER_FAILURE;
+                        }
                     }
+                    result = responseResult;
                 } else {
                     result = RESULT_OTHER_FAILURE;
                 }
 
+                // Non-negative results indicate success. Return immediately and bypass the error
+                // handling.
+                if (result >= 0) {
+                    return result;
+                }
+
                 // If this operation has distinct handling for 403 errors, do that.
                 if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) {
                     LogUtils.e(LOG_TAG, "Forbidden response");
@@ -251,6 +272,8 @@
                     if (handleProvisionError(syncResult, mAccountId)) {
                         // 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) {
@@ -376,7 +399,7 @@
      * @throws IOException
      */
     protected abstract int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException;
+            throws IOException, CommandStatusException;
 
     /**
      * The following functions may be overriden by a subclass, but most operations will not need
diff --git a/src/com/android/exchange/eas/EasSync.java b/src/com/android/exchange/eas/EasSync.java
index 710f692..6395092 100644
--- a/src/com/android/exchange/eas/EasSync.java
+++ b/src/com/android/exchange/eas/EasSync.java
@@ -183,7 +183,7 @@
 
     @Override
     protected int handleResponse(final EasResponse response, final SyncResult syncResult)
-            throws IOException {
+            throws IOException, CommandStatusException {
         final Account account = Account.restoreAccountWithId(mContext, mAccountId);
         if (account == null) {
             // TODO: Make this some other error type, since the account is just gone now.
@@ -200,9 +200,6 @@
             mMessageUpdateStatus = parser.getMessageStatuses();
         } catch (final Parser.EmptyStreamException e) {
             // This indicates a compressed response which was empty, which is OK.
-        } catch (final CommandStatusException e) {
-            // TODO: This is the wrong error type.
-            return RESULT_OTHER_FAILURE;
         }
         return 0;
     }
diff --git a/src/com/android/exchange/service/EasSyncHandler.java b/src/com/android/exchange/service/EasSyncHandler.java
index 0aefdb9..3f763a3 100644
--- a/src/com/android/exchange/service/EasSyncHandler.java
+++ b/src/com/android/exchange/service/EasSyncHandler.java
@@ -84,13 +84,12 @@
     public static final int PIM_WINDOW_SIZE_CALENDAR = 10;
 
     // TODO: For each type of failure, provide info about why.
+    protected static final int SYNC_RESULT_DENIED = -3;
+    protected static final int SYNC_RESULT_PROVISIONING_ERROR = -2;
     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;
-
     protected final ContentResolver mContentResolver;
     protected final Mailbox mMailbox;
     protected final Bundle mSyncExtras;
@@ -308,6 +307,15 @@
         } catch (final IOException e) {
             return SYNC_RESULT_FAILED;
         } catch (final CommandStatusException e) {
+            // TODO: This is basically copied from EasOperation, will go away when this merges.
+            final int status = e.mStatus;
+            LogUtils.e(TAG, "CommandStatusException: %d", status);
+            if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
+               return SYNC_RESULT_PROVISIONING_ERROR;
+            }
+            if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
+                return SYNC_RESULT_DENIED;
+            }
             return SYNC_RESULT_FAILED;
         }
         return SYNC_RESULT_DONE;
@@ -340,35 +348,44 @@
 
         final int result;
         try {
+            final int responseResult;
             final int code = resp.getStatus();
             if (code == HttpStatus.SC_OK) {
                 // A successful sync can have an empty response -- this indicates no change.
                 // In the case of a compressed stream, resp will be non-empty, but parse() handles
                 // that case.
                 if (!resp.isEmpty()) {
-                    result = parse(resp);
+                    responseResult = parse(resp);
                 } else {
-                    result = SYNC_RESULT_DONE;
+                    responseResult = SYNC_RESULT_DONE;
                 }
             } else {
                 LogUtils.e(TAG, "Sync failed with Status: " + code);
-                if (resp.isProvisionError()) {
-                    final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
-                    if (provision.provision(syncResult, mAccount.mId)) {
-                        // We handled the provisioning error, so loop.
-                        result = SYNC_RESULT_MORE_AVAILABLE;
-                    } else {
-                        syncResult.stats.numAuthExceptions++;
-                        return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_SECURITY;
-                    }
-                } else if (resp.isAuthError()) {
-                    syncResult.stats.numAuthExceptions++;
-                    return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_LOGIN;
-                } else {
-                    syncResult.stats.numParseExceptions++;
-                    return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_OTHER;
-                }
+                responseResult = SYNC_RESULT_FAILED;
             }
+
+            if (responseResult == SYNC_RESULT_DONE
+                    || responseResult == SYNC_RESULT_MORE_AVAILABLE) {
+                result = responseResult;
+            } else if (resp.isProvisionError()
+                    || responseResult == SYNC_RESULT_PROVISIONING_ERROR) {
+                final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
+                if (provision.provision(syncResult, mAccount.mId)) {
+                    // We handled the provisioning error, so loop.
+                    LogUtils.d(TAG, "Provisioning error handled during sync, retrying");
+                    result = SYNC_RESULT_MORE_AVAILABLE;
+                } else {
+                    syncResult.stats.numAuthExceptions++;
+                    result = SYNC_RESULT_FAILED;
+                }
+            } else if (resp.isAuthError() || responseResult == SYNC_RESULT_DENIED) {
+                syncResult.stats.numAuthExceptions++;
+                result = SYNC_RESULT_FAILED;
+            } else {
+                syncResult.stats.numParseExceptions++;
+                result = SYNC_RESULT_FAILED;
+            }
+
         } finally {
             resp.close();
         }
@@ -385,8 +402,8 @@
     /**
      * Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from
      * the system SyncManager and will be read by it on the way out).
-     * This function can send multiple Sync messages to the Exchange server, up to
-     * {@link #MAX_LOOPING_COUNT}, due to the server replying to a Sync request with MoreAvailable.
+     * This function can send multiple Sync messages to the Exchange server, due to the server
+     * replying to a Sync request with MoreAvailable.
      * In the case of errors, this function should not attempt any retries, but rather should
      * set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle
      * any it.