Restore automatic ping and backoff on error functionality

This functionality was lost when I converted to use EasService
for syncing. Now it's back. This means that:
1. We will always have a ping that is scheduled to run every hour,
so even if the regular ping is lost due to some code error,
we'll start it again within the hour. This code is currently disabled
so that we will be able to test and discover if pings are being lost.
If they are, and we can't figure out why, then we can enable this code.
2. If sync fails due to some error, we will not start the ping
immediately. Instead we'll wait five minutes and then restart it.
This keeps us from rapidly spinning and burning the battery in
cases where the server is misbehaving.

Change-Id: I26e74a198a9dd6c87401079d048b5918d4869b50
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index 174b000..ef7d2a8 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -281,6 +281,12 @@
     }
 
     /**
+     * Should return true if the last operation encountered an error. Default implementation
+     * always returns false, child classes can override.
+     */
+    public final boolean lastSyncHadError() { return false; }
+
+    /**
      * The skeleton of performing an operation. This function handles all the common code and
      * error handling, calling into virtual functions that are implemented or overridden by the
      * subclass to do the operation-specific logic.
diff --git a/src/com/android/exchange/service/EasService.java b/src/com/android/exchange/service/EasService.java
index fb56e1c..8c29ba1 100644
--- a/src/com/android/exchange/service/EasService.java
+++ b/src/com/android/exchange/service/EasService.java
@@ -323,15 +323,19 @@
     public int doOperation(final EasOperation operation, final String loggingName) {
         LogUtils.d(TAG, "%s: %d", loggingName, operation.getAccountId());
         mSynchronizer.syncStart(operation.getAccountId());
+        int result = EasOperation.RESULT_MIN_OK_RESULT;
         // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA
         // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls.
         // If we add a wakelock (or anything else for that matter) here, must remember to undo
         // 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();
+            result = operation.performOperation();
+            LogUtils.d(TAG, "Operation result %d", result);
+            return result;
         } finally {
-            mSynchronizer.syncEnd(operation.getAccount());
+            mSynchronizer.syncEnd(result >= EasOperation.RESULT_MIN_OK_RESULT,
+                    operation.getAccount());
         }
     }
 
@@ -416,10 +420,10 @@
      * @return EmailServiceStatus
      */
     private int convertToEmailServiceStatus(int easStatus) {
+        if (easStatus >= EasOperation.RESULT_MIN_OK_RESULT) {
+            return EmailServiceStatus.SUCCESS;
+        }
         switch (easStatus) {
-            case EasOperation.RESULT_MIN_OK_RESULT:
-                return EmailServiceStatus.SUCCESS;
-
             case EasOperation.RESULT_ABORT:
             case EasOperation.RESULT_RESTART:
                 // This should only happen if a ping is interruped for some reason. We would not
diff --git a/src/com/android/exchange/service/PingSyncSynchronizer.java b/src/com/android/exchange/service/PingSyncSynchronizer.java
index 2c68463..11d57e6 100644
--- a/src/com/android/exchange/service/PingSyncSynchronizer.java
+++ b/src/com/android/exchange/service/PingSyncSynchronizer.java
@@ -16,12 +16,20 @@
 
 package com.android.exchange.service;
 
+import android.app.AlarmManager;
+import android.app.PendingIntent;
 import android.app.Service;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.util.LongSparseArray;
+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.Eas;
 import com.android.exchange.eas.EasPing;
 import com.android.mail.utils.LogUtils;
@@ -74,6 +82,18 @@
 
     private static final String TAG = Eas.LOG_TAG;
 
+    private static final long SYNC_ERROR_BACKOFF_MILLIS =  DateUtils.MINUTE_IN_MILLIS;
+    private static final String EXTRA_START_PING = "START_PING";
+    private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
+
+    // Enable this to make pings get automatically renewed every hour. This
+    // should not be needed, but if there is a software error that results in
+    // the ping being lost, this is a fallback to make sure that messages are
+    // not delayed more than an hour.
+    private static final boolean SCHEDULE_KICK = false;
+    private static final long KICK_SYNC_INTERVAL_SECONDS =
+            DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
+
     /**
      * This class handles bookkeeping for a single account.
      */
@@ -133,7 +153,8 @@
          * go ahead, or starting the ping if appropriate and there are no waiting ops.
          * @return Whether this account is now idle.
          */
-        public boolean syncEnd(final Account account, final PingSyncSynchronizer synchronizer) {
+        public boolean syncEnd(final boolean lastSyncHadError, final Account account,
+                               final PingSyncSynchronizer synchronizer) {
             --mSyncCount;
             if (mSyncCount > 0) {
                 LogUtils.d(TAG, "Signalling a pending sync to proceed.");
@@ -141,13 +162,21 @@
                 return false;
             } else {
                 if (mPushEnabled) {
-                    final android.accounts.Account amAccount =
-                            new android.accounts.Account(account.mEmailAddress,
+                    if (lastSyncHadError) {
+                        final android.accounts.Account amAccount =
+                                new android.accounts.Account(account.mEmailAddress,
                                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                    mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
-                            synchronizer);
-                    mPingTask.start();
-                    return false;
+                        scheduleDelayedPing(synchronizer.getContext(), amAccount);
+                        return true;
+                    } else {
+                        final android.accounts.Account amAccount =
+                                new android.accounts.Account(account.mEmailAddress,
+                                        Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+                        mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
+                                synchronizer);
+                        mPingTask.start();
+                        return false;
+                    }
                 }
             }
             return true;
@@ -157,7 +186,7 @@
          * Update bookkeeping when the ping task terminates, including signaling any waiting ops.
          * @return Whether this account is now idle.
          */
-        public boolean pingEnd(final android.accounts.Account amAccount) {
+        private boolean pingEnd(final android.accounts.Account amAccount) {
             mPingTask = null;
             if (mSyncCount > 0) {
                 mCondition.signal();
@@ -176,17 +205,32 @@
             return true;
         }
 
+        private void scheduleDelayedPing(final Context context,
+                                         final android.accounts.Account amAccount) {
+            LogUtils.d(TAG, "Scheduling a delayed ping.");
+            final Intent intent = new Intent(context, 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(context, 0, intent,
+                    PendingIntent.FLAG_ONE_SHOT);
+            final AlarmManager am = (AlarmManager)context.getSystemService(
+                    Context.ALARM_SERVICE);
+            final long atTime = SystemClock.elapsedRealtime() + SYNC_ERROR_BACKOFF_MILLIS;
+            am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
+        }
+
         /**
          * Modifies or starts a ping for this account if no syncs are running.
          */
         public void pushModify(final Account account, final PingSyncSynchronizer synchronizer) {
             mPushEnabled = true;
+            final android.accounts.Account amAccount =
+                    new android.accounts.Account(account.mEmailAddress,
+                            Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
             if (mSyncCount == 0) {
                 if (mPingTask == null) {
                     // No ping, no running syncs -- start a new ping.
-                    final android.accounts.Account amAccount =
-                            new android.accounts.Account(account.mEmailAddress,
-                                    Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
                     mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
                             synchronizer);
                     mPingTask.start();
@@ -195,6 +239,12 @@
                     mPingTask.restart();
                 }
             }
+            if (SCHEDULE_KICK) {
+                final Bundle extras = new Bundle(1);
+                extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
+                ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
+                        KICK_SYNC_INTERVAL_SECONDS);
+            }
         }
 
         /**
@@ -286,7 +336,7 @@
         }
     }
 
-    public void syncEnd(final Account account) {
+    public void syncEnd(final boolean lastSyncHadError, final Account account) {
         mLock.lock();
         try {
             final long accountId = account.getId();
@@ -296,7 +346,7 @@
                 LogUtils.w(TAG, "PSS syncEnd for account %d but no state found", accountId);
                 return;
             }
-            if (accountState.syncEnd(account, this)) {
+            if (accountState.syncEnd(lastSyncHadError, account, this)) {
                 removeAccount(accountId);
             }
         } finally {