Add EasService & PingSyncSynchronizer.

This is the first step to decoupling actual sync execution from
the mail sync adapter. This change does not yet move to using the
new service, it only adds the code and necessary changes to existing
operations to be compatible with it. This change should not affect
existing functionality.

Change-Id: I80663c2bc216fdee44756d83fd567bc2c447e993
(cherry picked from commit 6c715246946dc4a7b7ca535dd9ff7f3cfc227c6d)
diff --git a/src/com/android/exchange/eas/EasFolderSync.java b/src/com/android/exchange/eas/EasFolderSync.java
index 1be9fe7..48d4070 100644
--- a/src/com/android/exchange/eas/EasFolderSync.java
+++ b/src/com/android/exchange/eas/EasFolderSync.java
@@ -30,6 +30,7 @@
 import com.android.exchange.adapter.FolderSyncParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
+import com.android.exchange.service.EasService;
 import com.android.mail.utils.LogUtils;
 
 import org.apache.http.HttpEntity;
@@ -55,15 +56,26 @@
      */
     public static final int RESULT_WRONG_OPERATION = 2;
 
-    // TODO: Eliminate the need for mAccount (requires FolderSyncParser changes).
-    private final Account mAccount;
-
     /** Indicates whether this object is for validation rather than sync. */
     private final boolean mStatusOnly;
 
     /** During validation, this holds the policy we must enforce. */
     private Policy mPolicy;
 
+    /** During validation, this holds the result. */
+    private Bundle mValidationResult;
+
+    /**
+     * Constructor for use with {@link EasService} when performing an actual sync.
+     * @param context
+     * @param accountId
+     */
+    public EasFolderSync(final Context context, final long accountId) {
+        super(context, accountId);
+        mStatusOnly = false;
+        mPolicy = null;
+    }
+
     /**
      * Constructor for actually doing folder sync.
      * @param context
@@ -71,7 +83,6 @@
      */
     public EasFolderSync(final Context context, final Account account) {
         super(context, account);
-        mAccount = account;
         mStatusOnly = false;
         mPolicy = null;
     }
@@ -82,18 +93,32 @@
      * @param hostAuth
      */
     public EasFolderSync(final Context context, final HostAuth hostAuth) {
-        this(context, new Account(), hostAuth);
+        super(context, -1);
+        setDummyAccount(hostAuth);
+        mStatusOnly = true;
     }
 
-    private EasFolderSync(final Context context, final Account account, final HostAuth hostAuth) {
-        super(context, account, hostAuth);
-        mAccount = account;
-        mAccount.mEmailAddress = hostAuth.mLogin;
-        mStatusOnly = true;
+    @Override
+    public int performOperation(final SyncResult syncResult) {
+        if (mStatusOnly) {
+            return validate();
+        } else {
+            LogUtils.d(LOG_TAG, "Performing FolderSync for account %d", getAccountId());
+            return super.performOperation(syncResult);
+        }
+    }
+
+    /**
+     * Returns the validation results after this operation has been performed.
+     * @return The validation results.
+     */
+    public Bundle getValidationResult() {
+        return mValidationResult;
     }
 
     /**
      * Perform a folder sync.
+     * TODO: Remove this function when transition to EasService is complete.
      * @param syncResult The {@link SyncResult} object for this sync operation.
      * @return A result code, either from above or from the base class.
      */
@@ -101,42 +126,61 @@
         if (mStatusOnly) {
             return RESULT_WRONG_OPERATION;
         }
-        LogUtils.d(LOG_TAG, "Performing sync for account %d", mAccount.mId);
-        return performOperation(syncResult);
+        LogUtils.d(LOG_TAG, "Performing sync for account %d", getAccountId());
+        // This intentionally calls super.performOperation -- calling our performOperation
+        // will simply end up calling super.performOperation anyway. This is part of the transition
+        // to EasService and will go away when this function is deleted.
+        return super.performOperation(syncResult);
     }
 
     /**
-     * Perform account validation.
-     * @return The response {@link Bundle} expected by the RPC.
+     * Helper function for {@link #performOperation} -- do some initial checks and, if they pass,
+     * perform a folder sync to verify that we can. This sets {@link #mValidationResult} as a side
+     * effect which holds the result details needed by the UI.
+     * @return A result code, either from above or from the base class.
      */
-    public Bundle validate() {
-        final Bundle bundle = new Bundle(3);
+    private int validate() {
+        mValidationResult = new Bundle(3);
         if (!mStatusOnly) {
-            writeResultCode(bundle, RESULT_OTHER_FAILURE);
-            return bundle;
+            writeResultCode(mValidationResult, RESULT_OTHER_FAILURE);
+            return RESULT_OTHER_FAILURE;
         }
         LogUtils.d(LOG_TAG, "Performing validation");
 
         if (!registerClientCert()) {
-            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
+            mValidationResult.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
                     MessagingException.CLIENT_CERTIFICATE_ERROR);
-            return bundle;
+            return RESULT_CLIENT_CERTIFICATE_REQUIRED;
         }
 
         if (shouldGetProtocolVersion()) {
             final EasOptions options = new EasOptions(this);
             final int result = options.getProtocolVersionFromServer(null);
             if (result != EasOptions.RESULT_OK) {
-                writeResultCode(bundle, result);
-                return bundle;
+                writeResultCode(mValidationResult, result);
+                return result;
             }
             final String protocolVersion = options.getProtocolVersionString();
             setProtocolVersion(protocolVersion);
-            bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION, protocolVersion);
+            mValidationResult.putString(EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION,
+                    protocolVersion);
         }
 
