Make validation an operation.

This entails also making an operation for getting the
protocol version.

Change-Id: I8b2855cca338e3dbd8b6826b2c428b9e418a675e
diff --git a/src/com/android/exchange/eas/EasFolderSync.java b/src/com/android/exchange/eas/EasFolderSync.java
index 93d6141..28eb77e 100644
--- a/src/com/android/exchange/eas/EasFolderSync.java
+++ b/src/com/android/exchange/eas/EasFolderSync.java
@@ -20,8 +20,11 @@
 import android.content.SyncResult;
 import android.os.Bundle;
 
+import com.android.emailcommon.mail.MessagingException;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.service.EmailServiceProxy;
 import com.android.exchange.CommandStatusException;
 import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.FolderSyncParser;
@@ -38,6 +41,7 @@
  * during account adding flow as a convenient command to validate the account settings (e.g. since
  * it needs to login and will tell us about provisioning requirements).
  * TODO: Doing validation here is kind of wonky. There must be a better way.
+ * TODO: Add the use of the Settings command during validation.
  *
  * See http://msdn.microsoft.com/en-us/library/ee237648(v=exchg.80).aspx for more details.
  */
@@ -57,6 +61,9 @@
     /** 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;
+
     /**
      * Constructor for actually doing folder sync.
      * @param context
@@ -66,6 +73,7 @@
         super(context, account);
         mAccount = account;
         mStatusOnly = false;
+        mPolicy = null;
     }
 
     /**
@@ -99,17 +107,36 @@
 
     /**
      * Perform account validation.
-     * TODO: Implement correctly.
-     * @param bundle The {@link Bundle} to provide the results of validation to the UI.
-     * @return A result code, either from above or from the base class.
+     * @return The response {@link Bundle} expected by the RPC.
      */
-    public int validate(final Bundle bundle) {
-        if (!mStatusOnly || bundle == null) {
-            return RESULT_WRONG_OPERATION;
+    public Bundle validate() {
+        final Bundle bundle = new Bundle(3);
+        if (!mStatusOnly) {
+            writeResultCode(bundle, RESULT_OTHER_FAILURE);
+            return bundle;
         }
         LogUtils.i(LOG_TAG, "Performing validation");
-        final int result = performOperation(null);
-        return RESULT_OK;
+
+        if (!registerClientCert()) {
+            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
+                    MessagingException.CLIENT_CERTIFICATE_ERROR);
+            return bundle;
+        }
+
+        if (shouldGetProtocolVersion()) {
+            final EasOptions options = new EasOptions(this);
+            final int result = options.getProtocolVersionFromServer(null);
+            if (result != EasOptions.RESULT_OK) {
+                writeResultCode(bundle, result);
+                return bundle;
+            }
+            final String protocolVersion = options.getProtocolVersionString();
+            setProtocolVersion(protocolVersion);
+            bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION, protocolVersion);
+        }
+
+        writeResultCode(bundle, performOperation(null));
+        return bundle;
     }
 
     @Override
@@ -152,4 +179,70 @@
     protected boolean handleForbidden() {
         return mStatusOnly;
     }
