| package com.android.exchange.service; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| 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.HostAuth; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.service.EmailServiceStatus; |
| import com.android.exchange.Eas; |
| import com.android.exchange.EasException; |
| import com.android.exchange.EasResponse; |
| import com.android.exchange.adapter.PingParser; |
| import com.android.exchange.adapter.Serializer; |
| import com.android.exchange.adapter.Tags; |
| |
| 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 final ContentResolver mContentResolver; |
| private final PingTask mPingTask; |
| |
| private class PingTask extends AsyncTask<Void, Void, Void> { |
| private static final String AND_FREQUENCY_PING_PUSH = " AND " + |
| MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING + |
| ',' + Mailbox.CHECK_INTERVAL_PUSH + ")"; |
| private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = |
| MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; |
| |
| private final EmailSyncAdapterService.SyncHandlerSynchronizer mSyncHandlerMap; |
| |
| // TODO: The old code used to increase the heartbeat time after successful pings. Is there |
| // a good reason to not just start at the high value? If there's a problem, it'll just fail |
| // early anyway... |
| private static final long PING_HEARTBEAT = 8 * DateUtils.MINUTE_IN_MILLIS; |
| |
| private PingTask(final EmailSyncAdapterService.SyncHandlerSynchronizer syncHandlerMap) { |
| mSyncHandlerMap = syncHandlerMap; |
| } |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| // We keep pinging until we're interrupted, or reach some error condition that |
| // prevents us from proceeding. |
| boolean continuePing = true; |
| while(continuePing) { |
| final Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, |
| Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + '=' + |
| mAccount.mId + AND_FREQUENCY_PING_PUSH, null, null); |
| if (c == null) { |
| // TODO: Signal error: can't read mailbox data. |
| break; |
| } |
| final android.accounts.Account amAccount = new android.accounts.Account( |
| mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| try { |
| Serializer s = null; |
| 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(); |
| final EasResponse resp = sendHttpClientPost("Ping", s.toByteArray(), |
| PING_HEARTBEAT); |
| try { |
| continuePing = handleResponse(resp, amAccount); |
| } finally { |
| resp.close(); |
| } |
| } else { |
| // No mailboxes want to receive a push notification right now. |
| // TODO: Need to set up code that restarts the push when things change. |
| break; |
| } |
| } catch (IOException e) { |
| // TODO: This assumes that all IOExceptions are interruptions. Other sorts may |
| // require additional handling. |
| break; |
| } |
| } |
| |
| mSyncHandlerMap.signalDone(mAccount.mId, false, true); |
| return null; |
| } |
| |
| 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; |
| } |
| } |
| |
| 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, |
| Long.toString(PING_HEARTBEAT/DateUtils.SECOND_IN_MILLIS)); |
| 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; |
| } |
| |
| /** |
| * Parse the response to determine which mailboxes need sync, and request them. |
| * @param resp The response to the Ping from the Exchange server. |
| * @param amAccount The AccountManager Account object for this account. |
| * @return Whether the ping should continue after this returns. For example, if we request |
| * a sync, we should stop pinging, but if the ping timed out, then we should repeat it. |
| * @throws IOException |
| */ |
| private boolean handleResponse(final EasResponse resp, |
| final android.accounts.Account amAccount) throws IOException { |
| final int code = resp.getStatus(); |
| if (code == HttpStatus.SC_OK) { |
| if (resp.isEmpty()) { |
| // Act as if we have an IOException (backoff, etc.) |
| // TODO: Err... is this right? |
| //throw new IOException(); |
| } |
| |
| final PingParser pp = new PingParser(resp.getInputStream()); |
| final boolean parseResult; |
| try { |
| parseResult = pp.parse(); |
| } catch (EasException e) { |
| // TODO: proper error handling |
| return false; |
| } |
| boolean syncRequested = false; |
| if (parseResult) { |
| final ArrayList<String> pingChangeList = pp.getSyncList(); |
| final String[] bindArguments = new String[2]; |
| bindArguments[0] = Long.toString(mAccount.mId); |
| for (final String serverId : pingChangeList) { |
| 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()) { |
| // Request the sync for this mailbox. |
| // TODO: Refactor with code in EmailProvider. |
| final Bundle extras = new Bundle(); |
| extras.putLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, |
| c.getLong(Mailbox.CONTENT_ID_COLUMN)); |
| extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, |
| EmailContent.CONTENT_URI.toString()); |
| extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, |
| "sync_status"); |
| ContentResolver.requestSync(amAccount, getAuthority(c.getInt( |
| Mailbox.CONTENT_TYPE_COLUMN)), extras); |
| syncRequested = true; |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| } |
| |
| // TODO: Handle pp.getSyncStatus(). |
| /* |
| // If our ping completed (status = 1), and wasn't forced and we're |
| // not at the maximum, try increasing timeout by two minutes |
| if (pingResult == PROTOCOL_PING_STATUS_BAD_PARAMETERS || |
| pingResult == PROTOCOL_PING_STATUS_RETRY) { |
| // These errors appear to be server-related (I've seen a bad |
| // parameters result with known good parameters...) |
| // Act as if we have an IOException (backoff, etc.) |
| throw new IOException(); |
| } |
| */ |
| return parseResult && !syncRequested; |
| } else if (EasResponse.isAuthError(code)) { |
| // TODO: signal this error. |
| } |
| return false; |
| } |
| |
| } |
| |
| public EasPingSyncHandler(final Context context, final Account account, |
| final EmailSyncAdapterService.SyncHandlerSynchronizer syncHandlerMap) { |
| super(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv)); |
| mContentResolver = context.getContentResolver(); |
| mPingTask = new PingTask(syncHandlerMap); |
| mPingTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| |
| @Override |
| public synchronized void stop() { |
| // TODO: Consider also interrupting mPingTask in the non-refresh case, or alter refresh |
| // to restart the task. (Yes, this override currently exists purely for this comment.) |
| super.stop(); |
| } |
| } |