Allow limited looping requests in sync

* Microsoft has documented cases in which the server can continue to
  send MoreAvailable=true even when no new data is received.  This
  can cause looping behavior, which we stop when we recognize it.
* This workaround, however, can prevent the situation from resolving
  itself, and lead to delayed sync (up to a few hours has been noticed)
* In this limited CL, we allow the sync to loop up to a maximum number
  of times before stopping it forcibly.

Bug: 2685984
Change-Id: I2913b7e3438f6180c3c440508fab892176a06540
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index e5b9919..694f6b9 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -158,6 +158,10 @@
     static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
     static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES;
 
+    // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
+    // forcing it to stop.  This number has been determined empirically.
+    static private final int MAX_LOOPING_COUNT = 100;
+
     static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
 
     // The amount of time we allow for a thread to release its post lock after receiving an alert
@@ -1926,6 +1930,7 @@
         Mailbox mailbox = target.mMailbox;
 
         boolean moreAvailable = true;
+        int loopingCount = 0;
         while (!mStop && moreAvailable) {
             // If we have no connectivity, just exit cleanly.  SyncManager will start us up again
             // when connectivity has returned
@@ -2025,6 +2030,18 @@
                 InputStream is = resp.getEntity().getContent();
                 if (is != null) {
                     moreAvailable = target.parse(is);
+                    if (target.isLooping()) {
+                        loopingCount++;
+                        userLog("** Looping: " + loopingCount);
+                        // After the maximum number of loops, we'll set moreAvailable to false and
+                        // allow the sync loop to terminate
+                        if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
+                            userLog("** Looping force stopped");
+                            moreAvailable = false;
+                        }
+                    } else {
+                        loopingCount = 0;
+                    }
                     target.cleanup();
                 } else {
                     userLog("Empty input stream in sync command response");
diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
index 1e0aff5..6473000 100644
--- a/src/com/android/exchange/adapter/AbstractSyncAdapter.java
+++ b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
@@ -57,6 +57,10 @@
     public abstract void cleanup();
     public abstract boolean isSyncable();
 
+    public boolean isLooping() {
+        return false;
+    }
+
     public AbstractSyncAdapter(Mailbox mailbox, EasSyncService service) {
         mMailbox = mailbox;
         mService = service;
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index 97dfb50..af17b79 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -45,6 +45,8 @@
     protected ContentResolver mContentResolver;
     protected AbstractSyncAdapter mAdapter;
 
+    private boolean mLooping;
+
     public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
         super(in);
         mAdapter = adapter;
@@ -78,6 +80,10 @@
      */
     public abstract void wipe();
 
+    public boolean isLooping() {
+        return mLooping;
+    }
+
     /**
      * Loop through the top-level structure coming from the Exchange server
      * Sync keys and the more available flag are handled here, whereas specific data parsing
@@ -89,7 +95,7 @@
         boolean moreAvailable = false;
         boolean newSyncKey = false;
         int interval = mMailbox.mSyncInterval;
-
+        mLooping = false;
         // If we're not at the top of the xml tree, throw an exception
         if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
             throw new EasParserException();
@@ -154,8 +160,7 @@
 
         // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
         if (moreAvailable && !newSyncKey) {
-            userLog("!! SyncKey hasn't changed, setting moreAvailable = false");
-            moreAvailable = false;
+            mLooping = true;
         }
 
         // Commit any changes
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index dc4f204..4e478b5 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -81,6 +81,9 @@
     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
 
+    // Holds the parser's value for isLooping()
+    boolean mIsLooping = false;
+
     public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
         super(mailbox, service);
     }
@@ -88,7 +91,18 @@
     @Override
     public boolean parse(InputStream is) throws IOException {
         EasEmailSyncParser p = new EasEmailSyncParser(is, this);
-        return p.parse();
+        boolean res = p.parse();
+        // Hold on to the parser's value for isLooping() to pass back to the service
+        mIsLooping = p.isLooping();
+        return res;
+    }
+
+    /**
+     * Return the value of isLooping() as returned from the parser
+     */
+    @Override
+    public boolean isLooping() {
+        return mIsLooping;
     }
 
     @Override