+
+    @Override
+    protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
+        if (mStatusOnly) {
+            final EasProvision provisionOperation = new EasProvision(this);
+            mPolicy = provisionOperation.test();
+            // Regardless of whether the policy is supported, we return false because there's
+            // no need to re-run the operation.
+            return false;
+        }
+        return super.handleProvisionError(syncResult, accountId);
+    }
+
+    /**
+     * Translate {@link EasOperation} result codes to the values needed by the RPC, and write
+     * them to the {@link Bundle}.
+     * @param bundle The {@link Bundle} to return to the RPC.
+     * @param resultCode The result code for this operation.
+     */
+    private void writeResultCode(final Bundle bundle, final int resultCode) {
+        final int messagingExceptionCode;
+        switch (resultCode) {
+            case RESULT_ABORT:
+                messagingExceptionCode = MessagingException.IOERROR;
+                break;
+            case RESULT_RESTART:
+                messagingExceptionCode = MessagingException.IOERROR;
+                break;
+            case RESULT_TOO_MANY_REDIRECTS:
+                messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
+                break;
+            case RESULT_REQUEST_FAILURE:
+                messagingExceptionCode = MessagingException.IOERROR;
+                break;
+            case RESULT_FORBIDDEN:
+                messagingExceptionCode = MessagingException.ACCESS_DENIED;
+                break;
+            case RESULT_PROVISIONING_ERROR:
+                if (mPolicy == null) {
+                    messagingExceptionCode = MessagingException.SECURITY_POLICIES_REQUIRED;
+                    bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET, mPolicy);
+                } else {
+                    messagingExceptionCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
+                }
+                break;
+            case RESULT_AUTHENTICATION_ERROR:
+                messagingExceptionCode = MessagingException.AUTHENTICATION_FAILED;
+                break;
+            case RESULT_CLIENT_CERTIFICATE_REQUIRED:
+                messagingExceptionCode = MessagingException.CLIENT_CERTIFICATE_REQUIRED;
+                break;
+            case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
+                messagingExceptionCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
+                break;
+            case RESULT_OTHER_FAILURE:
+                messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
+                break;
+            case RESULT_OK:
+                messagingExceptionCode = MessagingException.NO_ERROR;
+                break;
+            default:
+                messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
+                break;
+        }
+        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, messagingExceptionCode);
+    }
 }
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index 6629667..c75e95f 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -17,8 +17,11 @@
 package com.android.exchange.eas;
 
 import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.SyncResult;
+import android.net.Uri;
 import android.os.Bundle;
 import android.text.format.DateUtils;
 
@@ -26,6 +29,7 @@
 import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.HostAuth;
 import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.utility.Utility;
 import com.android.exchange.Eas;
 import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.Serializer;
@@ -33,7 +37,7 @@
 import com.android.mail.utils.LogUtils;
 
 import org.apache.http.HttpEntity;
-import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.entity.ByteArrayEntity;
 
 import java.io.IOException;
@@ -78,8 +82,12 @@
     public static final int RESULT_PROVISIONING_ERROR = -6;
     /** Error code indicating an authentication problem. */
     public static final int RESULT_AUTHENTICATION_ERROR = -7;
+    /** Error code indicating the client is missing a certificate. */
+    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 some other failure. */
-    public static final int RESULT_OTHER_FAILURE = -8;
+    public static final int RESULT_OTHER_FAILURE = -10;
 
     protected final Context mContext;
 
@@ -91,6 +99,13 @@
     private final long mAccountId;
     private final EasServerConnection mConnection;
 
+    private EasOperation(final Context context, final long accountId,
+            final EasServerConnection connection) {
+        mContext = context;
+        mAccountId = accountId;
+        mConnection = connection;
+    }
+
     protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
         this(context, account.mId, new EasServerConnection(context, account, hostAuth));
     }
@@ -99,11 +114,13 @@
         this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv));
     }
 
