blob: 2b40112a139ba2a6545537f3679611b553de6310 [file] [log] [blame]
package com.android.exchange.service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Base64;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.AccountServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.utility.EmailClientConnectionManager;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.params.ConnManagerPNames;
import org.apache.http.conn.params.ConnPerRoute;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import java.io.IOException;
import java.net.URI;
/**
* Base class for performing a single sync action. It holds the state needed for all sync actions
* (e.g. account and auth info, sync extras and results), commonly used functions to handle the
* network communication with the server, and functions to communicate to with the app UI.
* Sublclasses must implement {@link #performSync}, but otherwise have no other requirements.
*/
public abstract class EasSyncHandler {
private static final String DEVICE_TYPE = "Android";
private static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
Eas.CLIENT_VERSION;
protected final Context mContext;
protected final ContentResolver mContentResolver;
protected final Account mAccount;
protected final HostAuth mHostAuth;
protected final Mailbox mMailbox;
protected final Bundle mSyncExtras;
protected final SyncResult mSyncResult;
/**
* Status values to indicate success or manner of failure when sending a single Message.
*/
public enum SyncStatus {
// Message sent successfully.
SUCCESS,
// Sync failed due to an I/O error (file or network).
FAILURE_IO,
// Sync failed due to login problems.
FAILURE_LOGIN,
// Sync failed due to login problems.
FAILURE_SECURITY,
// Sync failed due to bad message data.
FAILURE_MESSAGE,
FAILURE_OTHER
}
/**
* Cached value of the device id. The app should return the same value each time it's requested
* so instead of always waiting on the RPC we keep a copy here.
* TODO: Why *is* this an RPC -- wouldn't it make sense to just compute locally?
*/
public static String sDeviceId = null;
protected EasSyncHandler(final Context context, final ContentResolver contentResolver,
final Account account, final Mailbox mailbox, final Bundle syncExtras,
final SyncResult syncResult) {
mContext = context;
mContentResolver = contentResolver;
mAccount = account;
mHostAuth = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
mMailbox = mailbox;
mSyncExtras = syncExtras;
mSyncResult = syncResult;
}
/**
* Create an instance of the appropriate subclass to handle sync for mailbox.
* @param context
* @param contentResolver
* @param account The {@link Account} for mailbox.
* @param mailbox The {@link Mailbox} to sync.
* @param syncExtras The extras for this sync, for consumption by {@link #performSync}.
* @param syncResult The output results for this sync, which may be written to by
* {@link #performSync}.
* @return An appropriate EasSyncHandler for this mailbox, or null if this sync can't be
* handled.
*/
public static EasSyncHandler getEasSyncHandler(final Context context,
final ContentResolver contentResolver, final Account account, final Mailbox mailbox,
final Bundle syncExtras, final SyncResult syncResult) {
if (account != null && mailbox != null) {
switch (mailbox.mType) {
case Mailbox.TYPE_INBOX:
case Mailbox.TYPE_MAIL:
return new EasMailboxSyncHandler(context, contentResolver, account, mailbox,
syncExtras, syncResult);
case Mailbox.TYPE_OUTBOX:
return new EasOutboxSyncHandler(context, contentResolver, account, mailbox,
syncExtras, syncResult);
case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
return new EasAccountSyncHandler(context, contentResolver, account, mailbox,
syncExtras, syncResult);
}
}
// Could not handle this sync.
return null;
}
/**
* Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from
* the system SyncManager and will be read by it on the way out).
* In the case of errors, this function should not attempt any retries, but rather should
* set the {@link SyncResult} to reflect the problem and let the system SyncManager handle
* any retries etc.
* @return An exit status code.
* TODO: Do we really need a return value, or should we just use the SyncResult for this?
*/
public abstract SyncStatus performSync();
// Client - server communication.
// Command timeout is the the time allowed for reading data from an open connection before an
// IOException is thrown. After a small added allowance, our watchdog alarm goes off (allowing
// us to detect a silently dropped connection). The allowance is defined below.
protected static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
// Connection timeout is the time given to connect to the server before reporting an IOException
protected static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
private static final ConnPerRoute sConnPerRoute = new ConnPerRoute() {
@Override
public int getMaxForRoute(final HttpRoute route) {
return 8;
}
};
// TODO: Don't make a new one each time.
private EmailClientConnectionManager getClientConnectionManager() {
final HttpParams params = new BasicHttpParams();
params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
final boolean ssl = mHostAuth.shouldUseSsl();
final int port = mHostAuth.mPort;
return EmailClientConnectionManager.newInstance(mContext, params, mHostAuth);
}
private HttpClient getHttpClient(final long timeout) {
final HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT));
HttpConnectionParams.setSoTimeout(params, (int)(timeout));
HttpConnectionParams.setSocketBufferSize(params, 8192);
return new DefaultHttpClient(getClientConnectionManager(), params);
}
private String makeAuthString() {
final String cs = mHostAuth.mLogin + ":" + mHostAuth.mPassword;
return "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
}
private String makeUserString() {
return "&User=" + Uri.encode(mHostAuth.mLogin) + "&DeviceId=" +
getDeviceId(mContext) + "&DeviceType=" + DEVICE_TYPE;
}
private String makeBaseUriString() {
return EmailClientConnectionManager.makeScheme(mHostAuth.shouldUseSsl(),
mHostAuth.shouldTrustAllServerCerts(), mHostAuth.mClientCertAlias) +
"://" + mHostAuth.mAddress + "/Microsoft-Server-ActiveSync";
}
private String makeUriString(final String cmd, final String extra) {
String uriString = makeBaseUriString();
if (cmd != null) {
uriString += "?Cmd=" + cmd + makeUserString();
}
if (extra != null) {
uriString += extra;
}
return uriString;
}
protected String getProtocolVersion() {
if (mAccount.mProtocolVersion != null) {
return mAccount.mProtocolVersion;
}
return Eas.DEFAULT_PROTOCOL_VERSION;
}
/**
* Set standard HTTP headers, using a policy key if required
* @param method the method we are going to send
* @param usePolicyKey whether or not a policy key should be sent in the headers
*/
private void setHeaders(final HttpRequestBase method, final boolean usePolicyKey) {
method.setHeader("Authorization", makeAuthString());
method.setHeader("MS-ASProtocolVersion", getProtocolVersion());
method.setHeader("User-Agent", USER_AGENT);
method.setHeader("Accept-Encoding", "gzip");
if (usePolicyKey) {
// If there's an account in existence, use its key; otherwise (we're creating the
// account), send "0". The server will respond with code 449 if there are policies
// to be enforced
String key = "0";
if (mAccount != null) {
final String accountKey = mAccount.mSecuritySyncKey;
if (!TextUtils.isEmpty(accountKey)) {
key = accountKey;
}
}
method.setHeader("X-MS-PolicyKey", key);
}
}
protected EasResponse sendHttpClientPost(String cmd, final HttpEntity entity,
final long timeout) throws IOException {
final HttpClient client = getHttpClient(timeout);
final boolean isPingCommand = cmd.equals("Ping");
// Split the mail sending commands
String extra = null;
boolean msg = false;
if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
final int cmdLength = cmd.indexOf('&');
extra = cmd.substring(cmdLength);
cmd = cmd.substring(0, cmdLength);
msg = true;
} else if (cmd.startsWith("SendMail&")) {
msg = true;
}
final String us = makeUriString(cmd, extra);
final HttpPost method = new HttpPost(URI.create(us));
// Send the proper Content-Type header; it's always wbxml except for messages when
// the EAS protocol version is < 14.0
// If entity is null (e.g. for attachments), don't set this header
final String protocolVersion = getProtocolVersion();
final Double protocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
if (msg && (protocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) {
method.setHeader("Content-Type", "message/rfc822");
} else if (entity != null) {
method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
}
setHeaders(method, !isPingCommand);
// NOTE
// The next lines are added at the insistence of $VENDOR, who is seeing inappropriate
// network activity related to the Ping command on some networks with some servers.
// This code should be removed when the underlying issue is resolved
if (isPingCommand) {
method.setHeader("Connection", "close");
}
method.setEntity(entity);
return executePostWithTimeout(client, method);
}
protected EasResponse sendHttpClientPost(final String cmd, final byte[] bytes)
throws IOException {
return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
}
private EasResponse executePostWithTimeout(final HttpClient client, final HttpPost method)
throws IOException {
// TODO: The first argument below is probably bad.
return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
}
// Communication with the application.
/**
* EAS requires a unique device id, so that sync is possible from a variety of different
* devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other
* device that doesn't provide one, we can create it as "device".
* This would work on a real device as well, but it would be better to use the "real" id if
* it's available
*/
public static String getDeviceId(final Context context) {
if (sDeviceId == null) {
sDeviceId = new AccountServiceProxy(context).getDeviceId();
}
return sDeviceId;
}
// TODO: Consider bringing the EmailServiceStatus functions here?
/**
* Convenience wrapper to {@link EmailServiceStatus#syncMailboxStatus}.
* @param statusCode
* @param progress
*/
protected void syncMailboxStatus(final int statusCode, final int progress) {
EmailServiceStatus.syncMailboxStatus(mContentResolver, mSyncExtras, mMailbox.mId,
statusCode, progress);
}
/**
* Convenience wrapper to {@link EmailServiceStatus#sendMessageStatus}.
* @param messageId
* @param subject
* @param statusCode
* @param progress
*/
protected void sendMessageStatus(final long messageId, final String subject,
final int statusCode, final int progress) {
EmailServiceStatus.sendMessageStatus(mContentResolver, mSyncExtras, messageId, subject,
statusCode, progress);
}
/**
* Convenience wrapper to {@link EmailServiceStatus#syncMailboxListStatus}.
* @param statusCode
* @param progress
*/
protected void syncMailboxListStatus(final int statusCode, final int progress) {
EmailServiceStatus.syncMailboxListStatus(mContentResolver, mSyncExtras, mAccount.mId,
statusCode, progress);
}
}