-        writeResultCode(bundle, performOperation(null));
-        return bundle;
+        // This is intentionally a call to super.performOperation. This is a helper function for
+        // our version of perfomOperation so calling that function would infinite loop.
+        final int result = super.performOperation(null);
+        writeResultCode(mValidationResult, result);
+        return result;
+    }
+
+    /**
+     * Perform account validation.
+     * TODO: Remove this function when transition to EasService is complete.
+     * @return The response {@link Bundle} expected by the RPC.
+     */
+    public Bundle doValidate() {
+        validate();
+        return mValidationResult;
     }
 
     @Override
diff --git a/src/com/android/exchange/eas/EasMoveItems.java b/src/com/android/exchange/eas/EasMoveItems.java
index 97ce18c..ed2ecd7 100644
--- a/src/com/android/exchange/eas/EasMoveItems.java
+++ b/src/com/android/exchange/eas/EasMoveItems.java
@@ -53,7 +53,7 @@
 
     // TODO: Allow multiple messages in one request. Requires parser changes.
     public int upsyncMovedMessages(final SyncResult syncResult) {
-        final List<MessageMove> moves = MessageMove.getMoves(mContext, mAccountId);
+        final List<MessageMove> moves = MessageMove.getMoves(mContext, getAccountId());
         if (moves == null) {
             return RESULT_NO_MESSAGES;
         }
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index fcdeeda..4007847 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -56,15 +56,45 @@
  * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one.
  * This class abstracts the connection handling from its subclasses and callers.
  *
- * A subclass must implement the abstract functions below that create the request and parse the
- * response. There are also a set of functions that a subclass may override if it's substantially
- * different from the "normal" operation (e.g. most requests use the same request URI, but auto
- * discover deviates since it's not account-specific), but the default implementation should suffice
- * for most. The subclass must also define a public function which calls {@link #performOperation},
- * possibly doing nothing other than that. (I chose to force subclasses to do this, rather than
- * provide that function in the base class, in order to force subclasses to consider, for example,
- * whether it needs a {@link SyncResult} parameter, and what the proper name for the "doWork"
- * function ought to be for the subclass.)
+ * {@link #performOperation} calls various abstract functions to create the request and parse the
+ * response. For the most part subclasses can implement just these bits of functionality and rely
+ * on {@link #performOperation} to do all the boilerplate etc.
+ *
+ * There are also a set of functions that a subclass may override if it's substantially
+ * different from the "normal" operation (e.g. autodiscover deviates from the standard URI since
+ * it's not account-specific so it needs to override {@link #getRequestUri()}), but the default
+ * implementations of these functions should suffice for most operations.
+ *
+ * Some subclasses may need to override {@link #performOperation} to add validation and results
+ * processing around a call to super.performOperation. Subclasses should avoid doing too much more
+ * than wrapping some handling around the chained call; if you find that's happening, it's likely
+ * a sign that the base class needs to be enhanced.
+ *
+ * One notable reason this wrapping happens is for operations that need to return a result directly
+ * to their callers (as opposed to simply writing the results to the provider, as is common with
+ * sync operations). This happens for example in
+ * {@link com.android.emailcommon.service.IEmailService} message handlers. In such cases, due to
+ * how {@link com.android.exchange.service.EasService} uses this class, the subclass needs to
+ * store the result as a member variable and then provide an accessor to read the result. Since
+ * different operations have different results (or none at all), there is no function in the base
+ * class for this.
+ *
+ * Note that it is not practical to avoid the race between when an operation loads its account data
+ * and when it uses it, as that would require some form of locking in the provider. There are three
+ * interesting situations where this might happen, and that this class must handle:
+ *
+ * 1) Deleted from provider: Any subsequent provider access should return an error. Operations
+ *    must detect this and terminate with an error.
+ * 2) Account sync settings change: Generally only affects Ping. We interrupt the operation and
+ *    load the new settings before proceeding.
+ * 3) Sync suspended due to hold: A special case of the previous, and affects all operations, but
+ *    fortunately doesn't need special handling here. Correct provider functionality must generate
+ *    write failures, so the handling for #1 should cover this case as well.
+ *
+ * This class attempts to defer loading of account data as long as possible -- ideally we load
+ * immediately before the network request -- but does not proactively check for changes after that.
+ * This approach is a a practical balance between minimizing the race without adding too much
+ * complexity beyond what's required.
  */
 public abstract class EasOperation {
     public static final String LOG_TAG = Eas.LOG_TAG;
@@ -93,42 +123,94 @@
     public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8;
     /** Error code indicating we don't have a protocol version in common with the server. */
     public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9;
+    /** Error code indicating the account could not be loaded from the provider. */
+    public static final int RESULT_ACCOUNT_ID_INVALID = -10;
     /** Error code indicating some other failure. */
-    public static final int RESULT_OTHER_FAILURE = -10;
+    public static final int RESULT_OTHER_FAILURE = -11;
 
     protected final Context mContext;
 
-    /**
-     * The account id for this operation.
-     * NOTE: You will be tempted to add a reference to the {@link Account} here. Resist.
-     * It's too easy for that to lead to creep and stale data.
-     */
-    protected final long mAccountId;
-    private final EasServerConnection mConnection;
+    /** The provider id for the account this operation is on. */
+    private final long mAccountId;
 
-    // TODO: Make this private again when EasSyncHandler is converted to be a subclass.
-    protected EasOperation(final Context context, final long accountId,
-            final EasServerConnection connection) {
+    /** The cached {@link Account} state; can be null if it hasn't been loaded yet. */
+    protected Account mAccount;
+
+    /** The connection to use for this operation. This is created when {@link #mAccount} is set. */
+    private EasServerConnection mConnection;
+
+    /**
+     * Constructor which defers loading of account and connection info.
+     * @param context
+     * @param accountId
+     */
+    protected EasOperation(final Context context, final long accountId) {
         mContext = context;
         mAccountId = accountId;
+    }
+
+    // TODO: Make this private again when EasSyncHandler is converted to be a subclass.
+    protected EasOperation(final Context context, final Account account,
+            final EasServerConnection connection) {
+        this(context, account.mId);
+        mAccount = account;
         mConnection = connection;
     }
 
     protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
-        this(context, account.mId, new EasServerConnection(context, account, hostAuth));
+        this(context, account, new EasServerConnection(context, account, hostAuth));
     }
 
     protected EasOperation(final Context context, final Account account) {
-        this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv));
+        this(context, account, account.getOrCreateHostAuthRecv(context));
     }
 
     /**
      * This constructor is for use by operations that are created by other operations, e.g.
-     * {@link EasProvision}.
+     * {@link EasProvision}. It reuses the account and connection of its parent.
      * @param parentOperation The {@link EasOperation} that is creating us.
      */
     protected EasOperation(final EasOperation parentOperation) {
-        this(parentOperation.mContext, parentOperation.mAccountId, parentOperation.mConnection);
+        mContext = parentOperation.mContext;
+        mAccountId = parentOperation.mAccountId;
+        mAccount = parentOperation.mAccount;
+        mConnection = parentOperation.mConnection;
+    }
+
+    /**
+     * Some operations happen before the account exists (e.g. account validation).
+     * These operations cannot use {@link #loadAccount}, so instead we make a dummy account and
+     * supply a temporary {@link HostAuth}.
+     * @param hostAuth
+     */
+    protected final void setDummyAccount(final HostAuth hostAuth) {
+        mAccount = new Account();
+        mAccount.mEmailAddress = hostAuth.mLogin;
+        mConnection = new EasServerConnection(mContext, mAccount, hostAuth);
+    }
+
+    /**
+     * Loads (or reloads) the {@link Account} for this operation, and sets up our connection to the
+     * server.
+     * @param allowReload If false, do not perform a load if we already have an {@link Account}
+     *                    (i.e. just keep the existing one); otherwise allow replacement of the
+     *                    account. Note that this can result in a valid Account being replaced with
+     *                    null if the account no longer exists.
+     * @return Whether we now have a valid {@link Account} object.
+     */
+    public final boolean loadAccount(final boolean allowReload) {
+        if (mAccount == null || allowReload) {
+            mAccount = Account.restoreAccountWithId(mContext, getAccountId());
+            if (mAccount != null) {
+                mConnection = new EasServerConnection(mContext, mAccount,
+                        mAccount.getOrCreateHostAuthRecv(mContext));
+            }
+        }
+        return (mAccount != null);
+    }
+
+    public final long getAccountId() {
+        return mAccountId;
     }
 
     /**
@@ -169,7 +251,14 @@
      *                   be written to for this sync; otherwise null.
      * @return A result code for the outcome of this operation, as described above.
      */
-    protected final int performOperation(final SyncResult syncResult) {
+    public int performOperation(final SyncResult syncResult) {
+        // Make sure the account is loaded if it hasn't already been.
+        if (!loadAccount(false)) {
+            LogUtils.i(LOG_TAG, "Failed to load account %d before sending request for operation %s",
+                    getAccountId(), getCommand());
+            return RESULT_ACCOUNT_ID_INVALID;
+        }
+
         // We handle server redirects by looping, but we need to protect against too much looping.
         int redirectCount = 0;
 
@@ -269,7 +358,7 @@
 
                 // Handle provisioning errors.
                 if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) {
-                    if (handleProvisionError(syncResult, mAccountId)) {
+                    if (handleProvisionError(syncResult, getAccountId())) {
                         // 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",
@@ -331,8 +420,9 @@
      * @param protocolVersion The new protocol version to use, as a string.
      */
     protected final void setProtocolVersion(final String protocolVersion) {
-        if (mConnection.setProtocolVersion(protocolVersion) && mAccountId != Account.NOT_SAVED) {
-            final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId);
+        final long accountId = getAccountId();
+        if (mConnection.setProtocolVersion(protocolVersion) && accountId != Account.NOT_SAVED) {
+            final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
             final ContentValues cv = new ContentValues(2);
             if (getProtocolVersion() >= 12.0) {
                 final int oldFlags = Utility.getFirstRowInt(mContext, uri,
diff --git a/src/com/android/exchange/eas/EasPing.java b/src/com/android/exchange/eas/EasPing.java
index 507ca0c..d8e28a4 100644
--- a/src/com/android/exchange/eas/EasPing.java
+++ b/src/com/android/exchange/eas/EasPing.java
@@ -23,8 +23,6 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.provider.CalendarContract;
-import android.provider.ContactsContract;
 import android.text.format.DateUtils;
 
 import com.android.emailcommon.provider.Account;
@@ -44,9 +42,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Set;
 
 /**
  * Performs an Exchange Ping, which is the command for receiving push notifications.
@@ -58,7 +54,6 @@
     private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
             MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
 
-    private final long mAccountId;
     private final android.accounts.Account mAmAccount;
     private long mPingDuration;
 
@@ -97,13 +92,12 @@
     public EasPing(final Context context, final Account account,
             final android.accounts.Account amAccount) {
         super(context, account);
-        mAccountId = account.mId;
         mAmAccount = amAccount;
         mPingDuration = account.mPingDuration;
         if (mPingDuration == 0) {
             mPingDuration = DEFAULT_PING_HEARTBEAT;
         }
-        LogUtils.d(TAG, "initial ping duration " + mPingDuration + " account " + mAccountId);
+        LogUtils.d(TAG, "initial ping duration " + mPingDuration + " account " + getAccountId());
     }
 
     public final int doPing() {
@@ -123,7 +117,7 @@
         mPingDuration = Math.max(MINIMUM_PING_HEARTBEAT,
                 mPingDuration - MAXIMUM_HEARTBEAT_INCREMENT);
         LogUtils.d(TAG, "decreasePingDuration adjusting by " + MAXIMUM_HEARTBEAT_INCREMENT +
-                " new duration " + mPingDuration + " account " + mAccountId);
+                " new duration " + mPingDuration + " account " + getAccountId());
         storePingDuration();
     }
 
@@ -131,18 +125,14 @@
         mPingDuration = Math.min(MAXIMUM_PING_HEARTBEAT,
                 mPingDuration + MAXIMUM_HEARTBEAT_INCREMENT);
         LogUtils.d(TAG, "increasePingDuration adjusting by " + MAXIMUM_HEARTBEAT_INCREMENT +
-                " new duration " + mPingDuration + " account " + mAccountId);
+                " new duration " + mPingDuration + " account " + getAccountId());
         storePingDuration();
     }
 
     private void storePingDuration() {
         final ContentValues values = new ContentValues(1);
         values.put(AccountColumns.PING_DURATION, mPingDuration);
-        Account.update(mContext, Account.CONTENT_URI, mAccountId, values);
-    }
-
-    public final long getAccountId() {
-        return mAccountId;
+        Account.update(mContext, Account.CONTENT_URI, getAccountId(), values);
     }
 
     public final android.accounts.Account getAmAccount() {
@@ -158,7 +148,7 @@
     protected HttpEntity getRequestEntity() throws IOException {
         // Get the mailboxes that need push notifications.
         final Cursor c = Mailbox.getMailboxesForPush(mContext.getContentResolver(),
-                mAccountId);
+                getAccountId());
         if (c == null) {
             throw new IllegalStateException("Could not read mailboxes");
         }
@@ -201,14 +191,15 @@
         // Take the appropriate action for this response.
         // Many of the responses require no explicit action here, they just influence
         // our re-ping behavior, which is handled by the caller.
+        final long accountId = getAccountId();
         switch (pingStatus) {
             case PingParser.STATUS_EXPIRED:
-                LogUtils.i(TAG, "Ping expired for account %d", mAccountId);
+                LogUtils.i(TAG, "Ping expired for account %d", accountId);
                 // On successful expiration, we can increase our ping duration
                 increasePingDuration();
                 break;
             case PingParser.STATUS_CHANGES_FOUND:
-                LogUtils.i(TAG, "Ping found changed folders for account %d", mAccountId);
+                LogUtils.i(TAG, "Ping found changed folders for account %d", accountId);
                 requestSyncForSyncList(pp.getSyncList());
                 break;
             case PingParser.STATUS_REQUEST_INCOMPLETE:
@@ -216,28 +207,28 @@
                 // These two cases indicate that the ping request was somehow bad.
                 // TODO: It's insanity to re-ping with the same data and expect a different
                 // result. Improve this if possible.
-                LogUtils.e(TAG, "Bad ping request for account %d", mAccountId);
+                LogUtils.e(TAG, "Bad ping request for account %d", accountId);
                 break;
             case PingParser.STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS:
                 long newDuration = pp.getHeartbeatInterval();
                 LogUtils.i(TAG, "Heartbeat out of bounds for account %d, " +
-                        "old duration %d new duration %d", mAccountId, mPingDuration, newDuration);
+                        "old duration %d new duration %d", accountId, mPingDuration, newDuration);
                 mPingDuration = newDuration;
                 storePingDuration();
                 break;
             case PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS:
-                LogUtils.i(TAG, "Too many folders for account %d", mAccountId);
+                LogUtils.i(TAG, "Too many folders for account %d", accountId);
                 break;
             case PingParser.STATUS_FOLDER_REFRESH_NEEDED:
-                LogUtils.i(TAG, "FolderSync needed for account %d", mAccountId);
+                LogUtils.i(TAG, "FolderSync needed for account %d", accountId);
                 requestFolderSync();
                 break;
             case PingParser.STATUS_SERVER_ERROR:
-                LogUtils.i(TAG, "Server error for account %d", mAccountId);
+                LogUtils.i(TAG, "Server error for account %d", accountId);
                 break;
             case CommandStatus.SERVER_ERROR_RETRY:
                 // Try again later.
-                LogUtils.i(TAG, "Retryable server error for account %d", mAccountId);
+                LogUtils.i(TAG, "Retryable server error for account %d", accountId);
                 return RESULT_RESTART;
 
             // These errors should not happen.
@@ -328,7 +319,7 @@
      */
     private void requestSyncForSyncList(final ArrayList<String> syncList) {
         final String[] bindArguments = new String[2];
-        bindArguments[0] = Long.toString(mAccountId);
+        bindArguments[0] = Long.toString(getAccountId());
 
         final ArrayList<Long> mailboxIds = new ArrayList<Long>();
         final HashSet<Integer> contentTypes = new HashSet<Integer>();
diff --git a/src/com/android/exchange/eas/EasProvision.java b/src/com/android/exchange/eas/EasProvision.java
index 97e6a65..7b5bcb9 100644
--- a/src/com/android/exchange/eas/EasProvision.java
+++ b/src/com/android/exchange/eas/EasProvision.java
@@ -21,6 +21,7 @@
 import android.os.Bundle;
 import android.telephony.TelephonyManager;
 
+import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.Policy;
 import com.android.emailcommon.service.PolicyServiceProxy;
@@ -95,9 +96,9 @@
     private int mPhase;
 
     // TODO: Temporary until EasSyncHandler converts to EasOperation.
-    public EasProvision(final Context context, final long accountId,
+    public EasProvision(final Context context, final Account account,
             final EasServerConnection connection) {
-        super(context, accountId, connection);
+        super(context, account, connection);
         mPolicy = null;
         mPolicyKey = null;
         mStatus = null;
diff --git a/src/com/android/exchange/eas/EasSync.java b/src/com/android/exchange/eas/EasSync.java
index 6395092..084f921 100644
--- a/src/com/android/exchange/eas/EasSync.java
+++ b/src/com/android/exchange/eas/EasSync.java
@@ -104,8 +104,8 @@
      * @return Number of messages successfully synced, or -1 if we encountered an error.
      */
     public final int upsync(final SyncResult syncResult) {
-        final List<MessageStateChange> changes = MessageStateChange.getChanges(mContext, mAccountId,
-                        getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE);
+        final List<MessageStateChange> changes = MessageStateChange.getChanges(mContext,
+                getAccountId(), getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE);
         if (changes == null) {
             return 0;
         }
@@ -184,17 +184,12 @@
     @Override
     protected int handleResponse(final EasResponse response, final SyncResult syncResult)
             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.
-            return RESULT_OTHER_FAILURE;
-        }
         final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
         if (mailbox == null) {
             return RESULT_OTHER_FAILURE;
         }
         final EmailSyncParser parser = new EmailSyncParser(mContext, mContext.getContentResolver(),
-                response.getInputStream(), mailbox, account);
+                response.getInputStream(), mailbox, mAccount);
         try {
             parser.parse();
             mMessageUpdateStatus = parser.getMessageStatuses();
diff --git a/src/com/android/exchange/service/EasService.java b/src/com/android/exchange/service/EasService.java
new file mode 100644
index 0000000..54b7f45
--- /dev/null
+++ b/src/com/android/exchange/service/EasService.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2014 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.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.os.IBinder;
+
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.service.IEmailService;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.service.SearchParams;
+import com.android.exchange.Eas;
+import com.android.exchange.eas.EasFolderSync;
+import com.android.exchange.eas.EasOperation;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * Service to handle all communication with the EAS server. Note that this is completely decoupled
+ * from the sync adapters; sync adapters should make blocking calls on this service to actually
+ * perform any operations.
+ */
+public class EasService extends Service {
+
+    private static final String TAG = Eas.LOG_TAG;
+
+    private final PingSyncSynchronizer mSynchronizer;
+
+    private final IEmailService.Stub mBinder = new IEmailService.Stub() {
+        @Override
+        public void sendMail(final long accountId) {}
+
+        @Override
+        public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId,
+                final boolean background) {
+            LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
+        }
+
+        @Override
+        public void updateFolderList(final long accountId) {
+            final EasFolderSync operation = new EasFolderSync(EasService.this, accountId);
+            doOperation(operation, null, "IEmailService.updateFolderList");
+        }
+
+        @Override
+        public Bundle validate(final HostAuth hostAuth) {
+            final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth);
+            doOperation(operation, null, "IEmailService.validate");
+            return operation.getValidationResult();
+        }
+
+        @Override
+        public int searchMessages(final long accountId, final SearchParams searchParams,
+                final long destMailboxId) {
+            LogUtils.d(TAG, "IEmailService.searchMessages");
+            return 0;
+        }
+
+        @Override
+        public void sendMeetingResponse(final long messageId, final int response) {
+            LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
+        }
+
+        @Override
+        public Bundle autoDiscover(final String username, final String password) {
+            LogUtils.d(TAG, "IEmailService.autoDiscover");
+            return null;
+        }
+
+        @Override
+        public void setLogging(final int flags) {
+            LogUtils.d(TAG, "IEmailService.setLogging");
+        }
+
+        @Override
+        public void deleteAccountPIMData(final String emailAddress) {
+            LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
+        }
+    };
+
+    public EasService() {
+        super();
+        mSynchronizer = new PingSyncSynchronizer(this);
+    }
+
+    @Override
+    public void onCreate() {
+        // TODO: Restart all pings that are needed.
+    }
+
+    @Override
+    public void onDestroy() {
+        // TODO: Stop all running pings.
+    }
+
+    @Override
+    public IBinder onBind(final Intent intent) {
+        return mBinder;
+    }
+
+    public int doOperation(final EasOperation operation, final SyncResult syncResult,
+            final String loggingName) {
+        final long accountId = operation.getAccountId();
+        LogUtils.d(TAG, "%s: %d", loggingName, accountId);
+        mSynchronizer.syncStart(accountId);
+        // 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(syncResult);
+        } finally {
+            // TODO: Fix pushEnabled param
+            mSynchronizer.syncEnd(accountId, false);
+        }
+    }
+}
diff --git a/src/com/android/exchange/service/EasSyncHandler.java b/src/com/android/exchange/service/EasSyncHandler.java
index f2c92c3..0b6ee1c 100644
--- a/src/com/android/exchange/service/EasSyncHandler.java
+++ b/src/com/android/exchange/service/EasSyncHandler.java
@@ -374,7 +374,7 @@
                 result = responseResult;
             } else if (resp.isProvisionError()
                     || responseResult == SYNC_RESULT_PROVISIONING_ERROR) {
-                final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
+                final EasProvision provision = new EasProvision(mContext, mAccount, this);
                 if (provision.provision(syncResult, mAccount.mId)) {
                     // We handled the provisioning error, so loop.
                     LogUtils.d(TAG, "Provisioning error handled during sync, retrying");
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index e7eae82..834d84b 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -362,7 +362,7 @@
         @Override
         public Bundle validate(final HostAuth hostAuth) {
             LogUtils.d(TAG, "IEmailService.validate");
-            return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).validate();
+            return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).doValidate();
         }
 
         @Override
diff --git a/src/com/android/exchange/service/PingSyncSynchronizer.java b/src/com/android/exchange/service/PingSyncSynchronizer.java
new file mode 100644
index 0000000..9fa0197
--- /dev/null
+++ b/src/com/android/exchange/service/PingSyncSynchronizer.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2014 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.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.support.v4.util.LongSparseArray;
+
+import com.android.exchange.Eas;
+import com.android.mail.utils.LogUtils;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Bookkeeping for handling synchronization between pings and other sync related operations.
+ * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
+ * the term for the Exchange command, but this code should be generic enough to be extended to IMAP.
+ *
+ * Basic rules of how these interact (note that all rules are per account):
+ * - Only one operation (ping or other active sync operation) may run at a time.
+ * - For shorthand, this class uses "sync" to mean "non-ping operation"; most such operations are
+ *   sync ops, but some may not be (e.g. EAS Settings).
+ * - Syncs can come from many sources concurrently; this class must serialize them.
+ *
+ * WHEN A SYNC STARTS:
+ * - If nothing is running, proceed.
+ * - If something is already running: wait until it's done.
+ * - If the running thing is a ping task: interrupt it.
+ *
+ * WHEN A SYNC ENDS:
+ * - If there are waiting syncs: signal one to proceed.
+ * - If there are no waiting syncs and this account is configured for push: start a ping.
+ * - Otherwise: This account is now idle.
+ *
+ * WHEN A PING TASK ENDS:
+ * - A ping task loops until either it's interrupted by a sync (in which case, there will be one or
+ *   more waiting syncs when the ping terminates), or encounters an error.
+ * - If there are waiting syncs, and we were interrupted: signal one to proceed.
+ * - If there are waiting syncs, but the ping terminated with an error: TODO: How to handle?
+ * - If there are no waiting syncs and this account is configured for push: This means the ping task
+ *   was terminated due to an error. Handle this by sending a sync request through the SyncManager
+ *   that doesn't actually do any syncing, and whose only effect is to restart the ping.
+ * - Otherwise: This account is now idle.
+ *
+ * WHEN AN ACCOUNT WANTS TO START OR CHANGE ITS PUSH BEHAVIOR:
+ * - If nothing is running, start a new ping task.
+ * - If a ping task is currently running, restart it with the new settings.
+ * - If a sync is currently running, do nothing.
+ *
+ * WHEN AN ACCOUNT WANTS TO STOP GETTING PUSH:
+ * - If nothing is running, do nothing.
+ * - If a ping task is currently running, interrupt it.
+ */
+public class PingSyncSynchronizer {
+
+    private static final String TAG = Eas.LOG_TAG;
+
+    /**
+     * This class handles bookkeeping for a single account.
+     */
+    private static class AccountSyncState {
+        /** The currently running {@link PingTask}, or null if we aren't in the middle of a Ping. */
+        private PingTask mPingTask;
+
+        /**
+         * The number of syncs that are blocked waiting for the current operation to complete.
+         * Unlike Pings, sync operations do not start their own tasks and are assumed to run in
+         * whatever thread calls into this class.
+         */
+        private int mSyncCount;
+
+        /** The condition on which to block syncs that need to wait. */
+        private Condition mCondition;
+
+        /**
+         *
+         * @param lock The lock from which to create our condition.
+         */
+        public AccountSyncState(final Lock lock) {
+            mPingTask = null;
+            mSyncCount = 0;
+            mCondition = lock.newCondition();
+        }
+
+        /**
+         * Update bookkeeping for a new sync:
+         * - Stop the Ping if there is one.
+         * - Wait until there's nothing running for this account before proceeding.
+         */
+        public void syncStart() {
+            ++mSyncCount;
+            if (mPingTask != null) {
+                // Syncs are higher priority than Ping -- terminate the Ping.
+                LogUtils.d(TAG, "Sync is pre-empting a ping");
+                mPingTask.stop();
+            }
+            if (mPingTask != null || mSyncCount > 1) {
+                // There’s something we need to wait for before we can proceed.
+                try {
+                    LogUtils.d(TAG, "Sync needs to wait: Ping: %s, Pending tasks: %d",
+                            mPingTask != null ? "yes" : "no", mSyncCount);
+                    mCondition.await();
+                } catch (final InterruptedException e) {
+                    // TODO: Handle this properly. Not catching it might be the right answer.
+                }
+            }
+        }
+
+        /**
+         * Update bookkeeping when a sync completes. This includes signaling pending ops to
+         * go ahead, or starting the ping if appropriate and there are no waiting ops.
+         * @param pushEnabled Whether this account is configured for push.
+         * @return Whether this account is now idle.
+         */
+        public boolean syncEnd(final boolean pushEnabled) {
+            --mSyncCount;
+            if (mSyncCount > 0) {
+                LogUtils.d(TAG, "Signalling a pending sync to proceed.");
+                mCondition.signal();
+                return false;
+            } else {
+                if (pushEnabled) {
+                    // TODO: Start the ping task
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Update bookkeeping when the ping task terminates, including signaling any waiting ops.
+         * @param pushEnabled Whether this account is configured for push.
+         * @return Whether this account is now idle.
+         */
+        public boolean pingEnd(final boolean pushEnabled) {
+            mPingTask = null;
+            if (mSyncCount > 0) {
+                mCondition.signal();
+                return false;
+            } else {
+                if (pushEnabled) {
+                    // TODO: request a push-only sync.
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Modifies or starts a ping for this account if no syncs are running.
+         */
+        public void pushModify() {
+            if (mSyncCount == 0) {
+                if (mPingTask == null) {
+                    // No ping, no running syncs -- start a new ping.
+                    // TODO: Fix this.
+                    //mPingTask = new PingTask();
+                    mPingTask.start();
+                } else {
+                    // Ping is already running, so tell it to restart to pick up any new params.
+                    mPingTask.restart();
+                }
+            }
+        }
+
+        /**
+         * Stop the currently running ping.
+         */
+        public void pushStop() {
+            if (mPingTask != null) {
+                mPingTask.stop();
+            }
+        }
+    }
+
+    /**
+     * Lock for access to {@link #mAccountStateMap}, also used to create the {@link Condition}s for
+     * each Account.
+     */
+    private final ReentrantLock mLock;
+
+    /**
+     * Map from account ID -> {@link AccountSyncState} for accounts with a running operation.
+     * An account is in this map only when this account is active, i.e. has a ping or sync running
+     * or pending. If an account is not in the middle of a sync and is not configured for push,
+     * it will not be here. This allows to use emptiness of this map to know whether the service
+     * needs to be running, and is also handy when debugging.
+     */
+    private final LongSparseArray<AccountSyncState> mAccountStateMap;
+
+    /** The {@link Service} that this object is managing. */
+    private final Service mService;
+
+    public PingSyncSynchronizer(final Service service) {
+        mLock = new ReentrantLock();
+        mAccountStateMap = new LongSparseArray<AccountSyncState>();
+        mService = service;
+    }
+
+    /**
+     * Gets the {@link AccountSyncState} for an account.
+     * The caller must hold {@link #mLock}.
+     * @param accountId The id for the account we're interested in.
+     * @param createIfNeeded If true, create the account state if it's not already there.
+     * @return The {@link AccountSyncState} for that account, or null if the account is idle and
+     *         createIfNeeded is false.
+     */
+    private AccountSyncState getAccountState(final long accountId, final boolean createIfNeeded) {
+        assert mLock.isHeldByCurrentThread();
+        AccountSyncState state = mAccountStateMap.get(accountId);
+        if (state == null && createIfNeeded) {
+            LogUtils.d(TAG, "PSS adding account state for %d", accountId);
+            state = new AccountSyncState(mLock);
+            mAccountStateMap.put(accountId, state);
+            // TODO: Is this too late to startService?
+            if (mAccountStateMap.size() == 1) {
+                LogUtils.i(TAG, "PSS added first account, starting service");
+                mService.startService(new Intent(mService, mService.getClass()));
+            }
+        }
+        return state;
+    }
+
+    /**
+     * Remove an account from the map. If this was the last account, then also stop this service.
+     * The caller must hold {@link #mLock}.
+     * @param accountId The id for the account we're removing.
+     */
+    private void removeAccount(final long accountId) {
+        assert mLock.isHeldByCurrentThread();
+        LogUtils.d(TAG, "PSS removing account state for %d", accountId);
+        mAccountStateMap.delete(accountId);
+        if (mAccountStateMap.size() == 0) {
+            LogUtils.i(TAG, "PSS removed last account; stopping service.");
+            mService.stopSelf();
+        }
+    }
+
+    public void syncStart(final long accountId) {
+        mLock.lock();
+        try {
+            LogUtils.d(TAG, "PSS syncStart for account %d", accountId);
+            final AccountSyncState accountState = getAccountState(accountId, true);
+            accountState.syncStart();
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    public void syncEnd(final long accountId, final boolean pushEnabled) {
+        mLock.lock();
+        try {
+            LogUtils.d(TAG, "PSS syncEnd for account %d", accountId);
+            final AccountSyncState accountState = getAccountState(accountId, false);
+            if (accountState == null) {
+                LogUtils.w(TAG, "PSS syncEnd for account %d but no state found", accountId);
+                return;
+            }
+            if (accountState.syncEnd(pushEnabled)) {
+                removeAccount(accountId);
+            }
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    public void pingEnd(final long accountId, final boolean pushEnabled) {
+        mLock.lock();
+        try {
+            LogUtils.d(TAG, "PSS pingEnd for account %d", accountId);
+            final AccountSyncState accountState = getAccountState(accountId, false);
+            if (accountState == null) {
+                LogUtils.w(TAG, "PSS pingEnd for account %d but no state found", accountId);
+                return;
+            }
+            if (accountState.pingEnd(pushEnabled)) {
+                removeAccount(accountId);
+            }
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    public void pushModify(final long accountId) {
+        mLock.lock();
+        try {
+            LogUtils.d(TAG, "PSS pushModify for account %d", accountId);
+            final AccountSyncState accountState = getAccountState(accountId, true);
+            accountState.pushModify();
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    public void pushStop(final long accountId) {
+        mLock.lock();
+        try {
+            LogUtils.d(TAG, "PSS pushStop for account %d", accountId);
+            final AccountSyncState accountState = getAccountState(accountId, false);
+            if (accountState != null) {
+                accountState.pushStop();
+            }
+        } finally {
+            mLock.unlock();
+        }
+    }
+}
diff --git a/src/com/android/exchange/service/PingTask.java b/src/com/android/exchange/service/PingTask.java
index 768727c..a016556 100644
--- a/src/com/android/exchange/service/PingTask.java
+++ b/src/com/android/exchange/service/PingTask.java
@@ -31,15 +31,28 @@
  */
 public class PingTask extends AsyncTask<Void, Void, Void> {
     private final EasPing mOperation;
+    // TODO: Transition away from mSyncHandlerMap -> mPingSyncSynchronizer.
     private final EmailSyncAdapterService.SyncHandlerSynchronizer mSyncHandlerMap;
+    private final PingSyncSynchronizer mPingSyncSynchronizer;
 
     private static final String TAG = Eas.LOG_TAG;
 
     public PingTask(final Context context, final Account account,
             final android.accounts.Account amAccount,
             final EmailSyncAdapterService.SyncHandlerSynchronizer syncHandlerMap) {
+        assert syncHandlerMap != null;
         mOperation = new EasPing(context, account, amAccount);
         mSyncHandlerMap = syncHandlerMap;
+        mPingSyncSynchronizer = null;
+    }
+
+    public PingTask(final Context context, final Account account,
+            final android.accounts.Account amAccount,
+            final PingSyncSynchronizer pingSyncSynchronizer) {
+        assert pingSyncSynchronizer != null;
+        mOperation = new EasPing(context, account, amAccount);
+        mSyncHandlerMap = null;
+        mPingSyncSynchronizer = pingSyncSynchronizer;
     }
 
     /** Start the ping loop. */
@@ -74,8 +87,13 @@
         }
         LogUtils.i(TAG, "Ping task ending with status: %d", pingStatus);
 
-        mSyncHandlerMap.pingComplete(mOperation.getAmAccount(), mOperation.getAccountId(),
-                pingStatus);
+        if (mSyncHandlerMap != null) {
+            mSyncHandlerMap.pingComplete(mOperation.getAmAccount(), mOperation.getAccountId(),
+                    pingStatus);
+        } else {
+            // TODO: Fix the pushEnabled param.
+            mPingSyncSynchronizer.pingEnd(mOperation.getAccountId(), false);
+        }
         return null;
     }
 
@@ -84,7 +102,12 @@
         // TODO: This is also hacky, should have a separate result code at minimum.
         // If the ping is cancelled, make sure it reports something to the sync adapter.
         LogUtils.w(TAG, "Ping cancelled for %d", mOperation.getAccountId());
-        mSyncHandlerMap.pingComplete(mOperation.getAmAccount(), mOperation.getAccountId(),
-                EasOperation.RESULT_REQUEST_FAILURE);
+        if (mSyncHandlerMap != null) {
+            mSyncHandlerMap.pingComplete(mOperation.getAmAccount(), mOperation.getAccountId(),
+                    EasOperation.RESULT_REQUEST_FAILURE);
+        } else {
+            // TODO: Fix the pushEnabled param.
+            mPingSyncSynchronizer.pingEnd(mOperation.getAccountId(), false);
+        }
     }
 }
diff --git a/tests/src/com/android/exchange/service/PingSyncSynchronizerTest.java b/tests/src/com/android/exchange/service/PingSyncSynchronizerTest.java
new file mode 100644
index 0000000..40235d8
--- /dev/null
+++ b/tests/src/com/android/exchange/service/PingSyncSynchronizerTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 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.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.test.AndroidTestCase;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class PingSyncSynchronizerTest extends AndroidTestCase {
+
+    private static class StubService extends Service {
+        @Override
+        public IBinder onBind(final Intent intent) {
+            return null;
+        }
+    }
+
+}