Merge "Update EasPing durations" into jb-ub-mail-ur10
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 084c0c4..b90af09 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.exchange"
-    android:versionCode="500044" >
+    android:versionCode="500045" >
 
     <uses-permission
         android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
index 7b69b06..5eb892c 100644
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -22,7 +22,7 @@
 
 import com.android.emailcommon.provider.Policy;
 import com.android.exchange.R;
-import com.android.exchange.service.EasAccountValidator;
+import com.android.exchange.eas.EasProvision;
 import com.android.mail.utils.LogUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -543,7 +543,7 @@
                     LogUtils.i(TAG, "Policy status: %s", getValue());
                     break;
                 case Tags.PROVISION_DATA:
-                    if (policyType.equalsIgnoreCase(EasAccountValidator.EAS_2_POLICY_TYPE)) {
+                    if (policyType.equalsIgnoreCase(EasProvision.EAS_2_POLICY_TYPE)) {
                         // Parse the old style XML document
                         parseProvisionDocXml(getValue());
                     } else {
diff --git a/src/com/android/exchange/eas/EasFolderSync.java b/src/com/android/exchange/eas/EasFolderSync.java
index 93d6141..d5ba574 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,72 @@
     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.UNSPECIFIED_EXCEPTION;
+                } else {
+                    bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET, mPolicy);
+                    messagingExceptionCode = mPolicy.mProtocolPoliciesUnsupported == null ?
+                            MessagingException.SECURITY_POLICIES_REQUIRED :
+                            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..1485e78 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -17,8 +17,12 @@
 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.Build;
 import android.os.Bundle;
 import android.text.format.DateUtils;
 
@@ -26,14 +30,16 @@
 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;
+import com.android.exchange.adapter.Tags;
 import com.android.exchange.service.EasServerConnection;
 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 +84,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 +101,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 +116,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 +168,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 +190,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 +249,9 @@
                     if (syncResult != null) {
                         ++syncResult.stats.numAuthExceptions;
                     }
-                    handleAuthError();
+                    if (response.isMissingCertificate()) {
+                        return RESULT_CLIENT_CERTIFICATE_REQUIRED;
+                    }
                     return RESULT_AUTHENTICATION_ERROR;
                 }
 
@@ -265,11 +284,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 +336,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 +410,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 +426,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 +450,31 @@
     }
 
     /**
+     * @return Whether we succeeeded in registering the client cert.
+     */
+    protected final boolean registerClientCert() {
+        return mConnection.registerClientCert();
+    }
+
+    /**
+     * Add the device information to the current request.
+     * @param s The {@link Serializer} for our current request.
+     * @throws IOException
+     */
+    protected final void addDeviceInformationToSerlializer(final Serializer s) throws IOException {
+        s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
+        s.data(Tags.SETTINGS_MODEL, Build.MODEL);
+        //s.data(Tags.SETTINGS_IMEI, "");
+        //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
+        s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
+        //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
+        //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
+        //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
+        s.data(Tags.SETTINGS_USER_AGENT, getUserAgent());
+        s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
+    }
+
+    /**
      * 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..aa9484e 100644
--- a/src/com/android/exchange/eas/EasProvision.java
+++ b/src/com/android/exchange/eas/EasProvision.java
@@ -16,9 +16,7 @@
 
 package com.android.exchange.eas;
 
-import android.content.Context;
 import android.content.SyncResult;
-import android.os.Build;
 
 import com.android.emailcommon.provider.Policy;
 import com.android.emailcommon.service.PolicyServiceProxy;
@@ -27,7 +25,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;
@@ -52,9 +49,9 @@
     private static final String LOG_TAG = "EasProvision";
 
     /** The policy type for versions of EAS prior to 2007. */
-    private static final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
+    public static final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
     /** The policy type for versions of EAS starting with 2007. */
-    private static final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
+    public static final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
 
     /** The EAS protocol Provision status for "we implement all of the policies" */
     private static final String PROVISION_STATUS_OK = "1";
