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