-    protected EasOperation(final Context context, final long accountId,
-            final EasServerConnection connection) {
-        mContext = context;
-        mAccountId = accountId;
-        mConnection = connection;
+    /**
+     * This constructor is for use by operations that are created by other operations, e.g.
+     * {@link EasProvision}.
+     * @param parentOperation The {@link EasOperation} that is creating us.
+     */
+    protected EasOperation(final EasOperation parentOperation) {
+        this(parentOperation.mContext, parentOperation.mAccountId, parentOperation.mConnection);
     }
 
     /**
@@ -149,12 +166,10 @@
         int redirectCount = 0;
 
         do {
-            // Perform the POST and handle exceptions.
+            // Perform the HTTP request and handle exceptions.
             final EasResponse response;
             try {
-                final HttpPost post = mConnection.makePost(getRequestUri(), getRequestEntity(),
-                        getRequestContentType(), addPolicyKeyHeaderToRequest());
-                response = mConnection.executePost(post, getTimeout());
+                response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout());
             } catch (final IOException e) {
                 // If we were stopped, return the appropriate result code.
                 switch (mConnection.getStoppedReason()) {
@@ -173,7 +188,7 @@
                 return RESULT_REQUEST_FAILURE;
             } catch (final IllegalStateException e) {
                 // Subclasses use ISE to signal a hard error when building the request.
-                // TODO: If executePost can throw an ISE, we may want to tidy this up a bit.
+                // TODO: If executeHttpUriRequest can throw an ISE, we may want to tidy this up.
                 LogUtils.e(LOG_TAG, e, "Exception while sending request");
                 if (syncResult != null) {
                     syncResult.databaseError = true;
@@ -232,7 +247,9 @@
                     if (syncResult != null) {
                         ++syncResult.stats.numAuthExceptions;
                     }
-                    handleAuthError();
+                    if (response.isMissingCertificate()) {
+                        return RESULT_CLIENT_CERTIFICATE_REQUIRED;
+                    }
                     return RESULT_AUTHENTICATION_ERROR;
                 }
 
@@ -265,11 +282,42 @@
     }
 
     /**
-     * Handling for authentication errors. Should be the same for all operations.
-     * TODO: Implement.
+     * Reset the protocol version to use for this connection. If it's changed, and our account is
+     * persisted, also write back the changes to the DB.
+     * @param protocolVersion The new protocol version to use, as a string.
      */
-    private final void handleAuthError() {
+    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 ContentValues cv = new ContentValues(2);
+            if (getProtocolVersion() >= 12.0) {
+                final int oldFlags = Utility.getFirstRowInt(mContext, uri,
+                        Account.ACCOUNT_FLAGS_PROJECTION, null, null, null,
+                        Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0);
+                final int newFlags = oldFlags
+                        | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
+                if (oldFlags != newFlags) {
+                    cv.put(EmailContent.AccountColumns.FLAGS, newFlags);
+                }
+            }
+            cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion);
+            mContext.getContentResolver().update(uri, cv, null, null);
+        }
+    }
 
+    /**
+     * Create the request object for this operation.
+     * Most operations use a POST, but some use other request types (e.g. Options).
+     * @return An {@link HttpUriRequest}.
+     * @throws IOException
+     */
+    private final HttpUriRequest makeRequest() throws IOException {
+        final String requestUri = getRequestUri();
+        if (requestUri == null) {
+            return mConnection.makeOptions();
+        }
+        return mConnection.makePost(requestUri, getRequestEntity(),
+                getRequestContentType(), addPolicyKeyHeaderToRequest());
     }
 
     /**
@@ -286,8 +334,9 @@
     protected abstract String getCommand();
 
     /**
-     * Build the {@link HttpEntity} which us used to construct the POST. Typically this function
+     * Build the {@link HttpEntity} which is used to construct the POST. Typically this function
      * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}.
+     * If the subclass is not using a POST, then it should override this to return null.
      * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}.
      * @throws IOException
      */
@@ -359,7 +408,7 @@
      * @return
      */
     protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
-        final EasProvision provisionOperation = new EasProvision(mContext, accountId, mConnection);
+        final EasProvision provisionOperation = new EasProvision(this);
         return provisionOperation.provision(syncResult, accountId);
     }
 
