Handle correction of rejected Ping heartbeat

* Handle status 5 for Ping command (heartbeat of out range)
* Write unit test for heartbeat reset

Bug: 2834195
Change-Id: Ic7952a4b296cf15c6ba895d6579fe7956b171e5b
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 3eb48c6..a95f304 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -153,10 +153,7 @@
     static private final int PING_MINUTES = 60; // in seconds
     static private final int PING_FUDGE_LOW = 10;
     static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
-    static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW;
-    static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW;
     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.
@@ -192,12 +189,18 @@
     private ArrayList<String> mPingChangeList;
     // The HttpPost in progress
     private volatile HttpPost mPendingPost = null;
+    // Our heartbeat when we are waiting for ping boxes to be ready
+    /*package*/ int mPingForceHeartbeat = 2*PING_MINUTES;
+    // The minimum heartbeat we will send
+    /*package*/ int mPingMinHeartbeat = (5*PING_MINUTES)-PING_FUDGE_LOW;
+    // The maximum heartbeat we will send
+    /*package*/ int mPingMaxHeartbeat = (17*PING_MINUTES)-PING_FUDGE_LOW;
     // The ping time (in seconds)
-    private int mPingHeartbeat = PING_STARTING_HEARTBEAT;
+    /*package*/ int mPingHeartbeat = PING_STARTING_HEARTBEAT;
     // The longest successful ping heartbeat
     private int mPingHighWaterMark = 0;
     // Whether we've ever lowered the heartbeat
-    private boolean mPingHeartbeatDropped = false;
+    /*package*/ boolean mPingHeartbeatDropped = false;
     // Whether a POST was aborted due to alarm (watchdog alarm)
     private boolean mPostAborted = false;
     // Whether a POST was aborted due to reset
@@ -1541,6 +1544,10 @@
                 } catch (StaleFolderListException e) {
                     // We break out if we get told about a stale folder list
                     userLog("Ping interrupted; folder list requires sync...");
+                } catch (IllegalHeartbeatException e) {
+                    // If we're sending an illegal heartbeat, reset either the min or the max to
+                    // that heartbeat
+                    resetHeartbeats(e.mLegalHeartbeat);
                 } finally {
                     Thread.currentThread().setName(threadName);
                 }
@@ -1561,6 +1568,44 @@
         }
     }
 
+    /**
+     * Reset either our minimum or maximum ping heartbeat to a heartbeat known to be legal
+     * @param legalHeartbeat a known legal heartbeat (from the EAS server)
+     */
+    /*package*/ void resetHeartbeats(int legalHeartbeat) {
+        userLog("Resetting min/max heartbeat, legal = " + legalHeartbeat);
+        // We are here because the current heartbeat (mPingHeartbeat) is invalid.  Depending on
+        // whether the argument is above or below the current heartbeat, we can infer the need to
+        // change either the minimum or maximum heartbeat
+        if (legalHeartbeat > mPingHeartbeat) {
+            // The legal heartbeat is higher than the ping heartbeat; therefore, our minimum was
+            // too low.  We respond by raising either or both of the minimum heartbeat or the
+            // force heartbeat to the argument value
+            if (mPingMinHeartbeat < legalHeartbeat) {
+                mPingMinHeartbeat = legalHeartbeat;
+            }
+            if (mPingForceHeartbeat < legalHeartbeat) {
+                mPingForceHeartbeat = legalHeartbeat;
+            }
+            // If our minimum is now greater than the max, bring them together
+            if (mPingMinHeartbeat > mPingMaxHeartbeat) {
+                mPingMaxHeartbeat = legalHeartbeat;
+            }
+        } else if (legalHeartbeat < mPingHeartbeat) {
+            // The legal heartbeat is lower than the ping heartbeat; therefore, our maximum was
+            // too high.  We respond by lowering the maximum to the argument value
+            mPingMaxHeartbeat = legalHeartbeat;
+            // If our maximum is now less than the minimum, bring them together
+            if (mPingMaxHeartbeat < mPingMinHeartbeat) {
+                mPingMinHeartbeat = legalHeartbeat;
+            }
+        }
+        // Set current heartbeat to the legal heartbeat
+        mPingHeartbeat = legalHeartbeat;
+        // Allow the heartbeat logic to run
+        mPingHeartbeatDropped = false;
+    }
+
     private void pushFallback(long mailboxId) {
         Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
         if (mailbox == null) {
@@ -1593,7 +1638,8 @@
         return false;
     }
 
-    private void runPingLoop() throws IOException, StaleFolderListException {
+    private void runPingLoop() throws IOException, StaleFolderListException,
+            IllegalHeartbeatException {
         int pingHeartbeat = mPingHeartbeat;
         userLog("runPingLoop");
         // Do push for all sync services here
@@ -1693,7 +1739,7 @@
                         userLog("Forcing ping after waiting for all boxes to be ready");
                     }
                     HttpResponse res =
-                        sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat);
+                        sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat);
 
                     int code = res.getStatusLine().getStatusCode();
                     userLog("Ping response: ", code);
