blob: b4aa39993575e164533ab878ddb767010f1fbe71 [file] [log] [blame]
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();
}
}