@@ -375,6 +424,16 @@
     }
 
     /**
+     * Check whether we should ask the server what protocol versions it supports and set this
+     * account to use that version.
+     * @return Whether we need a new protocol version from the server.
+     */
+    protected final boolean shouldGetProtocolVersion() {
+        // TODO: Find conditions under which we should check other than not having one yet.
+        return !mConnection.isProtocolVersionSet();
+    }
+
+    /**
      * @return The protocol version to use.
      */
     protected final double getProtocolVersion() {
@@ -389,6 +448,13 @@
     }
 
     /**
+     * @return Whether we succeeeded in registering the client cert.
+     */
+    protected final boolean registerClientCert() {
+        return mConnection.registerClientCert();
+    }
+
+    /**
      * Convenience method for adding a Message to an account's outbox
      * @param account The {@link Account} from which to send the message.
      * @param msg the message to send
diff --git a/src/com/android/exchange/eas/EasOptions.java b/src/com/android/exchange/eas/EasOptions.java
new file mode 100644
index 0000000..7e44609
--- /dev/null
+++ b/src/com/android/exchange/eas/EasOptions.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013 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.eas;
+
+import android.content.SyncResult;
+
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.mail.utils.LogUtils;
+import com.google.common.collect.Sets;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+
+import java.util.HashSet;
+
+/**
+ * Performs an HTTP Options request to the Exchange server, in order to get the protocol
+ * version.
+ */
+public class EasOptions extends EasOperation {
+    private static final String LOG_TAG = "EasOptions";
+
+    /** Result code indicating we successfully got a protocol version. */
+    public static final int RESULT_OK = 1;
+
+    /** Set of Exchange protocol versions we understand. */
+    private static final HashSet<String> SUPPORTED_PROTOCOL_VERSIONS = Sets.newHashSet(
+            Eas.SUPPORTED_PROTOCOL_EX2003,
+            Eas.SUPPORTED_PROTOCOL_EX2007, Eas.SUPPORTED_PROTOCOL_EX2007_SP1,
+            Eas.SUPPORTED_PROTOCOL_EX2010, Eas.SUPPORTED_PROTOCOL_EX2010_SP1);
+
+    private String mProtocolVersion = null;
+
+    public EasOptions(final EasOperation parentOperation) {
+        super(parentOperation);
+    }
+
+    /**
+     * Perform the server request. If successful, callers should use
+     * {@link #getProtocolVersionString} to get the actual protocol version value.
+     * @param syncResult The {@link SyncResult} to use for this operation.
+     * @return A result code; {@link #RESULT_OK} is the only value that indicates success.
+     */
+    public int getProtocolVersionFromServer(final SyncResult syncResult) {
+        return performOperation(syncResult);
+    }
+
+    /**
+     * @return The protocol version to use, or null if we did not successfully get one.
+     */
+    public String getProtocolVersionString() {
+        return mProtocolVersion;
+    }
+
+    @Override
+    protected String getCommand() {
+        return null;
+    }
+
+    @Override
+    protected HttpEntity getRequestEntity() {
+        return null;
+    }
+
+    @Override
+    protected int handleResponse(final EasResponse response, final SyncResult syncResult) {
+        final Header commands = response.getHeader("MS-ASProtocolCommands");
+        final Header versions = response.getHeader("ms-asprotocolversions");
+        final boolean hasProtocolVersion;
+        if (commands == null || versions == null) {
+            LogUtils.e(LOG_TAG, "OPTIONS response without commands or versions");
+            hasProtocolVersion = false;
+        } else {
+            mProtocolVersion = getProtocolVersionFromHeader(versions);
+            hasProtocolVersion = (mProtocolVersion != null);
+        }
+        if (!hasProtocolVersion) {
+            return RESULT_PROTOCOL_VERSION_UNSUPPORTED;
+        }
+
+        return RESULT_OK;
+    }
+
+    @Override
+    protected String getRequestUri() {
+        return null;
+    }
+
+    /**
+     * Find the best protocol version to use from the header.
+     * @param versionHeader The {@link Header} for the server's supported versions.
+     * @return The best protocol version we mutually support, or null if none found.
+     */
+    private String getProtocolVersionFromHeader(final Header versionHeader) {
+        // The string is a comma separated list of EAS versions in ascending order
+        // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
+        final String supportedVersions = versionHeader.getValue();
+        LogUtils.i(LOG_TAG, "Server supports versions: %s", supportedVersions);
+        final String[] supportedVersionsArray = supportedVersions.split(",");
+        // Find the most recent version we support
+        String newProtocolVersion = null;
+        for (final String version: supportedVersionsArray) {
+            if (SUPPORTED_PROTOCOL_VERSIONS.contains(version)) {
+                newProtocolVersion = version;
+            }
+        }
+        return newProtocolVersion;
+    }
+}
diff --git a/src/com/android/exchange/eas/EasProvision.java b/src/com/android/exchange/eas/EasProvision.java
index 4b6c1eb..80e8a86 100644
--- a/src/com/android/exchange/eas/EasProvision.java
+++ b/src/com/android/exchange/eas/EasProvision.java
@@ -16,7 +16,6 @@
 
 package com.android.exchange.eas;
 
-import android.content.Context;
 import android.content.SyncResult;
 import android.os.Build;
 
@@ -27,7 +26,6 @@
 import com.android.exchange.adapter.ProvisionParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
-import com.android.exchange.service.EasServerConnection;
 import com.android.mail.utils.LogUtils;
 
 import org.apache.http.HttpEntity;
@@ -92,9 +90,8 @@
      */
     private int mPhase;
 