@@ -92,9 +89,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,15 +115,20 @@
 
     /**
      * 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) {
+            // The server is ok with us not supporting everything, so clear the unsupported ones.
+            mPolicy.mProtocolPoliciesUnsupported = null;
+        }
+        return (result == RESULT_POLICY_SUPPORTED || result == RESULT_POLICY_UNSUPPORTED)
+                ? mPolicy : null;
     }
 
     /**
@@ -165,6 +166,20 @@
 
         // Write the final policy key to the Account.
         PolicyServiceProxy.setAccountPolicy(mContext, accountId, mPolicy, mPolicyKey);
+
+        // For 12.1 and 14.0, after provisioning we need to also send the device information via
+        // the Settings command.
+        // See the comments for EasSettings for more details.
+        final double version = getProtocolVersion();
+        if (version == Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE
+                || version == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+            final EasSettings settingsOperation = new EasSettings(this);
+            if (!settingsOperation.sendDeviceInformation(syncResult)) {
+                // If the Settings command failed, so do we.
+                return false;
+            }
+        }
+
         return true;
     }
 
@@ -181,16 +196,7 @@
         // When requesting the policy in 14.1, we also need to send device information.
         if (mPhase == PHASE_INITIAL &&
                 getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
-            s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
-            s.data(Tags.SETTINGS_MODEL, Build.MODEL);
-            //s.data(Tags.SETTINGS_IMEI, "");
-            //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
-            s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
-            //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
-            //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
-            //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
-            s.data(Tags.SETTINGS_USER_AGENT, getUserAgent());
-            s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
+            addDeviceInformationToSerlializer(s);
         }
         s.start(Tags.PROVISION_POLICIES);
         s.start(Tags.PROVISION_POLICY);
diff --git a/src/com/android/exchange/eas/EasSettings.java b/src/com/android/exchange/eas/EasSettings.java
new file mode 100644
index 0000000..09169c1
--- /dev/null
+++ b/src/com/android/exchange/eas/EasSettings.java
@@ -0,0 +1,77 @@
+/*
+ * 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.EasResponse;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.SettingsParser;
+import com.android.exchange.adapter.Tags;
+
+import org.apache.http.HttpEntity;
+
+import java.io.IOException;
+
+/**
+ * Performs an Exchange Settings request to the server to communicate our device information.
+ * While the settings command can be used for all sorts of things, we currently only use it to
+ * notify the server of our device information after a Provision command, and only for certain
+ * versions of the protocol (12.1 and 14.0; versions after 14.0 instead specify the device info
+ * in the provision command).
+ *
+ * See http://msdn.microsoft.com/en-us/library/ee202944(v=exchg.80).aspx for details on the Settings
+ * command in general.
+ * See http://msdn.microsoft.com/en-us/library/gg675476(v=exchg.80).aspx for details on the
+ * requirement for communicating device info for some versions of Exchange.
+ */
+public class EasSettings extends EasOperation {
+
+
+    /** Result code indicating the Settings command succeeded. */
+    private static final int RESULT_OK = 1;
+
+    public EasSettings(final EasOperation parentOperation) {
+        super(parentOperation);
+    }
+
+    public boolean sendDeviceInformation(final SyncResult syncResult) {
+        return performOperation(syncResult) == RESULT_OK;
+    }
+
+    @Override
+    protected String getCommand() {
+        return "Settings";
+    }
+
+    @Override
+    protected HttpEntity getRequestEntity() throws IOException {
+        final Serializer s = new Serializer();
+        s.start(Tags.SETTINGS_SETTINGS);
+        addDeviceInformationToSerlializer(s);
+        s.end().done();
+        return makeEntity(s);
+    }
+
+    @Override
+    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
+            throws IOException {
+        return new SettingsParser(response.getInputStream()).parse()
+                ? RESULT_OK : RESULT_OTHER_FAILURE;
+    }
+
+}
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/EasAccountValidator.java b/src/com/android/exchange/service/EasAccountValidator.java
deleted file mode 100644
index b06978f..0000000
--- a/src/com/android/exchange/service/EasAccountValidator.java
+++ /dev/null
@@ -1,629 +0,0 @@
-package com.android.exchange.service;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Policy;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.PolicyServiceProxy;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.CommandStatusException.CommandStatus;
-import com.android.exchange.Eas;
-import com.android.exchange.EasResponse;
-import com.android.exchange.adapter.FolderSyncParser;
-import com.android.exchange.adapter.ProvisionParser;
-import com.android.exchange.adapter.Serializer;
-import com.android.exchange.adapter.SettingsParser;
-import com.android.exchange.adapter.Tags;
-import com.android.mail.utils.LogUtils;
-import com.google.common.collect.Sets;
-
-import org.apache.http.Header;
-import org.apache.http.HttpStatus;
-
-import java.io.IOException;
-import java.security.cert.CertificateException;
-import java.util.HashSet;
-
-/**
- * Base class to perform the various requests needed to validate or sync an account.
- * "Account sync" consists primarily of syncing all folders for this account, but also includes
- * handling the protocol version, security policies, and other authentication issues.
- */
-public class EasAccountValidator extends EasServerConnection {
-    /** Logging tag. */
-    private static final String TAG = "EasAccountValidator";
-
-    /**
-     * The maximum number of redirects we permit before giving up. Ideally the server should not
-     * send us on a chase like this, so this is here to prevent infinite recursion in a bad case.
-     */
-    private static final int MAX_REDIRECTS = 3;
-
-    public static final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
-    public static final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
-
-    /** The EAS protocol Provision status for "we implement all of the policies" */
-    private static final String PROVISION_STATUS_OK = "1";
-    /** The EAS protocol Provision status meaning "we partially implement the policies" */
-    private static final String PROVISION_STATUS_PARTIAL = "2";
-
-    /** 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);
-
-    /** The number of times we've been redirected so far. */
-    private int mRedirectCount;
-
-    /**
-     * An exception type used exclusively within this class -- some sub-functions throw this to
-     * signal that a response from the Exchange server indicated that we should be using a different
-     * host. This exception is caught
-     */
-    private static class RedirectException extends Exception {
-        public final String mRedirectAddress;
-        public RedirectException(final EasResponse resp) {
-            mRedirectAddress = resp.getRedirectAddress();
-        }
-    }
-
-    private EasAccountValidator(final Context context, final Account account,
-            final HostAuth hostAuth) {
-        super(context, account, hostAuth);
-        mRedirectCount = 0;
-    }
-
-    protected EasAccountValidator(final Context context, final Account account) {
-        this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv));
-    }
-
-    public EasAccountValidator(final Context context, final HostAuth hostAuth) {
-        this(context, new Account(), hostAuth);
-        mAccount.mEmailAddress = hostAuth.mLogin;
-    }
-
-    /**
-     * Update our account's protocol version based on the server's supported versions.
-     * @param versionHeader The {@link Header} for the server's supported versions.
-     * @return Whether we found a suitable protocol version.
-     */
-    private boolean setProtocolVersion(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(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;
-            }
-        }
-        if (newProtocolVersion == null) {
-            LogUtils.w(TAG, "No supported EAS versions: %s", supportedVersions);
-            // TODO: if mAccount.isSaved(), we should delete the account.
-            return false;
-        }
-
-        // Update our account with the new protocol version.
-        final boolean protocolChanged = !newProtocolVersion.equals(mAccount.mProtocolVersion);
-        if (protocolChanged) {
-            mAccount.mProtocolVersion = newProtocolVersion;
-            setProtocolVersion(newProtocolVersion);
-        }
-
-        // Fixup search flags, if they're not set.
-        final boolean flagsChanged;
-        if (getProtocolVersion() >= 12.0) {
-            int oldFlags = mAccount.mFlags;
-            mAccount.mFlags |= Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
-            flagsChanged = (oldFlags != mAccount.mFlags);
-        } else {
-            flagsChanged = false;
-        }
-
-        // Write account back to DB if needed.
-        if ((protocolChanged || flagsChanged) && mAccount.isSaved()) {
-            final ContentValues cv = new ContentValues();
-            if (protocolChanged) {
-                cv.put(AccountColumns.PROTOCOL_VERSION, mAccount.mProtocolVersion);
-            }
-            if (flagsChanged) {
-                cv.put(AccountColumns.FLAGS, mAccount.mFlags);
-            }
-            mAccount.update(mContext, cv);
-        }
-        return true;
-    }
-
-    /**
-     * Make an OPTIONS request to determine the protocol version to use, and update our account to
-     * use the most recent protocol that both we and the server understand.
-     * @return A status code for getting the protocol version. If NO_ERROR, then mAccount will be
-     *     updated to the best version we mutually understand.
-     */
-    private int getServerProtocolVersion() throws IOException, RedirectException {
-        final EasResponse resp = sendHttpClientOptions();
-        try {
-            final int code = resp.getStatus();
-            LogUtils.d(TAG, "Validation (OPTIONS) response: %d", code);
-            if (code == HttpStatus.SC_OK) {
-                // No exception means successful validation
-                final Header commands = resp.getHeader("MS-ASProtocolCommands");
-                final Header versions = resp.getHeader("ms-asprotocolversions");
-                final boolean hasProtocolVersion;
-                if (commands == null || versions == null) {
-                    LogUtils.e(TAG, "OPTIONS response without commands or versions");
-                    hasProtocolVersion = false;
-                } else {
-                    hasProtocolVersion = setProtocolVersion(versions);
-                }
-                if (!hasProtocolVersion) {
-                    return MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
-                }
-                return MessagingException.NO_ERROR;
-            }
-            if (resp.isAuthError()) {
-                return resp.isMissingCertificate()
-                        ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
-                        : MessagingException.AUTHENTICATION_FAILED;
-            }
-            if (code == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
-                return MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
-            }
-            if (resp.isRedirectError()) {
-                throw new RedirectException(resp);
-            }
-            // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
-            LogUtils.w(TAG, "Validation failed, reporting I/O error: %d", code);
-            return MessagingException.IOERROR;
-        } finally {
-            resp.close();
-        }
-    }
-
-    /**
-     * Send a FolderSync request and handle the response. Depending on isStatusOnly, this either
-     * simply verifies that the response is valid, or it will also actually sync the folders.
-     * @param isStatusOnly If true, this is only a validation, otherwise it's a full sync.
-     * @return A status code indicating the result of this check.
-     * @throws IOException
-     * @throws CommandStatusException
-     * @throws RedirectException
-     */
-    private int doFolderSync(final boolean isStatusOnly)
-            throws IOException, CommandStatusException, RedirectException {
-        LogUtils.i(TAG, "FolderSync (%s) for %s, %s, ssl = %s", isStatusOnly ? "validate" : "sync",
-                mHostAuth.mAddress, mHostAuth.mLogin, mHostAuth.shouldUseSsl() ? "1" : "0");
-
-        // Send "0" as the sync key for new accounts; otherwise, use the current key
-        final String syncKey = mAccount.mSyncKey != null ? mAccount.mSyncKey : "0";
-        final Serializer s = new Serializer();
-        s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
-            .end().end().done();
-        final EasResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
-        final int resultCode;
-        try {
-            final int code = resp.getStatus();
-            LogUtils.d(TAG, "FolderSync response: %d", code);
-            if (code == HttpStatus.SC_OK) {
-                // We need to parse the result to see if we've got a provisioning issue
-                // (EAS 14.0 only)
-                if (!resp.isEmpty()) {
-                    new FolderSyncParser(mContext, mContext.getContentResolver(),
-                            resp.getInputStream(), mAccount, isStatusOnly).parse();
-                }
-                resultCode = MessagingException.NO_ERROR;
-            } else if (code == HttpStatus.SC_FORBIDDEN) {
-                // For validation only, we take 403 as ACCESS_DENIED (the account isn't
-                // authorized, possibly due to device type)
-                resultCode = MessagingException.ACCESS_DENIED;
-            } else if (resp.isProvisionError()) {
-                // The device needs to have security policies enforced
-                throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
-            } else if (code == HttpStatus.SC_NOT_FOUND) {
-                // We get a 404 from OWA addresses (which are NOT EAS addresses)
-                resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
-            } else if (code == HttpStatus.SC_UNAUTHORIZED) {
-                resultCode = resp.isMissingCertificate()
-                        ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
-                        : MessagingException.AUTHENTICATION_FAILED;
-            } else if (resp.isRedirectError()) {
-                throw new RedirectException(resp);
-            } else {
-                resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
-            }
-        } finally {
-            resp.close();
-        }
-        return resultCode;
-    }
-
-    /**
-     * Send a Settings request to the server and process the response.
-     * @return Whether the request succeeded.
-     * @throws IOException
-     */
-    private boolean sendSettings() throws IOException {
-        final Serializer s = new Serializer();
-        s.start(Tags.SETTINGS_SETTINGS);
-        s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
-        s.data(Tags.SETTINGS_MODEL, Build.MODEL);
-        s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
-        s.data(Tags.SETTINGS_USER_AGENT, getUserAgent());
-        s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS
-        final EasResponse resp = sendHttpClientPost("Settings", s.toByteArray());
-        try {
-            if (resp.getStatus() == HttpStatus.SC_OK) {
-                return new SettingsParser(resp.getInputStream()).parse();
-            }
-        } finally {
-            resp.close();
-        }
-        // On failures, simply return false
-        return false;
-    }
-
-    private String getPolicyType() {
-        return (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ?
-                EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
-    }
-
-    /**
-     * Acknowledge a remote wipe command from the server.
-     * @param tempKey The security key of our current (temporary) policy.
-     * @throws IOException
-     */
-    private void acknowledgeRemoteWipe(final String tempKey)
-            throws IOException {
-        acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
-    }
-
-    /**
-     * Acknowledge that we've set the required policy.
-     * @param tempKey The security key of our current (temporary) policy.
-     * @param result One of {@link #PROVISION_STATUS_OK} or {@link #PROVISION_STATUS_PARTIAL}
-     *               indicating how well we enforce the policy.
-     * @return A new security sync key, or null on failure.
-     * @throws IOException
-     */
-    private String acknowledgeProvision(final String tempKey, final String result)
-            throws IOException {
-        return acknowledgeProvisionImpl(tempKey, result, false);
-    }
-
-    /**
-     * Common function doing the work for acknowledging remote wipes or provisioning.
-     * @param tempKey The security key of our current (temporary) policy.
-     * @param status One of {@link #PROVISION_STATUS_OK} or {@link #PROVISION_STATUS_PARTIAL}
-     *               indicating how well we enforce the policy.
-     * @param remoteWipe Whether this is a remote wipe.
-     * @return A new security sync key, or null on failure.
-     * @throws IOException
-     */
-    private String acknowledgeProvisionImpl(final String tempKey, final String status,
-            final boolean remoteWipe) throws IOException {
-        final Serializer s = new Serializer();
-        s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
-        s.start(Tags.PROVISION_POLICY);
-
-        // Use the proper policy type, depending on EAS version
-        s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
-
-        s.data(Tags.PROVISION_POLICY_KEY, tempKey);
-        s.data(Tags.PROVISION_STATUS, status);
-        s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
-        if (remoteWipe) {
-            s.start(Tags.PROVISION_REMOTE_WIPE);
-            s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
-            s.end();
-        }
-        s.end().done(); // PROVISION_PROVISION
-        EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
-        try {
-            if (resp.getStatus() == HttpStatus.SC_OK) {
-                final ProvisionParser pp = new ProvisionParser(mContext, resp.getInputStream());
-                if (pp.parse()) {
-                    // Return the final policy key from the ProvisionParser
-                    final String result =
-                            (pp.getSecuritySyncKey() == null) ? "failed" : "confirmed";
-                    LogUtils.i(TAG, "Provision %s for %s set", result,
-                            PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL");
-                    return pp.getSecuritySyncKey();
-                }
-            }
-        } finally {
-            resp.close();
-        }
-        // On failures, log issue and return null
-        LogUtils.i(TAG, "Provisioning failed for %s set",
-                PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL");
-        return null;
-    }
-
-    /**
-     * Send an Exchange Provision request, and process the response to see if we can handle the
-     * provisioning requirements returned by the server.
-     * @return A {@link ProvisionParser} for the response, or null if we can't handle it.
-     * @throws IOException
-     */
-    private ProvisionParser canProvision() throws IOException {
-        final Serializer s = new Serializer();
-        s.start(Tags.PROVISION_PROVISION);
-        if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
-            // Send settings information in 14.1 and greater
-            s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
-            s.data(Tags.SETTINGS_MODEL, Build.MODEL);
-            //s.data(Tags.SETTINGS_IMEI, "");
-            //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
-            s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
-            //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
-            //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
-            //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
-            s.data(Tags.SETTINGS_USER_AGENT, getUserAgent());
-            s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
-        }
-        s.start(Tags.PROVISION_POLICIES);
-        s.start(Tags.PROVISION_POLICY);
-        s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
-        s.end().end().end().done(); // PROVISION_POLICY, PROVISION_POLICIES, PROVISION_PROVISION
-        final EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
-        try {
-            int code = resp.getStatus();
-            if (code == HttpStatus.SC_OK) {
-                final ProvisionParser pp = new ProvisionParser(mContext, resp.getInputStream());
-                if (pp.parse()) {
-                    // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
-                    // policies.  If others are required, hasSupportablePolicySet will be false
-                    if (pp.hasSupportablePolicySet() &&
-                            getProtocolVersion() == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
-                        // In EAS 14.0, we need the final security key in order to use the settings
-                        // command
-                        final String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
-                                PROVISION_STATUS_OK);
-                        if (policyKey != null) {
-                            pp.setSecuritySyncKey(policyKey);
-                        }
-                    } else if (!pp.hasSupportablePolicySet())  {
-                        // Try to acknowledge using the "partial" status (i.e. we can partially
-                        // accommodate the required policies).  The server will agree to this if the
-                        // "allow non-provisionable devices" setting is enabled on the server
-                        LogUtils.i(TAG, "PolicySet is NOT fully supportable");
-                        if (acknowledgeProvision(pp.getSecuritySyncKey(),
-                                PROVISION_STATUS_PARTIAL) != null) {
-                            // The server's ok with our inability to support policies, so we'll
-                            // clear them
-                            pp.clearUnsupportablePolicies();
-                        }
-                    }
-                    return pp;
-                }
-            }
-        } finally {
-            resp.close();
-        }
-
-        // On failures, simply return null
-        return null;
-    }
-
-    /**
-     * Process the provisioning requirements that's returned by the server in response to a
-     * Provision request.
-     * @param pp  A {@link ProvisionParser} for the server response to the Provision request.
-     * @return Whether we successfully handled the provisioning requirements.
-     * @throws IOException
-     */
-    private boolean tryProvision(final ProvisionParser pp) throws IOException {
-        if (pp == null) return false;
-        // Get the policies from ProvisionParser
-        final Policy policy = pp.getPolicy();
-        final Policy oldPolicy;
-        // Grab the old policy (if any)
-        if (mAccount.mPolicyKey > 0) {
-            oldPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
-        } else {
-            oldPolicy = null;
-        }
-        // Update the account with a null policyKey (the key we've gotten is
-        // temporary and cannot be used for syncing)
-        PolicyServiceProxy.setAccountPolicy(mContext, mAccount.mId, policy, null);
-        // Make sure mAccount is current (with latest policy key)
-        mAccount.refresh(mContext);
-        if (pp.getRemoteWipe()) {
-            // We've gotten a remote wipe command
-            LogUtils.i(TAG, "!!! Remote wipe request received");
-            // Start by setting the account to security hold
-            PolicyServiceProxy.setAccountHoldFlag(mContext, mAccount, true);
-
-            // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
-            // we wipe the device regardless of any errors in acknowledgment
-            try {
-                LogUtils.i(TAG, "!!! Acknowledging remote wipe to server");
-                acknowledgeRemoteWipe(pp.getSecuritySyncKey());
-            } catch (Exception e) {
-                // Because remote wipe is such a high priority task, we don't want to
-                // circumvent it if there's an exception in acknowledgment
-            }
-            // Then, tell SecurityPolicy to wipe the device
-            LogUtils.i(TAG, "!!! Executing remote wipe");
-            PolicyServiceProxy.remoteWipe(mContext);
-            return false;
-        } else if (pp.hasSupportablePolicySet() && PolicyServiceProxy.isActive(mContext, policy)) {
-            // See if the required policies are in force; if they are, acknowledge the policies
-            // to the server and get the final policy key
-            // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
-            String securitySyncKey;
-            if (getProtocolVersion() == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
-                securitySyncKey = pp.getSecuritySyncKey();
-            } else {
-                securitySyncKey = acknowledgeProvision(pp.getSecuritySyncKey(),
-                        PROVISION_STATUS_OK);
-            }
-            if (securitySyncKey != null) {
-                // If attachment policies have changed, fix up any affected attachment records
-                if (oldPolicy != null) {
-                    if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
-                            (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
-                        Policy.setAttachmentFlagsForNewPolicy(mContext, mAccount, policy);
-                    }
-                }
-                // Write the final policy key to the Account and say we've been successful
-                PolicyServiceProxy.setAccountPolicy(mContext, mAccount.mId, policy,
-                        securitySyncKey);
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Do the heavy lifting for validation and sync:
-     * - HTTP OPTIONS request to get protocol version from the server, if we don't already have it.
-     * - Exchange FolderSync request to get the folder info.
-     * (And exception handling for those operations.)
-     * Validation differs from sync in four ways:
-     * - Validation registers the client cert.
-     * - Validation doesn't save the FolderSync results.
-     * - Validation doesn't attempt to set device policies.
-     * - Validation must populate a bundle with the results of the validation.
-     * @param bundle If this is a validation call, this will be non-null, and this function will
-     *               write the results to it (specifically it writes
-     *               {@link EmailServiceProxy#VALIDATE_BUNDLE_RESULT_CODE},
-     *               {@link EmailServiceProxy#VALIDATE_BUNDLE_PROTOCOL_VERSION}, and
-     *               {@link EmailServiceProxy#VALIDATE_BUNDLE_POLICY_SET} (when there's a policy to
-     *               be had).
-     *               If this is a sync call, bundle will be null.
-     *               This function also uses the null/not null status to differentiate behavior in
-     *               the few places where validation and sync don't do the same thing.
-     */
-    protected void doValidationOrSync(final Bundle bundle) {
-        LogUtils.i(TAG, "Performing %s: %s, %s, ssl = %s", bundle != null ? "validation" : "sync",
-                mHostAuth.mAddress, mHostAuth.mLogin, mHostAuth.shouldUseSsl() ? "1" : "0");
-
-        if (bundle != null) {
-            if (mHostAuth.mClientCertAlias != null) {
-                try {
-                    getClientConnectionManager().registerClientCert(mContext, mHostAuth);
-                } catch (final CertificateException e) {
-                    // The client certificate the user specified is invalid/inaccessible.
-                    bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
-                            MessagingException.CLIENT_CERTIFICATE_ERROR);
-                    return;
-                }
-            }
-        }
-
-        int resultCode;
-
-        // Need a nested try here because the provisioning exception handler can throw IOException.
-        try {
-            try {
-                // TODO: also want to check protocol version at least once in a while after setup.
-                if (mAccount.mProtocolVersion == null) {
-                    final int optionsResult = getServerProtocolVersion();
-                    if (optionsResult != MessagingException.NO_ERROR) {
-                        if (bundle != null) {
-                            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
-                                    optionsResult);
-                        }
-                        return;
-                    }
-                    if (bundle != null) {
-                        bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION,
-                                mAccount.mProtocolVersion);
-                    }
-                }
-                resultCode = doFolderSync(bundle != null);
-            } catch (final CommandStatusException e) {
-                final int status = e.mStatus;
-                if (CommandStatus.isNeedsProvisioning(status)) {
-                    // Get the policies and see if we are able to support them
-                    final ProvisionParser pp = canProvision();
-                    if (pp != null && pp.hasSupportablePolicySet()) {
-                        // Set the proper result code and save the PolicySet in our Bundle
-                        if (bundle != null) {
-                            resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
-                            bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
-                                    pp.getPolicy());
-                            if (getProtocolVersion() == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
-                                mAccount.mSecuritySyncKey = pp.getSecuritySyncKey();
-                                if (!sendSettings()) {
-                                    LogUtils.i(TAG, "Denied access: %s",
-                                            CommandStatus.toString(status));
-                                    resultCode = MessagingException.ACCESS_DENIED;
-                                }
-                            }
-                        } else if (tryProvision(pp)) {
-                            resultCode = MessagingException.NO_ERROR;
-                        } else {
-                            resultCode = MessagingException.GENERAL_SECURITY;
-                        }
-                    } else {
-                        // If not, set the proper code (the account will not be created)
-                        resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
-                        if (bundle != null) {
-                            bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
-                                    pp.getPolicy());
-                        }
-                    }
-                } else if (CommandStatus.isDeniedAccess(status)) {
-                    LogUtils.i(TAG, "Denied access: %s", CommandStatus.toString(status));
-                    resultCode = MessagingException.ACCESS_DENIED;
-                } else if (CommandStatus.isTransientError(status)) {
-                    LogUtils.i(TAG, "Transient error: %s", CommandStatus.toString(status));
-                    resultCode = MessagingException.IOERROR;
-                } else {
-                    LogUtils.i(TAG, "Unexpected response: %s", CommandStatus.toString(status));
-                    resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
-                }
-            } catch (final RedirectException e) {
-                // We handle a limited number of redirects by recursion.
-                if (mRedirectCount < MAX_REDIRECTS && e.mRedirectAddress != null) {
-                    ++mRedirectCount;
-                    redirectHostAuth(e.mRedirectAddress);
-                    if (bundle != null) {
-                        bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS,
-                                e.mRedirectAddress);
-                    }
-                    doValidationOrSync(bundle);
-                    return;
-                } else {
-                    resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
-                }
-            }
-        } catch (final IOException e) {
-            final Throwable cause = e.getCause();
-            if (cause != null && cause instanceof CertificateException) {
-                // This could be because the server's certificate failed to validate.
-                resultCode = MessagingException.GENERAL_SECURITY;
-            } else {
-                resultCode = MessagingException.IOERROR;
-            }
-        }
-
-        if (bundle != null) {
-            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
-        }
-    }
-
-
-    /**
-     * Perform the actual validation.
-     * @return The validation response.
-     */
-    public Bundle validate() {
-        final Bundle bundle = new Bundle();
-        doValidationOrSync(bundle);
-        return bundle;
-    }
-}
diff --git a/src/com/android/exchange/service/EasServerConnection.java b/src/com/android/exchange/service/EasServerConnection.java
index 72716fc..1f07854 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) {
+        mProtocolVersionIsSet = (protocolVersionString != null);
         if (protocolVersionString == null) {
             protocolVersionString = Eas.DEFAULT_PROTOCOL_VERSION;
         }
+        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