| package com.android.exchange.service; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.util.Base64; |
| |
| import com.android.emailcommon.Device; |
| import com.android.emailcommon.internet.MimeUtility; |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.HostAuth; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.utility.EmailClientConnectionManager; |
| import com.android.exchange.Eas; |
| import com.android.exchange.EasResponse; |
| import com.android.mail.utils.LogUtils; |
| |
| import org.apache.http.HttpEntity; |
| import org.apache.http.client.HttpClient; |
| import org.apache.http.client.methods.HttpOptions; |
| import org.apache.http.client.methods.HttpPost; |
| 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; |
| import java.util.HashMap; |
| |
| /** |
| * Base class for communicating with an EAS server. Anything that needs to send messages to the |
| * server can subclass this to get access to the {@link #sendHttpClientPost} family of functions. |
| * TODO: This class has a regrettable name. It's not a connection, but rather a task that happens |
| * to have (and use) a connection to the server. |
| */ |
| public abstract class EasServerConnection { |
| /** Logging tag. */ |
| private static final String TAG = "EasServerConnection"; |
| |
| /** |
| * Timeout for establishing a connection to the server. |
| */ |
| private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; |
| |
| /** |
| * Timeout for http requests after the connection has been established. |
| */ |
| protected static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS; |
| |
| private static final String DEVICE_TYPE = "Android"; |
| protected static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' + |
| Eas.CLIENT_VERSION; |
| |
| /** Message MIME type for EAS version 14 and later. */ |
| private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml"; |
| |
| private static final ConnPerRoute sConnPerRoute = new ConnPerRoute() { |
| @Override |
| public int getMaxForRoute(final HttpRoute route) { |
| return 8; |
| } |
| }; |
| |
| /** |
| * Value for {@link #mStoppedReason} when we haven't been stopped. |
| */ |
| public static final int STOPPED_REASON_NONE = 0; |
| |
| /** |
| * Passed to {@link #stop} to indicate that this stop request should terminate this task. |
| */ |
| public static final int STOPPED_REASON_ABORT = 1; |
| |
| /** |
| * Passed to {@link #stop} to indicate that this stop request should restart this task (e.g. in |
| * order to reload parameters). |
| */ |
| public static final int STOPPED_REASON_RESTART = 2; |
| |
| protected final Context mContext; |
| // TODO: Make this private if possible. Subclasses must be careful about altering the HostAuth |
| // to not screw up any connection caching (use redirectHostAuth). |
| protected final HostAuth mHostAuth; |
| protected final Account mAccount; |
| |
| // Bookkeeping for interrupting a POST. 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 boolean mStopped = false; |
| private int mStoppedReason = STOPPED_REASON_NONE; |
| |
| /** |
| * The protocol version to use, as a double. This is a cached value based on the protocol |
| * version in {@link #mAccount}, so whenever that value is changed, |
| * {@link #uncacheProtocolVersion()} must be called. |
| */ |
| private double mProtocolVersionDouble = 0.0d; |
| |
| /** |
| * The client for any requests made by this object. This is created lazily, and cleared |
| * whenever our host auth is redirected. |
| */ |
| private HttpClient mClient; |
| |
| /** |
| * The connection manager for any requests made by this object. This is created lazily, and |
| * cleared whenever our host auth is redirected. |
| */ |
| private EmailClientConnectionManager mConnectionManager; |
| |
| |
| /** |
| * We want to reuse {@link EmailClientConnectionManager} across different requests to the same |
| * {@link HostAuth}. Since HostAuths have unique ids, we can use that as the cache key. |
| * All access to the cache must be synchronized in theory, although in practice since we don't |
| * have concurrent requests to the same account it should never come up. |
| */ |
| private static class ConnectionManagerCache { |
| private final HashMap<Long, EmailClientConnectionManager> mMap = |
| new HashMap<Long, EmailClientConnectionManager>(); |
| |
| /** |
| * Get a connection manager from the cache, or create one and add it if needed. |
| * @param context The {@link Context}. |
| * @param hostAuth The {@link HostAuth} to which we want to connect. |
| * @return The connection manager for hostAuth. |
| */ |
| public synchronized EmailClientConnectionManager getConnectionManager( |
| final Context context, final HostAuth hostAuth) { |
| EmailClientConnectionManager connectionManager = mMap.get(hostAuth.mId); |
| if (connectionManager == null) { |
| final HttpParams params = new BasicHttpParams(); |
| params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25); |
| params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute); |
| final boolean ssl = hostAuth.shouldUseSsl(); |
| final int port = hostAuth.mPort; |
| LogUtils.i(TAG, "Creating connection manager for port %d (%s)", port, |
| ssl ? "uses ssl" : "no ssl"); |
| connectionManager = |
| EmailClientConnectionManager.newInstance(context, params, hostAuth); |
| // We don't save managers for validation/autodiscover |
| if (hostAuth.isSaved()) { |
| mMap.put(hostAuth.mId, connectionManager); |
| } |
| } |
| return connectionManager; |
| } |
| |
| /** |
| * Remove a connection manager from the cache. This is necessary when a {@link HostAuth} is |
| * redirected or otherwise altered. |
| * TODO: We should uncache when we delete accounts. |
| * @param hostAuth The {@link HostAuth} whose connection manager should be deleted. |
| */ |
| public synchronized void uncacheConnectionManager(final HostAuth hostAuth) { |
| mMap.remove(hostAuth.mId); |
| } |
| } |
| private static final ConnectionManagerCache sConnectionManagers = new ConnectionManagerCache(); |
| |
| protected EasServerConnection(final Context context, final Account account, |
| final HostAuth hostAuth) { |
| mContext = context; |
| mHostAuth = hostAuth; |
| mAccount = account; |
| } |
| |
| protected EasServerConnection(final Context context, final Account account) { |
| this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv)); |
| } |
| |
| protected EmailClientConnectionManager getClientConnectionManager() { |
| if (mConnectionManager == null) { |
| mConnectionManager = sConnectionManagers.getConnectionManager(mContext, mHostAuth); |
| } |
| return mConnectionManager; |
| } |
| |
| protected void redirectHostAuth(final String newAddress) { |
| mClient = null; |
| mConnectionManager = null; |
| mHostAuth.mAddress = newAddress; |
| if (mHostAuth.isSaved()) { |
| sConnectionManagers.uncacheConnectionManager(mHostAuth); |
| final ContentValues cv = new ContentValues(1); |
| cv.put(EmailContent.HostAuthColumns.ADDRESS, newAddress); |
| mHostAuth.update(mContext, cv); |
| } |
| } |
| |
| private HttpClient getHttpClient(final long timeout) { |
| if (mClient == null) { |
| final HttpParams params = new BasicHttpParams(); |
| HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT)); |
| HttpConnectionParams.setSoTimeout(params, (int)(timeout)); |
| HttpConnectionParams.setSocketBufferSize(params, 8192); |
| mClient = new DefaultHttpClient(getClientConnectionManager(), params); |
| } |
| return mClient; |
| } |
| |
| private String makeAuthString() { |
| final String cs = mHostAuth.mLogin + ":" + mHostAuth.mPassword; |
| return "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP); |
| } |
| |
| private String makeUserString() { |
| String deviceId; |
| try { |
| deviceId = Device.getDeviceId(mContext); |
| } catch (final IOException e) { |
| // TODO: Make Device.getDeviceId not throw IOException, if possible. |
| // Otherwise use a better deviceId default. |
| deviceId = "0"; |
| } |
| return "&User=" + Uri.encode(mHostAuth.mLogin) + "&DeviceId=" + |
| deviceId + "&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; |
| } |
| |
| /** |
| * Get the protocol version for {@link #mAccount}, or a default if we can't determine it. |
| * @return The protocol version for {@link #mAccount}, as a String. |
| */ |
| private String getProtocolVersionString() { |
| if (mAccount.mProtocolVersion != null) { |
| return mAccount.mProtocolVersion; |
| } |
| return Eas.DEFAULT_PROTOCOL_VERSION; |
| } |
| |
| /** |
| * 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. |
| */ |
| protected void uncacheProtocolVersion() { |
| mProtocolVersionDouble = 0.0d; |
| } |
| |
| /** |
| * Get the protocol version for {@link #mAccount}, as a double. This function caches the result |
| * of looking up the value so that subsequent calls do not have to repeat that. |
| * @return The protocol version for {@link #mAccount}, as a double. |
| */ |
| protected double getProtocolVersion() { |
| if (mProtocolVersionDouble == 0.0d) { |
| mProtocolVersionDouble = Eas.getProtocolVersionDouble(getProtocolVersionString()); |
| } |
| return mProtocolVersionDouble; |
| } |
| |
| /** |
| * Send an http OPTIONS request to server. |
| * @return The {@link EasResponse} from the Exchange server. |
| * @throws IOException |
| */ |
| protected EasResponse sendHttpClientOptions() throws IOException { |
| // For OPTIONS, just use the base string and the single header |
| final HttpOptions method = new HttpOptions(URI.create(makeBaseUriString())); |
| method.setHeader("Authorization", makeAuthString()); |
| method.setHeader("User-Agent", USER_AGENT); |
| return EasResponse.fromHttpRequest(getClientConnectionManager(), |
| getHttpClient(COMMAND_TIMEOUT), method); |
| } |
| |
| protected void resetAuthorization(final HttpPost post) { |
| post.removeHeaders("Authorization"); |
| post.setHeader("Authorization", makeAuthString()); |
| } |
| |
| /** |
| * Make an {@link HttpPost} for a specific request. |
| * @param uri The uri for this request, as a {@link String}. |
| * @param entity The {@link HttpEntity} for this request. |
| * @param contentType The Content-Type for this request. |
| * @param usePolicyKey Whether or not a policy key should be sent. |
| * @return |
| */ |
| protected HttpPost makePost(final String uri, final HttpEntity entity, final String contentType, |
| final boolean usePolicyKey) { |
| final HttpPost post = new HttpPost(uri); |
| post.setHeader("Authorization", makeAuthString()); |
| post.setHeader("MS-ASProtocolVersion", getProtocolVersionString()); |
| post.setHeader("User-Agent", USER_AGENT); |
| post.setHeader("Accept-Encoding", "gzip"); |
| if (contentType != null) { |
| post.setHeader("Content-Type", contentType); |
| } |
| 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 |
| final String key; |
| final String accountKey = mAccount.mSecuritySyncKey; |
| if (!TextUtils.isEmpty(accountKey)) { |
| key = accountKey; |
| } else { |
| key = "0"; |
| } |
| post.setHeader("X-MS-PolicyKey", key); |
| } |
| post.setEntity(entity); |
| return post; |
| } |
| |
| /** |
| * 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. |
| * @param timeout The timeout for this POST. |
| * @return The response from the Exchange server. |
| * @throws IOException |
| */ |
| protected EasResponse sendHttpClientPost(String cmd, final HttpEntity entity, |
| final long timeout) throws IOException { |
| 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; |
| } |
| |
| // 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 contentType; |
| if (msg && (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) { |
| contentType = MimeUtility.MIME_TYPE_RFC822; |
| } else if (entity != null) { |
| contentType = EAS_14_MIME_TYPE; |
| } |
| else { |
| contentType = null; |
| } |
| final HttpPost method = makePost(makeUriString(cmd, extra), entity, contentType, |
| !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"); |
| } |
| return executePost(method, timeout); |
| } |
| |
| protected EasResponse sendHttpClientPost(final String cmd, final byte[] bytes, |
| final long timeout) throws IOException { |
| return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), timeout); |
| } |
| |
| protected EasResponse sendHttpClientPost(final String cmd, final byte[] bytes) |
| throws IOException { |
| return sendHttpClientPost(cmd, bytes, COMMAND_TIMEOUT); |
| } |
| |
| /** |
| * Executes an {@link HttpPost}. |
| * 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. |
| * @param timeout The timeout to use. |
| * @return The response from the Exchange server. |
| * @throws IOException |
| */ |
| protected EasResponse executePost(final HttpPost 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 |
| // concurrent access to this function, which is not supported. |
| synchronized (this) { |
| if (mStopped) { |
| mStopped = false; |
| // If this gets stopped after the POST actually starts, it throws an IOException. |
| // Therefore if we get stopped here, let's throw the same sort of exception, so |
| // callers can just equate IOException with the "this POST got killed for some |
| // reason". |
| throw new IOException("Command was stopped before POST"); |
| } |
| mPendingPost = method; |
| } |
| try { |
| return EasResponse.fromHttpRequest(getClientConnectionManager(), getHttpClient(timeout), |
| method); |
| } finally { |
| synchronized (this) { |
| mPendingPost = null; |
| mStoppedReason = STOPPED_REASON_NONE; |
| } |
| } |
| } |
| |
| protected EasResponse executePost(final HttpPost method) throws IOException { |
| return executePost(method, COMMAND_TIMEOUT); |
| } |
| |
| /** |
| * If called while this object is executing a POST, interrupt it with an {@link IOException}. |
| * Otherwise cause the next attempt to execute a POST to be interrupted with an |
| * {@link IOException}. |
| * @param reason The reason for requesting a stop. This should be one of the STOPPED_REASON_* |
| * constants defined in this class, other than {@link #STOPPED_REASON_NONE} which |
| * is used to signify that no stop has occurred. |
| * This class simply stores the value; subclasses are responsible for checking |
| * this value when catching the {@link IOException} and responding appropriately. |
| * @return Whether we were in the middle of a POST. |
| */ |
| public synchronized boolean stop(final int reason) { |
| final boolean isMidPost = (mPendingPost != null); |
| LogUtils.i(TAG, "%s with reason %d", (isMidPost ? "Interrupt" : "Stop next"), reason); |
| mStoppedReason = reason; |
| if (isMidPost) { |
| mPendingPost.abort(); |
| } else { |
| mStopped = true; |
| } |
| return isMidPost; |
| } |
| |
| /** |
| * Check the reason of the last {@link #stop} request. |
| * @return The reason supplied to the last call to {@link #stop}, or |
| * {@link #STOPPED_REASON_NONE} if we haven't been stopped since the last successful POST. |
| */ |
| protected int getStoppedReason() { |
| return mStoppedReason; |
| } |
| |
| /** |
| * Convenience method for adding a Message to an account's outbox |
| * @param msg the message to send |
| */ |
| protected void sendMessage(final EmailContent.Message msg) { |
| final Mailbox mailbox = |
| Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_OUTBOX); |
| if (mailbox != null) { |
| msg.mMailboxKey = mailbox.mId; |
| msg.mAccountKey = mAccount.mId; |
| msg.save(mContext); |
| } |
| } |
| |
| } |