-    public EasProvision(final Context context, final long accountId,
-            final EasServerConnection connection) {
-        super(context, accountId, connection);
+    public EasProvision(final EasOperation parentOperation) {
+        super(parentOperation);
         mPolicy = null;
         mPolicyKey = null;
         mStatus = null;
@@ -119,17 +116,22 @@
 
     /**
      * Make the provisioning calls to determine if we can handle the required policy.
-     * @return Whether we can support the required policy.
+     * @return The {@link Policy} if we support it, or null otherwise.
      */
-    public final boolean test() {
+    public final Policy test() {
         int result = performInitialRequest(null);
         if (result == RESULT_POLICY_UNSUPPORTED) {
             // Check if the server will permit partial policies.
             result = performAckRequest(null, true);
         }
-        return result == RESULT_POLICY_SUPPORTED;
+        if (result == RESULT_POLICY_SUPPORTED) {
+            return mPolicy;
+        }
+        return null;
     }
 
+
+
     /**
      * Get the required policy from the server and enforce it.
      * @param syncResult The {@link SyncResult}, if anym for this operation.
diff --git a/src/com/android/exchange/service/EasAccountSyncHandler.java b/src/com/android/exchange/service/EasAccountSyncHandler.java
deleted file mode 100644
index 3bf0b16..0000000
--- a/src/com/android/exchange/service/EasAccountSyncHandler.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.android.exchange.service;
-
-import android.content.Context;
-
-import com.android.emailcommon.provider.Account;
-
-
-/**
- * Performs an Exchange Account sync, which includes folder sync.
- */
-public class EasAccountSyncHandler extends EasAccountValidator {
-    public EasAccountSyncHandler(final Context context, final Account account) {
-        super(context, account);
-    }
-
-    public void performSync() {
-        doValidationOrSync(null);
-    }
-}
diff --git a/src/com/android/exchange/service/EasServerConnection.java b/src/com/android/exchange/service/EasServerConnection.java
index 72716fc..0ac9a8a 100644
--- a/src/com/android/exchange/service/EasServerConnection.java
+++ b/src/com/android/exchange/service/EasServerConnection.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2013 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.content.ContentResolver;
@@ -28,6 +44,7 @@
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpOptions;
 import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.params.BasicHttpParams;
@@ -36,6 +53,7 @@
 
 import java.io.IOException;
 import java.net.URI;
+import java.security.cert.CertificateException;
 
 /**
  * Base class for communicating with an EAS server. Anything that needs to send messages to the
@@ -92,17 +110,17 @@
     protected final Account mAccount;
     private final long mAccountId;
 
-    // Bookkeeping for interrupting a POST. This is primarily for use by Ping (there's currently
+    // Bookkeeping for interrupting a request. This is primarily for use by Ping (there's currently
     // no mechanism for stopping a sync).
     // Access to these variables should be synchronized on this.
-    private HttpPost mPendingPost = null;
+    private HttpUriRequest mPendingRequest = null;
     private boolean mStopped = false;
     private int mStoppedReason = STOPPED_REASON_NONE;
 
-    /**
-     * The protocol version to use, as a double.
-     */
+    /** The protocol version to use, as a double. */
     private double mProtocolVersion = 0.0d;
+    /** Whether {@link #setProtocolVersion} was last called with a non-null value. */
+    private boolean mProtocolVersionIsSet = false;
 
     /**
      * The client for any requests made by this object. This is created lazily, and cleared
@@ -198,12 +216,16 @@
     /**
      * If a sync causes us to update our protocol version, this function must be called so that
      * subsequent calls to {@link #getProtocolVersion()} will do the right thing.
+     * @return Whether the protocol version changed.
      */
-    protected void setProtocolVersion(String protocolVersionString) {
+    public boolean setProtocolVersion(String protocolVersionString) {
         if (protocolVersionString == null) {
             protocolVersionString = Eas.DEFAULT_PROTOCOL_VERSION;
         }
+        mProtocolVersionIsSet = (protocolVersionString != null);
+        final double oldProtocolVersion = mProtocolVersion;
         mProtocolVersion = Eas.getProtocolVersionDouble(protocolVersionString);
+        return (oldProtocolVersion != mProtocolVersion);
     }
 
     /**
@@ -282,6 +304,17 @@
     }
 
     /**
+     * Make an {@link HttpOptions} request for this connection.
+     * @return The {@link HttpOptions} object.
+     */
+    public HttpOptions makeOptions() {
+        final HttpOptions method = new HttpOptions(URI.create(makeBaseUriString()));
+        method.setHeader("Authorization", makeAuthString());
+        method.setHeader("User-Agent", getUserAgent());
+        return method;
+    }
+
+    /**
      * Send a POST request to the server.
      * @param cmd The command we're sending to the server.
      * @param entity The {@link HttpEntity} containing the payload of the message.
@@ -331,7 +364,7 @@
         if (isPingCommand) {
             method.setHeader("Connection", "close");
         }
-        return executePost(method, timeout);
+        return executeHttpUriRequest(method, timeout);
     }
 
     public EasResponse sendHttpClientPost(final String cmd, final byte[] bytes,
@@ -351,7 +384,7 @@
     }
 
     /**
-     * Executes an {@link HttpPost}.
+     * Executes an {@link HttpUriRequest}.
      * Note: this function must not be called by multiple threads concurrently. Only one thread may
      * send server requests from a particular object at a time.
      * @param method The post to execute.
@@ -359,7 +392,7 @@
      * @return The response from the Exchange server.
      * @throws IOException
      */
-    public EasResponse executePost(final HttpPost method, final long timeout)
+    public EasResponse executeHttpUriRequest(final HttpUriRequest method, final long timeout)
             throws IOException {
         // The synchronized blocks are here to support the stop() function, specifically to handle
         // when stop() is called first. Notably, they are NOT here in order to guard against
@@ -372,7 +405,7 @@
                 // callers can equate IOException with "this POST got killed for some reason".
                 throw new IOException("Command was stopped before POST");
             }
-           mPendingPost = method;
+           mPendingRequest = method;
         }
         boolean postCompleted = false;
         try {
@@ -382,7 +415,7 @@
             return response;
         } finally {
             synchronized (this) {
-                mPendingPost = null;
+                mPendingRequest = null;
                 if (postCompleted) {
                     mStoppedReason = STOPPED_REASON_NONE;
                 }
@@ -391,7 +424,7 @@
     }
 
     protected EasResponse executePost(final HttpPost method) throws IOException {
-        return executePost(method, COMMAND_TIMEOUT);
+        return executeHttpUriRequest(method, COMMAND_TIMEOUT);
     }
 
     /**
@@ -407,11 +440,11 @@
     public synchronized void stop(final int reason) {
         // Only process legitimate reasons.
         if (reason >= STOPPED_REASON_ABORT && reason <= STOPPED_REASON_RESTART) {
-            final boolean isMidPost = (mPendingPost != null);
+            final boolean isMidPost = (mPendingRequest != null);
             LogUtils.i(TAG, "%s with reason %d", (isMidPost ? "Interrupt" : "Stop next"), reason);
             mStoppedReason = reason;
             if (isMidPost) {
-                mPendingPost.abort();
+                mPendingRequest.abort();
             } else {
                 mStopped = true;
             }
@@ -428,6 +461,30 @@
     }
 
     /**
+     * Try to register our client certificate, if needed.
+     * @return True if we succeeded or didn't need a client cert, false if we failed to register it.
+     */
+    public boolean registerClientCert() {
+        if (mHostAuth.mClientCertAlias != null) {
+            try {
+                getClientConnectionManager().registerClientCert(mContext, mHostAuth);
+            } catch (final CertificateException e) {
+                // The client certificate the user specified is invalid/inaccessible.
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @return Whether {@link #setProtocolVersion} was last called with a non-null value. Note that
+     *         at construction time it is set to whatever protocol version is in the account.
+     */
+    public boolean isProtocolVersionSet() {
+        return mProtocolVersionIsSet;
+    }
+
+    /**
      * Convenience method for adding a Message to an account's outbox
      * @param account The {@link Account} from which to send the message.
      * @param msg The message to send
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index 29e5d6b..c8b1d5a 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -254,7 +254,7 @@
         @Override
         public Bundle validate(final HostAuth hostAuth) {
             LogUtils.d(TAG, "IEmailService.validate");
-            return new EasAccountValidator(EmailSyncAdapterService.this, hostAuth).validate();
+            return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).validate();
         }
 
         @Override