| package com.android.exchange.service; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.os.AsyncTask; |
| import android.provider.CalendarContract; |
| import android.provider.ContactsContract; |
| import android.text.format.DateUtils; |
| |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.EmailContent.MailboxColumns; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.exchange.Eas; |
| import com.android.exchange.EasResponse; |
| import com.android.exchange.adapter.PingParser; |
| import com.android.exchange.adapter.Serializer; |
| import com.android.exchange.adapter.Tags; |
| import com.android.mail.utils.LogUtils; |
| |
| import org.apache.http.HttpStatus; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| |
| /** |
| * Performs an Exchange Ping, which is the command for receiving push notifications. |
| * TODO: Rename this, now that it's no longer a subclass of EasSyncHandler. |
| */ |
| public class EasPingSyncHandler extends EasServerConnection { |
| private static final String TAG = "EasPingSyncHandler"; |
| |
| private final ContentResolver mContentResolver; |
| private final PingTask mPingTask; |
| |
| // TODO: Implement Heartbeat autoadjustments based on the server responses. |
| /** |
| * The heartbeat interval specified to the Exchange server. This is the maximum amount of |
| * time (in seconds) that the server should wait before responding to the ping request. |
| */ |
| private static final long PING_HEARTBEAT = |
| 8 * (DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); |
| |
| /** {@link #PING_HEARTBEAT}, as a String. */ |
| private static final String PING_HEARTBEAT_STRING = Long.toString(PING_HEARTBEAT); |
| |
| /** |
| * The timeout used for the HTTP POST (in milliseconds). Notionally this should be the same |
| * as {@link #PING_HEARTBEAT} but in practice is a few seconds longer to allow for latency |
| * in the server's response. |
| */ |
| private static final long POST_TIMEOUT = (5 + PING_HEARTBEAT) * DateUtils.SECOND_IN_MILLIS; |
| |
| /** |
| * An {@link AsyncTask} that actually does the work of pinging. |
| */ |
| private class PingTask extends AsyncTask<Void, Void, Void> { |
| /** Selection clause when querying for a specific mailbox. */ |
| private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = |
| MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; |
| |
| private final EmailSyncAdapterService.SyncHandlerSynchronizer mSyncHandlerMap; |
| |
| private PingTask(final EmailSyncAdapterService.SyncHandlerSynchronizer syncHandlerMap) { |
| mSyncHandlerMap = syncHandlerMap; |
| } |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| final android.accounts.Account amAccount = new android.accounts.Account( |
| mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| // We keep pinging until we're interrupted, or reach some error condition that |
| // prevents us from proceeding. |
| int pingStatus; |
| do { |
| // Get the mailboxes that need push notifications. |
| final Cursor c = Mailbox.getMailboxesForPush(mContentResolver, mAccount.mId); |
| if (c == null) { |
| pingStatus = PingParser.STATUS_FAILED; |
| continue; |
| } |
| // Set up the request. |
| Serializer s = null; |
| try { |
| try { |
| while (c.moveToNext()) { |
| Mailbox mailbox = new Mailbox(); |
| mailbox.restore(c); |
| s = handleOneMailbox(s, mailbox, amAccount); |
| } |
| } finally { |
| c.close(); |
| } |
| if (s != null) { |
| // Note: this sequence of end()s corresponds to the start()s that occur |
| // in handleOneMailbox when the Serializer is first created. |
| // If either side changes, the other must be kept in sync. |
| s.end().end().done(); |
| } |
| } catch (final IOException e) { |
| LogUtils.e(TAG, "IOException while building ping request for account %d: %s", |
| mAccount.mId, e.toString()); |
| pingStatus = PingParser.STATUS_FAILED; |
| continue; |
| } |
| |
| if (s == null) { |
| // No mailboxes want to receive a push notification right now. |
| // TODO: Need to set up code that restarts the push when things change. |
| pingStatus = PingParser.STATUS_NO_FOLDERS; |
| continue; |
| } |
| |
| final EasResponse resp; |
| try { |
| resp = sendHttpClientPost("Ping", s.toByteArray(), POST_TIMEOUT); |
| } catch (final IOException e) { |
| LogUtils.i(TAG, "IOException during ping: %s", e.getMessage()); |
| switch (getStoppedReason()) { |
| case STOPPED_REASON_NONE: |
| // The POST stopped for a reason other than an explicit stop request. |
| pingStatus = PingParser.STATUS_NETWORK_EXCEPTION; |
| break; |
| case STOPPED_REASON_ABORT: |
| // This ping was stopped by a new sync request. |
| pingStatus = PingParser.STATUS_INTERRUPTED; |
| break; |
| case STOPPED_REASON_RESTART: |
| // This ping was stopped in order to reload the push parameters. |
| mAccount.refresh(mContext); |
| if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { |
| // We still want to push. Treat this as if we had timed out, so |
| // we'll loop again. |
| pingStatus = PingParser.STATUS_EXPIRED; |
| } else { |
| // No longer want to push. Treat this as if there were no folders |
| // found that want push. |
| pingStatus = PingParser.STATUS_NO_FOLDERS; |
| } |
| break; |
| default: |
| // This shouldn't be possible, but treat it the same as |
| // STOPPED_REASON_NONE. |
| LogUtils.e(TAG, "Account %d got bad stop reason %d", mAccount.mId, |
| getStoppedReason()); |
| pingStatus = PingParser.STATUS_NETWORK_EXCEPTION; |
| break; |
| } |
| continue; |
| } |
| |
| try { |
| pingStatus = handleResponse(resp, amAccount); |
| } finally { |
| resp.close(); |
| } |
| } while (PingParser.shouldPingAgain(pingStatus)); |
| |
| mSyncHandlerMap.pingComplete(amAccount, mAccount, pingStatus); |
| return null; |
| } |
| |
| /** |
| * Gets the correct authority for a mailbox (PIM collections use different authorities). |
| * @param mailboxType The type of the mailbox we're interested in, from {@link Mailbox}. |
| * @return The authority for the mailbox we're interested in. |
| */ |
| private String getAuthority(final int mailboxType) { |
| switch (mailboxType) { |
| case Mailbox.TYPE_CALENDAR: |
| return CalendarContract.AUTHORITY; |
| case Mailbox.TYPE_CONTACTS: |
| return ContactsContract.AUTHORITY; |
| default: |
| return EmailContent.AUTHORITY; |
| } |
| } |
| |
| /** |
| * Gets the Exchange folder class for a mailbox type (PIM collections have different values |
| * from email), needed when forming the request. |
| * @param mailboxType The type of the mailbox we're interested in, from {@link Mailbox}. |
| * @return The folder class for the mailbox we're interested in. |
| */ |
| private String getFolderClass(final int mailboxType) { |
| switch (mailboxType) { |
| case Mailbox.TYPE_CALENDAR: |
| return "Calendar"; |
| case Mailbox.TYPE_CONTACTS: |
| return "Contacts"; |
| default: |
| return "Email"; |
| } |
| } |
| |
| /** |
| * If mailbox is eligible for push, add it to the ping request, creating the |
| * {@link Serializer} for the request if necessary. |
| * @param mailbox The mailbox to check. |
| * @param s The {@link Serializer} for this ping request, or null if it hasn't been created |
| * yet. |
| * @param amAccount The {@link android.accounts.Account} for this request. |
| * @return The {@link Serializer} for this ping request, or null if it hasn't been created |
| * yet. |
| * @throws IOException |
| */ |
| private Serializer handleOneMailbox(Serializer s, final Mailbox mailbox, |
| final android.accounts.Account amAccount) throws IOException { |
| // We can't push until the initial sync is done |
| if (mailbox.mSyncKey != null && !mailbox.mSyncKey.equals("0")) { |
| if (ContentResolver.getSyncAutomatically(amAccount, getAuthority(mailbox.mType))) { |
| if (s == null) { |
| // No serializer yet, so create and initialize it. |
| // Note that these start()s correspond to the end()s in doInBackground. |
| // If either side changes, the other must be kept in sync. |
| s = new Serializer(); |
| s.start(Tags.PING_PING); |
| s.data(Tags.PING_HEARTBEAT_INTERVAL, PING_HEARTBEAT_STRING); |
| s.start(Tags.PING_FOLDERS); |
| } |
| s.start(Tags.PING_FOLDER); |
| s.data(Tags.PING_ID, mailbox.mServerId); |
| s.data(Tags.PING_CLASS, getFolderClass(mailbox.mType)); |
| s.end(); |
| } |
| } |
| return s; |
| } |
| |
| /** |
| * Make the appropriate calls to {@link ContentResolver#requestSync} indicated by the |
| * current ping response. |
| * @param amAccount The {@link android.accounts.Account} that we pinged. |
| * @param syncList The list of folders that need to be synced. |
| */ |
| private void requestSyncForSyncList(final android.accounts.Account amAccount, |
| final ArrayList<String> syncList) { |
| final String[] bindArguments = new String[2]; |
| bindArguments[0] = Long.toString(mAccount.mId); |
| for (final String serverId : syncList) { |
| bindArguments[1] = serverId; |
| // TODO: Rather than one query per ping mailbox, do it all in one? |
| final Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, |
| Mailbox.CONTENT_PROJECTION, WHERE_ACCOUNT_KEY_AND_SERVER_ID, |
| bindArguments, null); |
| if (c == null) { |
| // TODO: proper error handling. |
| break; |
| } |
| try { |
| /** |
| * Check the boxes reporting changes to see if there really were any... |
| * We do this because bugs in various Exchange servers can put us into a |
| * looping behavior by continually reporting changes in a mailbox, even |
| * when there aren't any. |
| * |
| * This behavior is seemingly random, and therefore we must code |
| * defensively by backing off of push behavior when it is detected. |
| * |
| * One known cause, on certain Exchange 2003 servers, is acknowledged by |
| * Microsoft, and the server hotfix for this case can be found at |
| * http://support.microsoft.com/kb/923282 |
| */ |
| // TODO: Implement the above. |
| /* |
| String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); |
| int type = ExchangeService.getStatusType(status); |
| // This check should always be true... |
| if (type == ExchangeService.SYNC_PING) { |
| int changeCount = ExchangeService.getStatusChangeCount(status); |
| if (changeCount > 0) { |
| errorMap.remove(serverId); |
| } else if (changeCount == 0) { |
| // This means that a ping reported changes in error; we keep a |
| // count of consecutive errors of this kind |
| String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); |
| Integer failures = errorMap.get(serverId); |
| if (failures == null) { |
| userLog("Last ping reported changes in error for: ", name); |
| errorMap.put(serverId, 1); |
| } else if (failures > MAX_PING_FAILURES) { |
| // We'll back off of push for this box |
| pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN)); |
| continue; |
| } else { |
| userLog("Last ping reported changes in error for: ", name); |
| errorMap.put(serverId, failures + 1); |
| } |
| } |
| } |
| */ |
| if (c.moveToFirst()) { |
| requestSyncForMailbox(amAccount, |
| getAuthority(c.getInt(Mailbox.CONTENT_TYPE_COLUMN)), |
| c.getLong(Mailbox.CONTENT_ID_COLUMN)); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| } |
| |
| /** |
| * Parse the response and take the appropriate action. |
| * @param resp The response to the Ping from the Exchange server. |
| * @param amAccount The AccountManager Account object for this account. |
| * @return The status value from the {@link PingParser}, or (in case of other failures) |
| * {@link PingParser#STATUS_FAILED}. |
| */ |
| private int handleResponse(final EasResponse resp, |
| final android.accounts.Account amAccount) { |
| final int code = resp.getStatus(); |
| |
| // Handle error cases. |
| if (EasResponse.isAuthError(code)) { |
| // TODO: signal this error more precisely. |
| LogUtils.i(TAG, "Auth Error for account %d", mAccount.mId); |
| return PingParser.STATUS_FAILED; |
| } |
| if (EasResponse.isRedirectError(code)) { |
| redirectHostAuth(resp.getRedirectAddress()); |
| return PingParser.STATUS_REDIRECT; |
| } |
| if (code != HttpStatus.SC_OK || resp.isEmpty()) { |
| LogUtils.e(TAG, "Bad response (%d) for account %d", code, mAccount.mId); |
| return PingParser.STATUS_FAILED; |
| } |
| |
| // Handle a valid response. |
| final PingParser pp; |
| |
| try { |
| pp = new PingParser(resp.getInputStream()); |
| } catch (final IOException e) { |
| LogUtils.e(TAG, "IOException creating PingParser: %s", e.getMessage()); |
| return PingParser.STATUS_FAILED; |
| } |
| |
| pp.parse(); |
| final int pingStatus = pp.getPingStatus(); |
| |
| // Take the appropriate action for this response. |
| // Many of the responses require no explicit action here, they just influence |
| // our re-ping behavior, which is handled by the caller. |
| switch (pingStatus) { |
| case PingParser.STATUS_FAILED: |
| LogUtils.e(TAG, "Ping failed for account %d", mAccount.mId); |
| break; |
| case PingParser.STATUS_EXPIRED: |
| LogUtils.i(TAG, "Ping expired for account %d", mAccount.mId); |
| break; |
| case PingParser.STATUS_CHANGES_FOUND: |
| LogUtils.i(TAG, "Ping found changed folders for account %d", mAccount.mId); |
| requestSyncForSyncList(amAccount, pp.getSyncList()); |
| break; |
| case PingParser.STATUS_REQUEST_INCOMPLETE: |
| case PingParser.STATUS_REQUEST_MALFORMED: |
| // These two cases indicate that the ping request was somehow bad. |
| // TODO: It's insanity to re-ping with the same data and expect a different |
| // result. Improve this if possible. |
| LogUtils.e(TAG, "Bad ping request for account %d", mAccount.mId); |
| break; |
| case PingParser.STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS: |
| LogUtils.i(TAG, "Heartbeat out of bounds for account %d", mAccount.mId); |
| // TODO: Implement auto heartbeat adjustments. |
| break; |
| case PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS: |
| LogUtils.i(TAG, "Too many folders for account %d", mAccount.mId); |
| break; |
| case PingParser.STATUS_FOLDER_REFRESH_NEEDED: |
| LogUtils.i(TAG, "FolderSync needed for account %d", mAccount.mId); |
| requestFolderSync(amAccount); |
| break; |
| case PingParser.STATUS_SERVER_ERROR: |
| LogUtils.i(TAG, "Server error for account %d", mAccount.mId); |
| break; |
| default: |
| LogUtils.e(TAG, "Unknown ping status %d for account %d", pingStatus, |
| mAccount.mId); |
| } |
| |
| return pingStatus; |
| } |
| } |
| |
| /** |
| * Issue a {@link ContentResolver#requestSync} to trigger a FolderSync for an account. |
| * @param amAccount The {@link android.accounts.Account} for the account that needs to sync. |
| */ |
| private static void requestFolderSync(final android.accounts.Account amAccount) { |
| requestSyncForMailbox(amAccount, EmailContent.AUTHORITY, |
| Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY); |
| } |
| |
| public static void requestPing(final android.accounts.Account amAccount) { |
| requestSyncForMailbox(amAccount, EmailContent.AUTHORITY, |
| Mailbox.SYNC_EXTRA_MAILBOX_ID_PUSH_ONLY); |
| } |
| |
| public EasPingSyncHandler(final Context context, final Account account, |
| final EmailSyncAdapterService.SyncHandlerSynchronizer syncHandlerMap) { |
| super(context, account); |
| mContentResolver = context.getContentResolver(); |
| mPingTask = new PingTask(syncHandlerMap); |
| } |
| |
| /** |
| * Start pinging as an {@link AsyncTask}. |
| */ |
| public void startPing() { |
| mPingTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| |
| @Override |
| public synchronized boolean stop(final int reason) { |
| // TODO: Consider also interrupting mPingTask in the non-refresh case. |
| // (Yes, this override currently exists purely for this comment.) |
| return super.stop(reason); |
| } |
| } |