blob: 5e77b45292f3d3b11737da6b3ab3322b4db563cc [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange.service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.net.TrafficStats;
import android.os.Bundle;
import android.text.format.DateUtils;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Mailbox;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.exchange.adapter.AbstractSyncParser;
import com.android.exchange.adapter.Parser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.exchange.eas.EasProvision;
import com.android.mail.utils.LogUtils;
import org.apache.http.HttpStatus;
import java.io.IOException;
import java.io.InputStream;
/**
* Base class for syncing a single collection from an Exchange server. A "collection" is a single
* mailbox, or contacts for an account, or calendar for an account. (Tasks is part of the protocol
* but not implemented.)
* A single {@link ContentResolver#requestSync} for a single collection corresponds to a single
* object (of the appropriate subclass) being created and {@link #performSync} being called on it.
* This in turn will result in one or more Sync POST requests being sent to the Exchange server;
* from the client's point of view, these multiple Exchange Sync requests are all part of the same
* "sync" (i.e. the fact that there are multiple requests to the server is a detail of the Exchange
* protocol).
* Different collection types (e.g. mail, contacts, calendar) should subclass this class and
* implement the various abstract functions. The majority of how the sync flow is common to all,
* aside from a few details and the {@link Parser} used.
* Details on how this class (and Exchange Sync) works:
* - Overview MSDN link: http://msdn.microsoft.com/en-us/library/ee159766(v=exchg.80).aspx
* - Sync MSDN link: http://msdn.microsoft.com/en-us/library/gg675638(v=exchg.80).aspx
* - The very first time, the client sends a Sync request with SyncKey = 0 and no other parameters.
* This initial Sync request simply gets us a real SyncKey.
* TODO: We should add the initial Sync to EasAccountSyncHandler.
* - Non-initial Sync requests can be for one or more collections; this implementation does one at
* a time. TODO: allow sync for multiple collections to be aggregated?
* - For each collection, we send SyncKey, ServerId, other modifiers, Options, and Commands. The
* protocol has a specific order in which these elements must appear in the request.
* - {@link #buildEasRequest} forms the XML for the request, using {@link #setInitialSyncOptions},
* {@link #setNonInitialSyncOptions}, and {@link #setUpsyncCommands} to fill in the details
* specific for each collection type.
* - The Sync response may specify that there's more data available on the server, in which case
* we keep sending Sync requests to get that data.
* - The ordering constraints and other details may require subclasses to have member variables to
* store state between the various calls while performing a single Sync request. These may need
* to be reset between Sync requests to the Exchange server. Additionally, there are possibly
* other necessary cleanups after parsing a Sync response. These are handled in {@link #cleanup}.
*/
public abstract class EasSyncHandler extends EasServerConnection {
private static final String TAG = Eas.LOG_TAG;
/** Window sizes for PIM (contact & calendar) sync options. */
public static final int PIM_WINDOW_SIZE_CONTACTS = 10;
public static final int PIM_WINDOW_SIZE_CALENDAR = 10;
// TODO: For each type of failure, provide info about why.
protected static final int SYNC_RESULT_FAILED = -1;
protected static final int SYNC_RESULT_DONE = 0;
protected static final int SYNC_RESULT_MORE_AVAILABLE = 1;
/** Maximum number of Sync requests we'll send to the Exchange server in one sync attempt. */
private static final int MAX_LOOPING_COUNT = 100;
protected final ContentResolver mContentResolver;
protected final Mailbox mMailbox;
protected final Bundle mSyncExtras;
protected final SyncResult mSyncResult;
protected EasSyncHandler(final Context context, final ContentResolver contentResolver,
final Account account, final Mailbox mailbox, final Bundle syncExtras,
final SyncResult syncResult) {
super(context, account);
mContentResolver = contentResolver;
mMailbox = mailbox;
mSyncExtras = syncExtras;
mSyncResult = syncResult;
}
/**
* Create an instance of the appropriate subclass to handle sync for mailbox.
* @param context
* @param contentResolver
* @param accountManagerAccount The {@link android.accounts.Account} for this sync.
* @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 android.accounts.Account accountManagerAccount,
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:
case Mailbox.TYPE_DRAFTS:
case Mailbox.TYPE_SENT:
case Mailbox.TYPE_TRASH:
return new EasMailboxSyncHandler(context, contentResolver, account, mailbox,
syncExtras, syncResult);
case Mailbox.TYPE_CALENDAR:
return new EasCalendarSyncHandler(context, contentResolver,
accountManagerAccount, account, mailbox, syncExtras, syncResult);
case Mailbox.TYPE_CONTACTS:
return new EasContactsSyncHandler(context, contentResolver,
accountManagerAccount, account, mailbox, syncExtras, syncResult);
}
}
// Unknown mailbox type.
return null;
}
// Interface for subclasses to implement:
// Subclasses must implement the abstract functions below to provide the information needed by
// performSync.
/**
* Get the flag for traffic bookkeeping for this sync type.
* @return The appropriate value from {@link TrafficFlags} for this sync.
*/
protected abstract int getTrafficFlag();
/**
* Get the sync key for this mailbox.
* @return The sync key for the object being synced. "0" means this is the first sync. If
* there is an error in getting the sync key, this function returns null.
*/
protected String getSyncKey() {
if (mMailbox == null) {
return null;
}
if (mMailbox.mSyncKey == null) {
mMailbox.mSyncKey = "0";
}
return mMailbox.mSyncKey;
}
/**
* Get the folder class name for this mailbox.
* @return The string for this folder class, as defined by the Exchange spec.
*/
// TODO: refactor this to be the same strings as EasPingSyncHandler#handleOneMailbox.
protected abstract String getFolderClassName();
/**
* Return an {@link AbstractSyncParser} appropriate for this sync type and response.
* @param is The {@link InputStream} for the {@link EasResponse} for this sync.
* @return The {@link AbstractSyncParser} for this response.
* @throws IOException
*/
protected abstract AbstractSyncParser getParser(final InputStream is) throws IOException;
/**
* Add to the {@link Serializer} for this sync the child elements of a Collection needed for an
* initial sync for this collection.
* @param s The {@link Serializer} for this sync.
* @throws IOException
*/
protected abstract void setInitialSyncOptions(final Serializer s) throws IOException;
/**
* Add to the {@link Serializer} for this sync the child elements of a Collection needed for a
* non-initial sync for this collection, OTHER THAN Commands (which are written by
* {@link #setUpsyncCommands}.
* @param s The {@link Serializer} for this sync.
* @throws IOException
*/
protected abstract void setNonInitialSyncOptions(final Serializer s) throws IOException;
/**
* Add all Commands to the {@link Serializer} for this Sync request. Strictly speaking, it's
* not all Upsync requests since Fetch is also a command, but largely that's what this section
* is used for.
* @param s The {@link Serializer} for this sync.
* @throws IOException
*/
protected abstract void setUpsyncCommands(final Serializer s) throws IOException;
/**
* Perform any necessary cleanup after processing a Sync response.
*/
protected abstract void cleanup(final int syncResult);
// End of abstract functions.
/**
* Shared non-initial sync options for PIM (contacts & calendar) objects.
*
* @param s The {@link com.android.exchange.adapter.Serializer} for this sync request.
* @param filter The lookback to use, or null if no lookback is desired.
* @param windowSize
* @throws IOException
*/
protected void setPimSyncOptions(final Serializer s, final String filter, int windowSize)
throws IOException {
s.tag(Tags.SYNC_DELETES_AS_MOVES);
s.tag(Tags.SYNC_GET_CHANGES);
s.data(Tags.SYNC_WINDOW_SIZE, String.valueOf(windowSize));
s.start(Tags.SYNC_OPTIONS);
// Set the filter (lookback), if provided
if (filter != null) {
s.data(Tags.SYNC_FILTER_TYPE, filter);
}
// Set the truncation amount and body type
if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
s.start(Tags.BASE_BODY_PREFERENCE);
// Plain text
s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
s.end();
} else {
s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
}
s.end();
}
/**
* Create and populate the {@link Serializer} for this Sync POST to the Exchange server.
* @param syncKey The sync key to use for this request.
* @param initialSync Whether this sync is the first for this object.
* @return The {@link Serializer} for to use for this request.
* @throws IOException
*/
private Serializer buildEasRequest(final String syncKey, final boolean initialSync)
throws IOException {
final String className = getFolderClassName();
LogUtils.i(TAG, "Syncing account %d mailbox %d (class %s) with syncKey %s", mAccount.mId,
mMailbox.mId, className, syncKey);
final Serializer s = new Serializer();
s.start(Tags.SYNC_SYNC);
s.start(Tags.SYNC_COLLECTIONS);
s.start(Tags.SYNC_COLLECTION);
// The "Class" element is removed in EAS 12.1 and later versions
if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
s.data(Tags.SYNC_CLASS, className);
}
s.data(Tags.SYNC_SYNC_KEY, syncKey);
s.data(Tags.SYNC_COLLECTION_ID, mMailbox.mServerId);
if (initialSync) {
setInitialSyncOptions(s);
} else {
setNonInitialSyncOptions(s);
setUpsyncCommands(s);
}
s.end().end().end().done();
return s;
}
/**
* Interpret a successful (HTTP code = 200) response from the Exchange server.
* @param resp The {@link EasResponse} for the Sync message.
* @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
* {@link #SYNC_RESULT_DONE} as appropriate for the server response.
*/
private int parse(final EasResponse resp) {
try {
final AbstractSyncParser parser = getParser(resp.getInputStream());
final boolean moreAvailable = parser.parse();
if (moreAvailable) {
return SYNC_RESULT_MORE_AVAILABLE;
}
} catch (final Parser.EmptyStreamException e) {
// This indicates a compressed response which was empty, which is OK.
} catch (final IOException e) {
return SYNC_RESULT_FAILED;
} catch (final CommandStatusException e) {
return SYNC_RESULT_FAILED;
}
return SYNC_RESULT_DONE;
}
/**
* Send one Sync POST to the Exchange server, and handle the response.
* @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
* {@link #SYNC_RESULT_DONE} as appropriate for the server response.
* @param syncResult
*/
private int performOneSync(SyncResult syncResult) {
final String syncKey = getSyncKey();
if (syncKey == null) {
return SYNC_RESULT_FAILED;
}
final boolean initialSync = syncKey.equals("0");
final EasResponse resp;
try {
final Serializer s = buildEasRequest(syncKey, initialSync);
final long timeout = initialSync ? 120 * DateUtils.SECOND_IN_MILLIS : COMMAND_TIMEOUT;
resp = sendHttpClientPost("Sync", s.toByteArray(), timeout);
} catch (final IOException e) {
LogUtils.e(TAG, "Sync error: ", e);
syncResult.stats.numIoExceptions++;
return SYNC_RESULT_FAILED;
}
final int result;
try {
final int code = resp.getStatus();
if (code == HttpStatus.SC_OK) {
// A successful sync can have an empty response -- this indicates no change.
// In the case of a compressed stream, resp will be non-empty, but parse() handles
// that case.
if (!resp.isEmpty()) {
result = parse(resp);
} else {
result = SYNC_RESULT_DONE;
}
} else {
LogUtils.e(TAG, "Sync failed with Status: " + code);
if (resp.isProvisionError()) {
final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
if (provision.provision(syncResult, mAccount.mId)) {
// We handled the provisioning error, so loop.
result = SYNC_RESULT_MORE_AVAILABLE;
} else {
syncResult.stats.numAuthExceptions++;
return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_SECURITY;
}
} else if (resp.isAuthError()) {
syncResult.stats.numAuthExceptions++;
return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_LOGIN;
} else {
syncResult.stats.numParseExceptions++;
return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_OTHER;
}
}
} finally {
resp.close();
}
cleanup(result);
if (initialSync && result != SYNC_RESULT_FAILED) {
// TODO: Handle Automatic Lookback
}
return result;
}
/**
* 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).
* This function can send multiple Sync messages to the Exchange server, up to
* {@link #MAX_LOOPING_COUNT}, due to the server replying to a Sync request with MoreAvailable.
* In the case of errors, this function should not attempt any retries, but rather should
* set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle
* any it.
* @param syncResult
*/
public final void performSync(SyncResult syncResult) {
// Set up traffic stats bookkeeping.
final int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
TrafficStats.setThreadStatsTag(trafficFlags | getTrafficFlag());
// TODO: Properly handle UI status updates.
//syncMailboxStatus(EmailServiceStatus.IN_PROGRESS, 0);
int result = SYNC_RESULT_MORE_AVAILABLE;
int loopingCount = 0;
String key = getSyncKey();
while (result == SYNC_RESULT_MORE_AVAILABLE && loopingCount < MAX_LOOPING_COUNT) {
result = performOneSync(syncResult);
// TODO: Clear pending request queue.
++loopingCount;
final String newKey = getSyncKey();
if (result == SYNC_RESULT_MORE_AVAILABLE && key.equals(newKey)) {
LogUtils.e(TAG,
"Server has more data but we have the same key: %s loopingCount: %d",
key, loopingCount);
}
key = newKey;
}
if (result == SYNC_RESULT_MORE_AVAILABLE) {
// TODO: Signal caller that it probably wants to sync again.
}
}
}