If Ping encounters an error, wait before repinging.

This is a short-term fix; we should implement smarter backoff
behavior later. However, let's fix the battery burn immediately.

This CL also includes a fix to re-pinging in the error case for
people who do not have mail sync enabled.

Bug: 12589402
Bug: 13227663
Change-Id: If61d4a42fe846d8f4b5c16e84d6196ad61ddbe0e
diff --git a/src/com/android/exchange/eas/EasPing.java b/src/com/android/exchange/eas/EasPing.java
index be1a679..2ca4420 100644
--- a/src/com/android/exchange/eas/EasPing.java
+++ b/src/com/android/exchange/eas/EasPing.java
@@ -412,9 +412,17 @@
                 mAmAccount.toString(), extras.toString());
     }
 
+    /**
+     * Request a ping-only sync via the SyncManager. This is used in error paths, which is also why
+     * we don't just create and start a new ping task immediately: in the case where we have loss
+     * of network, we want to take advantage of the SyncManager to schedule this when we expect it
+     * to be able to work.
+     * @param amAccount Account that needs to ping.
+     */
     public static void requestPing(final android.accounts.Account amAccount) {
-        final Bundle extras = new Bundle(1);
+        final Bundle extras = new Bundle(2);
         extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
         LogUtils.i(LOG_TAG, "requestPing EasOperation %s, %s",
                 amAccount.toString(), extras.toString());
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index d963ec3..b378cd9 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -273,27 +273,16 @@
                 if (pingSyncHandler != null) {
                     pingSyncHandler.restart();
                 } else {
-                    // Start a new ping.
-                    // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
-                    // If we return before the ping starts, there's a race condition where another
-                    // ping or sync might start first. It only works for startSync because sync is
-                    // higher priority than ping (i.e. a ping can't start while a sync is pending)
-                    // and only one sync can run at a time.
                     if (lastSyncHadError) {
                         // Schedule an alarm to set up the ping in 5 minutes
-                        final Intent intent = new Intent(service, EmailSyncAdapterService.class);
-                        intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
-                        intent.putExtra(EXTRA_START_PING, true);
-                        intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
-                        final PendingIntent pi = PendingIntent.getService(
-                                EmailSyncAdapterService.this, 0, intent,
-                                PendingIntent.FLAG_ONE_SHOT);
-                        final AlarmManager am = (AlarmManager)getSystemService(
-                                Context.ALARM_SERVICE);
-                        final long atTime = SystemClock.elapsedRealtime() +
-                                SYNC_ERROR_BACKOFF_MILLIS;
-                        am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
+                        scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS);
                     } else {
+                        // Start a new ping.
+                        // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
+                        // If we return before the ping starts, there's a race condition where
+                        // another ping or sync might start first. It only works for startSync
+                        // because sync is higher priority than ping (i.e. a ping can't start while
+                        // a sync is pending) and only one sync can run at a time.
                         final PingTask pingHandler = new PingTask(service, account, amAccount,
                                 this);
                         mPingHandlers.put(account.mId, pingHandler);
@@ -351,14 +340,19 @@
             // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI.
             // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI.
 
-            // TODO: Should this just re-request ping if status < 0? This would do the wrong thing
-            // for e.g. auth errors, though.
             if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE ||
                     pingStatus == EasOperation.RESULT_OTHER_FAILURE) {
-                // Request a new ping through the SyncManager. This will do the right thing if the
-                // exception was due to loss of network connectivity, etc. (i.e. it will wait for
-                // network to restore and then request it).
-                EasPing.requestPing(amAccount);
+                // TODO: Sticky problem here: we necessarily aren't in a sync, so it's impossible to
+                // signal the error to the SyncManager and take advantage of backoff there. Worse,
+                // the current mechanism for how we do this will just encourage spammy requests
+                // since the actual ping-only sync request ALWAYS succeeds.
+                // So for now, let's delay a bit before asking the SyncManager to perform the sync.
+                // Longer term, this should be incorporated into some form of backoff, either
+                // by integrating with the SyncManager more fully or by implementing a Ping-specific
+                // backoff mechanism (e.g. integrate this with the logic for ping duration).
+                LogUtils.e(TAG, "Ping for account %d completed with error %d, delaying next ping",
+                        accountId, pingStatus);
+                scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS);
             } else {
                 stopServiceIfNoPings();
             }
@@ -978,4 +972,26 @@
         }
         return authsToSync;
     }
+
+    /**
+     * Schedule to have a ping start some time in the future. This is used when we encounter an
+     * error, and properly should be a more full featured back-off, but for the short run, just
+     * waiting a few minutes at least avoids burning battery.
+     * @param amAccount The account that needs to be pinged.
+     * @param delay The time in milliseconds to wait before requesting the ping-only sync. Note that
+     *              it may take longer than this before the ping actually happens, since there's two
+     *              layers of waiting ({@link AlarmManager} can choose to wait longer, as can the
+     *              SyncManager).
+     */
+    private void scheduleDelayedPing(final android.accounts.Account amAccount, final long delay) {
+        final Intent intent = new Intent(this, EmailSyncAdapterService.class);
+        intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
+        intent.putExtra(EXTRA_START_PING, true);
+        intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
+        final PendingIntent pi = PendingIntent.getService(this, 0, intent,
+                PendingIntent.FLAG_ONE_SHOT);
+        final AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+        final long atTime = SystemClock.elapsedRealtime() + delay;
+        am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
+    }
 }