@@ -1719,11 +1765,11 @@
                                     mPingHighWaterMark = pingHeartbeat;
                                     userLog("Setting high water mark at: ", mPingHighWaterMark);
                                 }
-                                if ((pingHeartbeat < PING_MAX_HEARTBEAT) &&
+                                if ((pingHeartbeat < mPingMaxHeartbeat) &&
                                         !mPingHeartbeatDropped) {
                                     pingHeartbeat += PING_HEARTBEAT_INCREMENT;
-                                    if (pingHeartbeat > PING_MAX_HEARTBEAT) {
-                                        pingHeartbeat = PING_MAX_HEARTBEAT;
+                                    if (pingHeartbeat > mPingMaxHeartbeat) {
+                                        pingHeartbeat = mPingMaxHeartbeat;
                                     }
                                     userLog("Increasing ping heartbeat to ", pingHeartbeat, "s");
                                 }
@@ -1748,12 +1794,12 @@
                         // ping.
                     } else if (mPostAborted || isLikelyNatFailure(message)) {
                         long pingLength = SystemClock.elapsedRealtime() - pingTime;
-                        if ((pingHeartbeat > PING_MIN_HEARTBEAT) &&
+                        if ((pingHeartbeat > mPingMinHeartbeat) &&
                                 (pingHeartbeat > mPingHighWaterMark)) {
                             pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
                             mPingHeartbeatDropped = true;
-                            if (pingHeartbeat < PING_MIN_HEARTBEAT) {
-                                pingHeartbeat = PING_MIN_HEARTBEAT;
+                            if (pingHeartbeat < mPingMinHeartbeat) {
+                                pingHeartbeat = mPingMinHeartbeat;
                             }
                             userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
                         } else if (mPostAborted) {
@@ -1823,7 +1869,7 @@
 
     private int parsePingResult(InputStream is, ContentResolver cr,
             HashMap<String, Integer> errorMap)
-        throws IOException, StaleFolderListException {
+            throws IOException, StaleFolderListException, IllegalHeartbeatException {
         PingParser pp = new PingParser(is, this);
         if (pp.parse()) {
             // True indicates some mailboxes need syncing...
diff --git a/src/com/android/exchange/IllegalHeartbeatException.java b/src/com/android/exchange/IllegalHeartbeatException.java
new file mode 100644
index 0000000..e480aa7
--- /dev/null
+++ b/src/com/android/exchange/IllegalHeartbeatException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+public class IllegalHeartbeatException extends EasException {
+    private static final long serialVersionUID = 1L;
+    public final int mLegalHeartbeat;
+
+    public IllegalHeartbeatException(int legalHeartbeat) {
+        mLegalHeartbeat = legalHeartbeat;
+    }
+}
diff --git a/src/com/android/exchange/adapter/PingParser.java b/src/com/android/exchange/adapter/PingParser.java
index 0731ac0..f061472 100644
--- a/src/com/android/exchange/adapter/PingParser.java
+++ b/src/com/android/exchange/adapter/PingParser.java
@@ -17,6 +17,7 @@
 package com.android.exchange.adapter;
 
 import com.android.exchange.EasSyncService;
+import com.android.exchange.IllegalHeartbeatException;
 import com.android.exchange.StaleFolderListException;
 
 import java.io.IOException;
@@ -62,7 +63,7 @@
     }
 
     @Override
-    public boolean parse() throws IOException, StaleFolderListException {
+    public boolean parse() throws IOException, StaleFolderListException, IllegalHeartbeatException {
         boolean res = false;
         if (nextTag(START_DOCUMENT) != Tags.PING_PING) {
             throw new IOException();
@@ -77,9 +78,15 @@
                 } else if (status == 7 || status == 4) {
                     // Status of 7 or 4 indicate a stale folder list
                     throw new StaleFolderListException();
+                } else if (status == 5) {
+                    // Status 5 means our heartbeat is beyond allowable limits
+                    // In this case, there will be a heartbeat interval set
                 }
             } else if (tag == Tags.PING_FOLDERS) {
                 parsePingFolders(syncList);
+            } else if (tag == Tags.PING_HEARTBEAT_INTERVAL) {
+                // Throw an exception, saving away the legal heartbeat interval specified
+                throw new IllegalHeartbeatException(getValueInt());
             } else {
                 skipTag();
             }
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index d4372a5..1832d5a 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -23,7 +23,11 @@
 import java.io.File;
 import java.io.IOException;
 
-public class EasSyncServiceTests extends AndroidTestCase {
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.EasSyncServiceTests email
+ */
+ public class EasSyncServiceTests extends AndroidTestCase {
     Context mMockContext;
 
     @Override
@@ -73,4 +77,46 @@
             }
         }
     }
+
+    public void testResetHeartbeats() {
+        EasSyncService svc = new EasSyncService();
+        // Test case in which the minimum and force heartbeats need to come up
+        svc.mPingMaxHeartbeat = 1000;
+        svc.mPingMinHeartbeat = 200;
+        svc.mPingHeartbeat = 300;
+        svc.mPingForceHeartbeat = 100;
+        svc.mPingHeartbeatDropped = true;
+        svc.resetHeartbeats(400);
+        assertEquals(400, svc.mPingMinHeartbeat);
+        assertEquals(1000, svc.mPingMaxHeartbeat);
+        assertEquals(400, svc.mPingHeartbeat);
+        assertEquals(400, svc.mPingForceHeartbeat);
+        assertFalse(svc.mPingHeartbeatDropped);
+
+        // Test case in which the force heartbeat needs to come up
+        svc.mPingMaxHeartbeat = 1000;
+        svc.mPingMinHeartbeat = 200;
+        svc.mPingHeartbeat = 100;
+        svc.mPingForceHeartbeat = 100;
+        svc.mPingHeartbeatDropped = true;
+        svc.resetHeartbeats(150);
+        assertEquals(200, svc.mPingMinHeartbeat);
+        assertEquals(1000, svc.mPingMaxHeartbeat);
+        assertEquals(150, svc.mPingHeartbeat);
+        assertEquals(150, svc.mPingForceHeartbeat);
+        assertFalse(svc.mPingHeartbeatDropped);
+
+        // Test case in which the maximum needs to come down
+        svc.mPingMaxHeartbeat = 1000;
+        svc.mPingMinHeartbeat = 200;
+        svc.mPingHeartbeat = 800;
+        svc.mPingForceHeartbeat = 100;
+        svc.mPingHeartbeatDropped = true;
+        svc.resetHeartbeats(600);
+        assertEquals(200, svc.mPingMinHeartbeat);
+        assertEquals(600, svc.mPingMaxHeartbeat);
+        assertEquals(600, svc.mPingHeartbeat);
+        assertEquals(100, svc.mPingForceHeartbeat);
+        assertFalse(svc.mPingHeartbeatDropped);
+    }
 }