Fix upsync for email.
Calendar & Contact upsync are still stubs.
Change-Id: I62c168ce7e7aef449d6ff5a89919aed1fd6ce5c8
diff --git a/src/com/android/exchange/EasAuthenticationException.java b/src/com/android/exchange/EasAuthenticationException.java
index f5b14b9..2e7847f 100644
--- a/src/com/android/exchange/EasAuthenticationException.java
+++ b/src/com/android/exchange/EasAuthenticationException.java
@@ -25,7 +25,7 @@
public class EasAuthenticationException extends IOException {
private static final long serialVersionUID = 1L;
- EasAuthenticationException() {
+ public EasAuthenticationException() {
super();
}
}
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index e729809..8a50e86 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -1076,7 +1076,7 @@
if (status == HttpStatus.SC_OK) {
if (!resp.isEmpty()) {
InputStream is = resp.getInputStream();
- MoveItemsParser p = new MoveItemsParser(is, this);
+ MoveItemsParser p = new MoveItemsParser(is);
p.parse();
int statusCode = p.getStatusCode();
ContentValues cv = new ContentValues();
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index 09b908a..b364d91 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -161,8 +161,8 @@
// we sync folders"...
throw new IOException();
} else if (status == 7) {
- // TODO: Fix this.
- //mService.mUpsyncFailed = true;
+ // TODO: Fix this. The handling here used to be pretty bogus, and it's not
+ // obvious that simply forcing another resync makes sense here.
moreAvailable = true;
} else {
// Access, provisioning, transient, etc.
diff --git a/src/com/android/exchange/adapter/MoveItemsParser.java b/src/com/android/exchange/adapter/MoveItemsParser.java
index 542331d..a654c76 100644
--- a/src/com/android/exchange/adapter/MoveItemsParser.java
+++ b/src/com/android/exchange/adapter/MoveItemsParser.java
@@ -15,7 +15,7 @@
package com.android.exchange.adapter;
-import com.android.exchange.EasSyncService;
+import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -24,7 +24,7 @@
* Parse the result of a MoveItems command.
*/
public class MoveItemsParser extends Parser {
- private final EasSyncService mService;
+ private static final String TAG = "MoveItemsParser";
private int mStatusCode = 0;
private String mNewServerId;
@@ -42,9 +42,8 @@
public static final int STATUS_CODE_REVERT = 2;
public static final int STATUS_CODE_RETRY = 3;
- public MoveItemsParser(InputStream in, EasSyncService service) throws IOException {
+ public MoveItemsParser(InputStream in) throws IOException {
super(in);
- mService = service;
}
public int getStatusCode() {
@@ -83,11 +82,11 @@
}
if (status != STATUS_SUCCESS) {
// There's not much to be done if this fails
- mService.userLog("Error in MoveItems: " + status);
+ LogUtils.i(TAG, "Error in MoveItems: %d", status);
}
} else if (tag == Tags.MOVE_DSTMSGID) {
mNewServerId = getValue();
- mService.userLog("Moved message id is now: " + mNewServerId);
+ LogUtils.i(TAG, "Moved message id is now: %s", mNewServerId);
} else {
skipTag();
}
diff --git a/src/com/android/exchange/service/EasCalendarSyncHandler.java b/src/com/android/exchange/service/EasCalendarSyncHandler.java
index da37e6e..44c262f 100644
--- a/src/com/android/exchange/service/EasCalendarSyncHandler.java
+++ b/src/com/android/exchange/service/EasCalendarSyncHandler.java
@@ -24,7 +24,7 @@
import java.io.InputStream;
/**
- *
+ * Performs an Exchange Sync for a Calendar collection.
*/
public class EasCalendarSyncHandler extends EasSyncHandler {
@@ -121,4 +121,8 @@
}
+ @Override
+ protected void cleanup(final int syncResult) {
+ // Nothing to do.
+ }
}
diff --git a/src/com/android/exchange/service/EasContactsSyncHandler.java b/src/com/android/exchange/service/EasContactsSyncHandler.java
index 6eeac87..773aee4 100644
--- a/src/com/android/exchange/service/EasContactsSyncHandler.java
+++ b/src/com/android/exchange/service/EasContactsSyncHandler.java
@@ -160,4 +160,10 @@
protected void setUpsyncCommands(final Serializer s) throws IOException {
}
+
+
+ @Override
+ protected void cleanup(final int syncResult) {
+
+ }
}
diff --git a/src/com/android/exchange/service/EasMailboxSyncHandler.java b/src/com/android/exchange/service/EasMailboxSyncHandler.java
index 0798946..60f3316 100644
--- a/src/com/android/exchange/service/EasMailboxSyncHandler.java
+++ b/src/com/android/exchange/service/EasMailboxSyncHandler.java
@@ -1,10 +1,16 @@
package com.android.exchange.service;
+import android.content.ContentProviderOperation;
import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.database.Cursor;
import android.os.Bundle;
+import android.os.RemoteException;
+import android.text.format.DateUtils;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.provider.Account;
@@ -15,19 +21,118 @@
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.SyncWindow;
import com.android.exchange.Eas;
+import com.android.exchange.EasAuthenticationException;
+import com.android.exchange.EasResponse;
import com.android.exchange.adapter.AbstractSyncParser;
import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.exchange.adapter.MoveItemsParser;
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.io.InputStream;
import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
/**
* Performs an Exchange mailbox sync for "normal" mailboxes.
*/
public class EasMailboxSyncHandler extends EasSyncHandler {
+ private static final String TAG = "EasMailboxSyncHandler";
+
+ /**
+ * The projection used for building the fetch request list.
+ */
+ private static final String[] FETCH_REQUEST_PROJECTION = { SyncColumns.SERVER_ID };
+ private static final int FETCH_REQUEST_SERVER_ID = 0;
+
+ /**
+ * The projection used for querying message tables for the purpose of determining what needs
+ * to be set as a sync update.
+ */
+ private static final String[] UPDATES_PROJECTION = { MessageColumns.ID,
+ MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
+ MessageColumns.FLAGS, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE };
+ private static final int UPDATES_ID_COLUMN = 0;
+ private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
+ private static final int UPDATES_SERVER_ID_COLUMN = 2;
+ private static final int UPDATES_FLAG_COLUMN = 3;
+ private static final int UPDATES_READ_COLUMN = 4;
+ private static final int UPDATES_FAVORITE_COLUMN = 5;
+
+ /**
+ * Message flags value to signify that the message has been moved, and eventually needs to be
+ * deleted.
+ */
+ public static final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
+
+ /**
+ * The selection for moved messages that get deleted after a successful sync.
+ */
+ private static final String WHERE_MAILBOX_KEY_AND_MOVED =
+ MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
+ MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
+
+ private static final String EMAIL_WINDOW_SIZE = "5";
+
+ private static final String WHERE_BODY_SOURCE_MESSAGE_KEY =
+ EmailContent.Body.SOURCE_MESSAGE_KEY + "=?";
+
+ // State needed across multiple functions during a Sync.
+ // TODO: We should perhaps invert the meaning of mDeletedMessages & mUpdatedMessages and
+ // store the values we want to *retain* after a successful upsync.
+
+ /**
+ * List of message ids that were read from Message_Deletes and were sent in the Commands section
+ * of the current sync.
+ */
+ private final ArrayList<Long> mDeletedMessages = new ArrayList<Long>();
+
+ /**
+ * List of message ids that were read from Message_Updates and were sent in the Commands section
+ * of the current sync.
+ */
+ private final ArrayList<Long> mUpdatedMessages = new ArrayList<Long>();
+
+ /**
+ * List of server ids for messages to fetch from the server.
+ */
+ private final ArrayList<String> mMessagesToFetch = new ArrayList<String>();
+
+ /**
+ * Holds all the data needed to process a MoveItems request.
+ */
+ private static class MoveRequest {
+ public final long messageId;
+ public final String messageServerId;
+ public final int messageFlags;
+ public final long sourceFolderId;
+ public final String sourceFolderServerId;
+ public final String destFolderServerId;
+
+ public MoveRequest(final long _messageId, final String _messageServerId,
+ final int _messageFlags,
+ final long _sourceFolderId, final String _sourceFolderServerId,
+ final String _destFolderServerId) {
+ messageId = _messageId;
+ messageServerId = _messageServerId;
+ messageFlags = _messageFlags;
+ sourceFolderId = _sourceFolderId;
+ sourceFolderServerId = _sourceFolderServerId;
+ destFolderServerId = _destFolderServerId;
+ }
+ }
+
+ /**
+ * List of all MoveRequests, i.e. all messages which have different mailboxes than they used to.
+ */
+ private final ArrayList<MoveRequest> mMessagesToMove = new ArrayList<MoveRequest>();
public EasMailboxSyncHandler(final Context context, final ContentResolver contentResolver,
final Account account, final Mailbox mailbox, final Bundle syncExtras,
@@ -35,26 +140,6 @@
super(context, contentResolver, account, mailbox, syncExtras, syncResult);
}
- private static final String[] FETCH_REQUEST_PROJECTION =
- new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID};
- private static final int FETCH_REQUEST_RECORD_ID = 0;
- private static final int FETCH_REQUEST_SERVER_ID = 1;
-
- private static final String EMAIL_WINDOW_SIZE = "5";
-
- /**
- * Holder for fetch request information (record id and server id)
- */
- private static class FetchRequest {
- final long messageId;
- final String serverId;
-
- FetchRequest(final long _messageId, final String _serverId) {
- messageId = _messageId;
- serverId = _serverId;
- }
- }
-
private String getEmailFilter() {
int syncLookback = mMailbox.mSyncLookback;
if (syncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN
@@ -81,26 +166,23 @@
}
}
- private ArrayList<FetchRequest> createFetchRequestList(final boolean initialSync) {
- final ArrayList<FetchRequest> fetchRequestList = new ArrayList<FetchRequest>();
- if (!initialSync) {
- // Find partially loaded messages; this should typically be a rare occurrence
- final Cursor c = mContentResolver.query(Message.CONTENT_URI, FETCH_REQUEST_PROJECTION,
- MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
- MessageColumns.MAILBOX_KEY + "=?", new String[] {Long.toString(mMailbox.mId)},
- null);
-
+ /**
+ * Find partially loaded messages and add their server ids to {@link #mMessagesToFetch}.
+ */
+ private void addToFetchRequestList() {
+ final Cursor c = mContentResolver.query(Message.CONTENT_URI, FETCH_REQUEST_PROJECTION,
+ MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
+ MessageColumns.MAILBOX_KEY + "=?", new String[] {Long.toString(mMailbox.mId)},
+ null);
+ if (c != null) {
try {
- // Put all of these messages into a list; we'll need both id and server id
while (c.moveToNext()) {
- fetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID),
- c.getString(FETCH_REQUEST_SERVER_ID)));
+ mMessagesToFetch.add(c.getString(FETCH_REQUEST_SERVER_ID));
}
} finally {
c.close();
}
}
- return fetchRequestList;
}
@Override
@@ -137,15 +219,15 @@
@Override
protected void setNonInitialSyncOptions(final Serializer s) throws IOException {
+ // Check for messages that aren't fully loaded.
+ addToFetchRequestList();
// The "empty" case is typical; we send a request for changes, and also specify a sync
// window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
// truncation
// If there are fetch requests, we only want the fetches (i.e. no changes from the server)
// so we turn MIME support off. Note that we are always using EAS 2.5 if there are fetch
// requests
- // TODO: Fix.
- final boolean hasFetchRequests = false;
- if (!hasFetchRequests) {
+ if (mMessagesToFetch.isEmpty()) {
// Permanently delete if in trash mailbox
// In Exchange 2003, deletes-as-moves tag = true; no tag = false
// In Exchange 2007 and up, deletes-as-moves tag is "0" (false) or "1" (true)
@@ -181,17 +263,434 @@
}
s.end();
} else {
+ // If we have any messages that are not fully loaded, ask for plain text rather than
+ // MIME, to guarantee we'll get usable text body. This also means we should NOT ask for
+ // new messages -- we only want data for the message explicitly fetched.
s.start(Tags.SYNC_OPTIONS);
- // Ask for plain text, rather than MIME data. This guarantees that we'll get a usable
- // text body
s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
s.end();
}
}
+ /**
+ * Check whether a message is referenced by another (and therefore must be kept around).
+ * @param messageId The id of the message to check.
+ * @return Whether the message in question is referenced by another message.
+ */
+ private boolean messageReferenced(final long messageId) {
+ final Cursor c = mContentResolver.query(EmailContent.Body.CONTENT_URI,
+ EmailContent.Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
+ new String[] {Long.toString(messageId)}, null);
+ if (c != null) {
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Write the command to delete a message to the {@link Serializer}.
+ * @param s The {@link Serializer} for this sync command.
+ * @param serverId The server id for the message to delete.
+ * @param firstCommand Whether any sync commands have already been written to s.
+ * @throws IOException
+ */
+ private void addDeleteMessageCommand(final Serializer s, final String serverId,
+ final boolean firstCommand) throws IOException {
+ if (firstCommand) {
+ s.start(Tags.SYNC_COMMANDS);
+ }
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ }
+
+ /**
+ * Adds a sync delete command for all messages in the Message_Deletes table.
+ * @param s The {@link Serializer} for this sync command.
+ * @param hasCommands Whether any Commands have already been written to s.
+ * @return Whether this function wrote any commands to s.
+ * @throws IOException
+ */
+ private boolean addDeletedCommands(final Serializer s, final boolean hasCommands)
+ throws IOException {
+ boolean wroteCommands = false;
+ final Cursor c = mContentResolver.query(Message.DELETED_CONTENT_URI,
+ Message.ID_COLUMNS_PROJECTION, MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId,
+ null, null);
+ if (c != null) {
+ try {
+ while (c.moveToNext()) {
+ final String serverId = c.getString(Message.ID_COLUMNS_SYNC_SERVER_ID);
+ final long messageId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
+ // Only upsync delete commands for messages that have server ids and are not
+ // referenced by other messages.
+ if (serverId != null && !messageReferenced(messageId)) {
+ addDeleteMessageCommand(s, serverId, wroteCommands || hasCommands);
+ wroteCommands = true;
+ mDeletedMessages.add(messageId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ return wroteCommands;
+ }
+
+ /**
+ * Create date/time in RFC8601 format. Oddly enough, for calendar date/time, Microsoft uses
+ * a different format that excludes the punctuation (this is why I'm not putting this in a
+ * parent class)
+ */
+ private static String formatDateTime(final Calendar calendar) {
+ final StringBuilder sb = new StringBuilder();
+ //YYYY-MM-DDTHH:MM:SS.MSSZ
+ sb.append(calendar.get(Calendar.YEAR));
+ sb.append('-');
+ sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MONTH) + 1));
+ sb.append('-');
+ sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
+ sb.append('T');
+ sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
+ sb.append(':');
+ sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MINUTE)));
+ sb.append(':');
+ sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.SECOND)));
+ sb.append(".000Z");
+ return sb.toString();
+ }
+
+ /**
+ * Get the server id for a mailbox from the content provider.
+ * @param mailboxId The id of the mailbox we're interested in.
+ * @return The server id for the mailbox.
+ */
+ private String getServerIdForMailbox(final long mailboxId) {
+ final Cursor c = mContentResolver.query(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
+ new String [] { EmailContent.MailboxColumns.SERVER_ID }, null, null, null);
+ if (c != null) {
+ try {
+ if (c.moveToNext()) {
+ return c.getString(0);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * For a message that's in the Message_Updates table, add a sync command to the
+ * {@link Serializer} if appropriate, and add the message to a list if it should be removed from
+ * Message_Updates.
+ * @param updatedMessageCursor The {@link Cursor} positioned at the message from Message_Updates
+ * that we're processing.
+ * @param s The {@link Serializer} for this sync command.
+ * @param hasCommands Whether the {@link Serializer} already has sync commands added to it.
+ * @param trashMailboxId The id for the trash mailbox.
+ * @return Whether we added a sync command to s.
+ * @throws IOException
+ */
+ private boolean handleOneUpdatedMessage(final Cursor updatedMessageCursor, final Serializer s,
+ final boolean hasCommands, final long trashMailboxId) throws IOException {
+ final long messageId = updatedMessageCursor.getLong(UPDATES_ID_COLUMN);
+ // Get the current state of this message (updated table has original state).
+ final Cursor currentCursor = mContentResolver.query(
+ ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
+ UPDATES_PROJECTION, null, null, null);
+ if (currentCursor == null) {
+ // If, somehow, the message isn't still around, we still want to handle it as having
+ // been updated so that it gets removed from the updated table.
+ mUpdatedMessages.add(messageId);
+ return false;
+ }
+
+ try {
+ if (currentCursor.moveToFirst()) {
+ final String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
+ if (serverId == null) {
+ // No serverId means there's nothing to do, but we should still remove from the
+ // updated table.
+ mUpdatedMessages.add(messageId);
+ return false;
+ }
+
+ final long currentMailboxId = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
+ final int currentFlags = currentCursor.getInt(UPDATES_FLAG_COLUMN);
+
+ // Handle message deletion (i.e. move to trash).
+ if (currentMailboxId == trashMailboxId) {
+ mUpdatedMessages.add(messageId);
+ addDeleteMessageCommand(s, serverId, !hasCommands);
+ // Also mark the message as moved in the DB (so the copy will be deleted if/when
+ // the server version is synced)
+ final ContentValues cv = new ContentValues(1);
+ cv.put(MessageColumns.FLAGS, currentFlags | MESSAGE_FLAG_MOVED_MESSAGE);
+ mContentResolver.update(
+ ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
+ cv, null, null);
+ return true;
+ }
+
+ // Handle message moved.
+ final long originalMailboxId =
+ updatedMessageCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
+ if (currentMailboxId != originalMailboxId) {
+ final String sourceMailboxId = getServerIdForMailbox(originalMailboxId);
+ final String destMailboxId;
+ if (sourceMailboxId != null) {
+ destMailboxId = getServerIdForMailbox(currentMailboxId);
+ } else {
+ destMailboxId = null;
+ }
+ if (destMailboxId != null) {
+ mMessagesToMove.add(
+ new MoveRequest(messageId, serverId, currentFlags,
+ originalMailboxId, sourceMailboxId, destMailboxId));
+ // Since we don't want to remove this message from updated table until it
+ // downsyncs, we do not add it to updatedIds.
+ } else {
+ // TODO: If the message's mailboxes aren't there, handle it better.
+ }
+ } else {
+ mUpdatedMessages.add(messageId);
+ }
+
+ final int favorite;
+ final boolean favoriteChanged;
+ // We can only send flag changes to the server in 12.0 or later
+ if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ favorite = currentCursor.getInt(UPDATES_FAVORITE_COLUMN);
+ favoriteChanged =
+ favorite != updatedMessageCursor.getInt(UPDATES_FAVORITE_COLUMN);
+ } else {
+ favorite = 0;
+ favoriteChanged = false;
+ }
+
+ final int read = currentCursor.getInt(UPDATES_READ_COLUMN);
+ final boolean readChanged =
+ read != updatedMessageCursor.getInt(UPDATES_READ_COLUMN);
+
+ if (favoriteChanged || readChanged) {
+ if (!hasCommands) {
+ s.start(Tags.SYNC_COMMANDS);
+ }
+ s.start(Tags.SYNC_CHANGE);
+ s.data(Tags.SYNC_SERVER_ID, serverId);
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ if (readChanged) {
+ s.data(Tags.EMAIL_READ, Integer.toString(read));
+ }
+
+ // "Flag" is a relatively complex concept in EAS 12.0 and above. It is not only
+ // the boolean "favorite" that we think of in Gmail, but it also represents a
+ // follow up action, which can include a subject, start and due dates, and even
+ // recurrences. We don't support any of this as yet, but EAS 12.0 and higher
+ // require that a flag contain a status, a type, and four date fields, two each
+ // for start date and end (due) date.
+ if (favoriteChanged) {
+ if (favorite != 0) {
+ // Status 2 = set flag
+ s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
+ // "FollowUp" is the standard type
+ s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
+ final long now = System.currentTimeMillis();
+ final Calendar calendar =
+ GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
+ calendar.setTimeInMillis(now);
+ // Flags are required to have a start date and end date (duplicated)
+ // First, we'll set the current date/time in GMT as the start time
+ String utc = formatDateTime(calendar);
+ s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
+ // And then we'll use one week from today for completion date
+ calendar.setTimeInMillis(now + DateUtils.WEEK_IN_MILLIS);
+ utc = formatDateTime(calendar);
+ s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
+ s.end();
+ } else {
+ s.tag(Tags.EMAIL_FLAG);
+ }
+ }
+ s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE
+ return true;
+ }
+ }
+ } finally {
+ currentCursor.close();
+ }
+ return false;
+ }
+
+ /**
+ * Send all message move requests and process responses.
+ * TODO: Make this just one request/response, which requires changes to the parser.
+ * @throws IOException
+ */
+ private void performMessageMove() throws IOException {
+
+ for (final MoveRequest req : mMessagesToMove) {
+ final Serializer s = new Serializer();
+ s.start(Tags.MOVE_MOVE_ITEMS);
+ s.start(Tags.MOVE_MOVE);
+ s.data(Tags.MOVE_SRCMSGID, req.messageServerId);
+ s.data(Tags.MOVE_SRCFLDID, req.sourceFolderServerId);
+ s.data(Tags.MOVE_DSTFLDID, req.destFolderServerId);
+ s.end();
+ s.end().done();
+ final EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
+ try {
+ final int status = resp.getStatus();
+ if (status == HttpStatus.SC_OK) {
+ if (!resp.isEmpty()) {
+ final MoveItemsParser p = new MoveItemsParser(resp.getInputStream());
+ p.parse();
+ final int statusCode = p.getStatusCode();
+ final ContentValues cv = new ContentValues();
+ if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+ // Restore the old mailbox id
+ cv.put(MessageColumns.MAILBOX_KEY, req.sourceFolderId);
+ mContentResolver.update(
+ ContentUris.withAppendedId(Message.CONTENT_URI, req.messageId),
+ cv, null, null);
+ } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
+ // Update with the new server id
+ cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
+ cv.put(Message.FLAGS, req.messageFlags | MESSAGE_FLAG_MOVED_MESSAGE);
+ mContentResolver.update(
+ ContentUris.withAppendedId(Message.CONTENT_URI, req.messageId),
+ cv, null, null);
+ }
+ if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
+ || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+ // If we revert or succeed, we no longer need the update information
+ // OR the now-duplicate email (the new copy will be synced down)
+ mContentResolver.delete(ContentUris.withAppendedId(
+ Message.UPDATED_CONTENT_URI, req.messageId), null, null);
+ } else {
+ // In this case, we're retrying, so do nothing. The request will be
+ // handled next sync
+ }
+ }
+ } else if (EasResponse.isAuthError(status)) {
+ throw new EasAuthenticationException();
+ } else {
+ LogUtils.i(TAG, "Move items request failed, code: %d", status);
+ throw new IOException();
+ }
+ } finally {
+ resp.close();
+ }
+ }
+ }
+
+ /**
+ * For each message in Message_Updates, add a sync command if appropriate, and add its id to
+ * our list of processed messages if appropriate.
+ * @param s The {@link Serializer} for this sync request.
+ * @param hasCommands Whether sync commands have already been written to s.
+ * @return Whether this function added any sync commands to s.
+ * @throws IOException
+ */
+ private boolean addUpdatedCommands(final Serializer s, final boolean hasCommands)
+ throws IOException {
+ // Find our trash mailbox, since deletions will have been moved there.
+ final long trashMailboxId =
+ Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
+ final Cursor c = mContentResolver.query(Message.UPDATED_CONTENT_URI, UPDATES_PROJECTION,
+ MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+ boolean addedCommands = false;
+ if (c != null) {
+ try {
+ while (c.moveToNext()) {
+ addedCommands |= handleOneUpdatedMessage(c, s, hasCommands || addedCommands,
+ trashMailboxId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // mMessagesToMove is now populated. If it's non-empty, let's send the move request now.
+ if (!mMessagesToMove.isEmpty()) {
+ performMessageMove();
+ }
+
+ return addedCommands;
+ }
+
+ /**
+ * Add FETCH commands for messages that need a body (i.e. we didn't find it during our earlier
+ * sync; this happens only in EAS 2.5 where the body couldn't be found after parsing the
+ * message's MIME data).
+ * @param s The {@link Serializer} for this sync request.
+ * @param hasCommands Whether sync commands have already been written to s.
+ * @return Whether this function added any sync commands to s.
+ * @throws IOException
+ */
+ private boolean addFetchCommands(final Serializer s, final boolean hasCommands)
+ throws IOException {
+ if (!hasCommands && !mMessagesToFetch.isEmpty()) {
+ s.start(Tags.SYNC_COMMANDS);
+ }
+ for (final String serverId : mMessagesToFetch) {
+ s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, serverId).end();
+ }
+
+ return !mMessagesToFetch.isEmpty();
+ }
+
@Override
protected void setUpsyncCommands(final Serializer s) throws IOException {
+ boolean addedCommands = addDeletedCommands(s, false);
+ addedCommands = addFetchCommands(s, addedCommands);
+ addedCommands = addUpdatedCommands(s, addedCommands);
+ if (addedCommands) {
+ s.end();
+ }
+ }
+ @Override
+ protected void cleanup(final int syncResult) {
+ // After a successful sync, we have things to delete from the DB.
+ if (syncResult != SYNC_RESULT_FAILED) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ // Delete any moved messages (since we've just synced the mailbox, and no longer need
+ // the placeholder message); this prevents duplicates from appearing in the mailbox.
+ // TODO: Verify this still makes sense.
+ ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
+ .withSelection(WHERE_MAILBOX_KEY_AND_MOVED,
+ new String[] {Long.toString(mMailbox.mId)}).build());
+ // Delete any entries in Message_Updates and Message_Deletes that were upsynced.
+ for (final long id: mDeletedMessages) {
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
+ }
+ for (final long id: mUpdatedMessages) {
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
+ }
+ try {
+ mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+ } catch (final RemoteException e) {
+ // TODO: Improve handling.
+ } catch (final OperationApplicationException e) {
+ // TODO: Improve handing.
+ }
+ }
+
+ if (syncResult == SYNC_RESULT_MORE_AVAILABLE) {
+ // Prepare our member variables for another sync request.
+ mDeletedMessages.clear();
+ mUpdatedMessages.clear();
+ mMessagesToFetch.clear();
+ mMessagesToMove.clear();
+ }
}
}
diff --git a/src/com/android/exchange/service/EasSyncHandler.java b/src/com/android/exchange/service/EasSyncHandler.java
index e595e10..149740f 100644
--- a/src/com/android/exchange/service/EasSyncHandler.java
+++ b/src/com/android/exchange/service/EasSyncHandler.java
@@ -27,12 +27,37 @@
import java.io.InputStream;
/**
- * Base class for performing a sync from an Exchange server (i.e. retrieving collection data from
- * the server). A single sync request from the app will result in one or more Sync POST messages to
- * the server, but that's all encapsulated in this class as a single "sync".
+ * 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 {@link 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 = "EasSyncHandler";
@@ -41,9 +66,9 @@
protected static final String PIM_WINDOW_SIZE = "4";
// TODO: For each type of failure, provide info about why.
- private static final int SYNC_RESULT_FAILED = -1;
- private static final int SYNC_RESULT_DONE = 0;
- private static final int SYNC_RESULT_MORE_AVAILABLE = 1;
+ 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;
@@ -132,23 +157,36 @@
protected abstract AbstractSyncParser getParser(final InputStream is) throws IOException;
/**
- * Add sync options to the {@link Serializer} for this sync, if it's the first sync on this
- * mailbox.
+ * 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 sync options to the {@link Serializer} for this sync, if it's not the first sync on this
- * mailbox.
+ * 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.
/**
@@ -207,7 +245,6 @@
setInitialSyncOptions(s);
} else {
setNonInitialSyncOptions(s);
- // TODO: handle when previous iteration's upsync failed.
setUpsyncCommands(s);
}
s.end().end().end().done();
@@ -223,7 +260,9 @@
*/
private int parse(final EasResponse resp) {
try {
- if (getParser(resp.getInputStream()).parse()) {
+ final AbstractSyncParser parser = getParser(resp.getInputStream());
+ final boolean moreAvailable = parser.parse();
+ if (moreAvailable) {
return SYNC_RESULT_MORE_AVAILABLE;
}
} catch (final Parser.EmptyStreamException e) {
@@ -280,9 +319,7 @@
resp.close();
}
- if (result == SYNC_RESULT_DONE) {
- // TODO: target.cleanup() or equivalent
- }
+ cleanup(result);
if (initialSync && result != SYNC_RESULT_FAILED) {
// TODO: Handle Automatic Lookback