blob: fef6c31ac9a3336dbd12d117130d56e39e80ca09 [file] [log] [blame]
package com.android.exchange.adapter;
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.database.Cursor;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import android.provider.CalendarContract;
import android.text.Html;
import android.text.SpannedString;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.android.emailcommon.internet.MimeMessage;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.mail.Address;
import com.android.emailcommon.mail.MeetingInfo;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.PackedString;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.provider.ProviderUnavailableException;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.emailcommon.utility.ConversionUtilities;
import com.android.emailcommon.utility.TextUtilities;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.utility.CalendarUtilities;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Parser for Sync on an email collection.
*/
public class EmailSyncParser extends AbstractSyncParser {
private static final String TAG = Eas.LOG_TAG;
private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = EmailContent.SyncColumns.SERVER_ID
+ "=? and " + EmailContent.MessageColumns.MAILBOX_KEY + "=?";
private final String mMailboxIdAsString;
private final ArrayList<EmailContent.Message>
newEmails = new ArrayList<EmailContent.Message>();
private final ArrayList<EmailContent.Message> fetchedEmails =
new ArrayList<EmailContent.Message>();
private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
new String[] { EmailContent.Message.RECORD_ID, EmailContent.MessageColumns.SUBJECT };
@VisibleForTesting
static final int LAST_VERB_REPLY = 1;
@VisibleForTesting
static final int LAST_VERB_REPLY_ALL = 2;
@VisibleForTesting
static final int LAST_VERB_FORWARD = 3;
private final Policy mPolicy;
// Max times to retry when we get a TransactionTooLargeException exception
private static final int MAX_RETRIES = 10;
// Max number of ops per batch. It could end up more than this but once we detect we are at or
// above this number, we flush.
private static final int MAX_OPS_PER_BATCH = 50;
private boolean mFetchNeeded = false;
private final Map<String, Integer> mMessageUpdateStatus = new HashMap();
public EmailSyncParser(final Context context, final ContentResolver resolver,
final InputStream in, final Mailbox mailbox, final Account account)
throws IOException {
super(context, resolver, in, mailbox, account);
mMailboxIdAsString = Long.toString(mMailbox.mId);
if (mAccount.mPolicyKey != 0) {
mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
} else {
mPolicy = null;
}
}
public EmailSyncParser(final Parser parser, final Context context,
final ContentResolver resolver, final Mailbox mailbox, final Account account)
throws IOException {
super(parser, context, resolver, mailbox, account);
mMailboxIdAsString = Long.toString(mMailbox.mId);
if (mAccount.mPolicyKey != 0) {
mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
} else {
mPolicy = null;
}
}
public boolean fetchNeeded() {
return mFetchNeeded;
}
public Map<String, Integer> getMessageStatuses() {
return mMessageUpdateStatus;
}
public void addData(EmailContent.Message msg, int endingTag) throws IOException {
ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>();
boolean truncated = false;
while (nextTag(endingTag) != END) {
switch (tag) {
case Tags.EMAIL_ATTACHMENTS:
case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
attachmentsParser(atts, msg);
break;
case Tags.EMAIL_TO:
msg.mTo = Address.pack(Address.parse(getValue()));
break;
case Tags.EMAIL_FROM:
Address[] froms = Address.parse(getValue());
if (froms != null && froms.length > 0) {
msg.mDisplayName = froms[0].toFriendly();
}
msg.mFrom = Address.pack(froms);
break;
case Tags.EMAIL_CC:
msg.mCc = Address.pack(Address.parse(getValue()));
break;
case Tags.EMAIL_REPLY_TO:
msg.mReplyTo = Address.pack(Address.parse(getValue()));
break;
case Tags.EMAIL_DATE_RECEIVED:
msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
break;
case Tags.EMAIL_SUBJECT:
msg.mSubject = getValue();
break;
case Tags.EMAIL_READ:
msg.mFlagRead = getValueInt() == 1;
break;
case Tags.BASE_BODY:
bodyParser(msg);
break;
case Tags.EMAIL_FLAG:
msg.mFlagFavorite = flagParser();
break;
case Tags.EMAIL_MIME_TRUNCATED:
truncated = getValueInt() == 1;
break;
case Tags.EMAIL_MIME_DATA:
// We get MIME data for EAS 2.5. First we parse it, then we take the
// html and/or plain text data and store it in the message
if (truncated) {
// If the MIME data is truncated, don't bother parsing it, because
// it will take time and throw an exception anyway when EOF is reached
// In this case, we will load the body separately by tagging the message
// "partially loaded".
// Get the data (and ignore it)
getValue();
userLog("Partially loaded: ", msg.mServerId);
msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
mFetchNeeded = true;
} else {
mimeBodyParser(msg, getValue());
}
break;
case Tags.EMAIL_BODY:
String text = getValue();
msg.mText = text;
break;
case Tags.EMAIL_MESSAGE_CLASS:
String messageClass = getValue();
if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_INVITE;
} else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_CANCEL;
}
break;
case Tags.EMAIL_MEETING_REQUEST:
meetingRequestParser(msg);
break;
case Tags.EMAIL_THREAD_TOPIC:
msg.mThreadTopic = getValue();
break;
case Tags.RIGHTS_LICENSE:
skipParser(tag);
break;
case Tags.EMAIL2_CONVERSATION_ID:
msg.mServerConversationId =
Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
break;
case Tags.EMAIL2_CONVERSATION_INDEX:
// Ignore this byte array since we're not constructing a tree.
getValueBytes();
break;
case Tags.EMAIL2_LAST_VERB_EXECUTED:
int val = getValueInt();
if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
// We aren't required to distinguish between reply and reply all here
msg.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
} else if (val == LAST_VERB_FORWARD) {
msg.mFlags |= EmailContent.Message.FLAG_FORWARDED;
}
break;
default:
skipTag();
}
}
if (atts.size() > 0) {
msg.mAttachments = atts;
}
if ((msg.mFlags & EmailContent.Message.FLAG_INCOMING_MEETING_MASK) != 0) {
String text = TextUtilities.makeSnippetFromHtmlText(
msg.mText != null ? msg.mText : msg.mHtml);
if (TextUtils.isEmpty(text)) {
// Create text for this invitation
String meetingInfo = msg.mMeetingInfo;
if (!TextUtils.isEmpty(meetingInfo)) {
PackedString ps = new PackedString(meetingInfo);
ContentValues values = new ContentValues();
putFromMeeting(ps, MeetingInfo.MEETING_LOCATION, values,
CalendarContract.Events.EVENT_LOCATION);
String dtstart = ps.get(MeetingInfo.MEETING_DTSTART);
if (!TextUtils.isEmpty(dtstart)) {
long startTime = Utility.parseEmailDateTimeToMillis(dtstart);
values.put(CalendarContract.Events.DTSTART, startTime);
}
putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values,
CalendarContract.Events.ALL_DAY);
msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(
mContext, values, null);
msg.mHtml = Html.toHtml(new SpannedString(msg.mText));
}
}
}
}
private static void putFromMeeting(PackedString ps, String field, ContentValues values,
String column) {
String val = ps.get(field);
if (!TextUtils.isEmpty(val)) {
values.put(column, val);
}
}
/**
* Set up the meetingInfo field in the message with various pieces of information gleaned
* from MeetingRequest tags. This information will be used later to generate an appropriate
* reply email if the user chooses to respond
* @param msg the Message being built
* @throws IOException
*/
private void meetingRequestParser(EmailContent.Message msg) throws IOException {
PackedString.Builder packedString = new PackedString.Builder();
while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
switch (tag) {
case Tags.EMAIL_DTSTAMP:
packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
break;
case Tags.EMAIL_START_TIME:
packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
break;
case Tags.EMAIL_END_TIME:
packedString.put(MeetingInfo.MEETING_DTEND, getValue());
break;
case Tags.EMAIL_ORGANIZER:
packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
break;
case Tags.EMAIL_LOCATION:
packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
break;
case Tags.EMAIL_GLOBAL_OBJID:
packedString.put(MeetingInfo.MEETING_UID,
CalendarUtilities.getUidFromGlobalObjId(getValue()));
break;
case Tags.EMAIL_CATEGORIES:
skipParser(tag);
break;
case Tags.EMAIL_RECURRENCES:
recurrencesParser();
break;
case Tags.EMAIL_RESPONSE_REQUESTED:
packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
break;
case Tags.EMAIL_ALL_DAY_EVENT:
if (getValueInt() == 1) {
packedString.put(MeetingInfo.MEETING_ALL_DAY, "1");
}
break;
default:
skipTag();
}
}
if (msg.mSubject != null) {
packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
}
msg.mMeetingInfo = packedString.toString();
}
private void recurrencesParser() throws IOException {
while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
switch (tag) {
case Tags.EMAIL_RECURRENCE:
skipParser(tag);
break;
default:
skipTag();
}
}
}
/**
* Parse a message from the server stream.
* @return the parsed Message
* @throws IOException
*/
private EmailContent.Message addParser() throws IOException, CommandStatusException {
EmailContent.Message msg = new EmailContent.Message();
msg.mAccountKey = mAccount.mId;
msg.mMailboxKey = mMailbox.mId;
msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
// Default to 1 (success) in case we don't get this tag
int status = 1;
while (nextTag(Tags.SYNC_ADD) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
msg.mServerId = getValue();
break;
case Tags.SYNC_STATUS:
status = getValueInt();
break;
case Tags.SYNC_APPLICATION_DATA:
addData(msg, tag);
break;
default:
skipTag();
}
}
// For sync, status 1 = success
if (status != 1) {
throw new CommandStatusException(status, msg.mServerId);
}
return msg;
}
// For now, we only care about the "active" state
private Boolean flagParser() throws IOException {
Boolean state = false;
while (nextTag(Tags.EMAIL_FLAG) != END) {
switch (tag) {
case Tags.EMAIL_FLAG_STATUS:
state = getValueInt() == 2;
break;
default:
skipTag();
}
}
return state;
}
private void bodyParser(EmailContent.Message msg) throws IOException {
String bodyType = Eas.BODY_PREFERENCE_TEXT;
String body = "";
while (nextTag(Tags.EMAIL_BODY) != END) {
switch (tag) {
case Tags.BASE_TYPE:
bodyType = getValue();
break;
case Tags.BASE_DATA:
body = getValue();
break;
default:
skipTag();
}
}
// We always ask for TEXT or HTML; there's no third option
if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
msg.mHtml = body;
} else {
msg.mText = body;
}
}
/**
* Parses untruncated MIME data, saving away the text parts
* @param msg the message we're building
* @param mimeData the MIME data we've received from the server
* @throws IOException
*/
private static void mimeBodyParser(EmailContent.Message msg, String mimeData)
throws IOException {
try {
ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
// The constructor parses the message
MimeMessage mimeMessage = new MimeMessage(in);
// Now process body parts & attachments
ArrayList<Part> viewables = new ArrayList<Part>();
// We'll ignore the attachments, as we'll get them directly from EAS
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(mimeMessage, viewables, attachments);
// parseBodyFields fills in the content fields of the Body
ConversionUtilities.BodyFieldData data =
ConversionUtilities.parseBodyFields(viewables);
// But we need them in the message itself for handling during commit()
msg.setFlags(data.isQuotedReply, data.isQuotedForward);
msg.mSnippet = data.snippet;
msg.mHtml = data.htmlContent;
msg.mText = data.textContent;
} catch (MessagingException e) {
// This would most likely indicate a broken stream
throw new IOException(e);
}
}
private void attachmentsParser(ArrayList<EmailContent.Attachment> atts,
EmailContent.Message msg) throws IOException {
while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
switch (tag) {
case Tags.EMAIL_ATTACHMENT:
case Tags.BASE_ATTACHMENT: // BASE_ATTACHMENT is used in EAS 12.0 and up
attachmentParser(atts, msg);
break;
default:
skipTag();
}
}
}
private void attachmentParser(ArrayList<EmailContent.Attachment> atts,
EmailContent.Message msg) throws IOException {
String fileName = null;
String length = null;
String location = null;
boolean isInline = false;
String contentId = null;
while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
switch (tag) {
// We handle both EAS 2.5 and 12.0+ attachments here
case Tags.EMAIL_DISPLAY_NAME:
case Tags.BASE_DISPLAY_NAME:
fileName = getValue();
break;
case Tags.EMAIL_ATT_NAME:
case Tags.BASE_FILE_REFERENCE:
location = getValue();
break;
case Tags.EMAIL_ATT_SIZE:
case Tags.BASE_ESTIMATED_DATA_SIZE:
length = getValue();
break;
case Tags.BASE_IS_INLINE:
isInline = getValueInt() == 1;
break;
case Tags.BASE_CONTENT_ID:
contentId = getValue();
break;
default:
skipTag();
}
}
if ((fileName != null) && (length != null) && (location != null)) {
EmailContent.Attachment att = new EmailContent.Attachment();
att.mEncoding = "base64";
att.mSize = Long.parseLong(length);
att.mFileName = fileName;
att.mLocation = location;
att.mMimeType = getMimeTypeFromFileName(fileName);
att.mAccountKey = mAccount.mId;
// Save away the contentId, if we've got one (for inline images); note that the
// EAS docs appear to be wrong about the tags used; inline images come with
// contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
if (isInline && !TextUtils.isEmpty(contentId)) {
att.mContentId = contentId;
}
// Check if this attachment can't be downloaded due to an account policy
if (mPolicy != null) {
if (mPolicy.mDontAllowAttachments ||
(mPolicy.mMaxAttachmentSize > 0 &&
(att.mSize > mPolicy.mMaxAttachmentSize))) {
att.mFlags = EmailContent.Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
}
}
atts.add(att);
msg.mFlagAttachment = true;
}
}
/**
* Returns an appropriate mimetype for the given file name's extension. If a mimetype
* cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension,
* if it exists or {@code application/octet-stream}].
* At the moment, this is somewhat lame, since many file types aren't recognized
* @param fileName the file name to ponder
*/
// Note: The MimeTypeMap method currently uses a very limited set of mime types
// A bug has been filed against this issue.
public String getMimeTypeFromFileName(String fileName) {
String mimeType;
int lastDot = fileName.lastIndexOf('.');
String extension = null;
if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
extension = fileName.substring(lastDot + 1).toLowerCase();
}
if (extension == null) {
// A reasonable default for now.
mimeType = "application/octet-stream";
} else {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType == null) {
mimeType = "application/" + extension;
}
}
return mimeType;
}
private Cursor getServerIdCursor(String serverId, String[] projection) {
Cursor c = mContentResolver.query(EmailContent.Message.CONTENT_URI, projection,
WHERE_SERVER_ID_AND_MAILBOX_KEY, new String[] {serverId, mMailboxIdAsString},
null);
if (c == null) throw new ProviderUnavailableException();
if (c.getCount() > 1) {
userLog("Multiple messages with the same serverId/mailbox: " + serverId);
}
return c;
}
@VisibleForTesting
void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
while (nextTag(entryTag) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
String serverId = getValue();
// Find the message in this mailbox with the given serverId
Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
try {
if (c.moveToFirst()) {
deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
if (Eas.USER_LOG) {
userLog("Deleting ", serverId + ", "
+ c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
}
}
} finally {
c.close();
}
break;
default:
skipTag();
}
}
}
@VisibleForTesting
class ServerChange {
final long id;
final Boolean read;
final Boolean flag;
final Integer flags;
ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) {
id = _id;
read = _read;
flag = _flag;
flags = _flags;
}
}
@VisibleForTesting
void changeParser(ArrayList<ServerChange> changes) throws IOException {
String serverId = null;
Boolean oldRead = false;
Boolean oldFlag = false;
int flags = 0;
long id = 0;
while (nextTag(Tags.SYNC_CHANGE) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
serverId = getValue();
Cursor c = getServerIdCursor(serverId, EmailContent.Message.LIST_PROJECTION);
try {
if (c.moveToFirst()) {
userLog("Changing ", serverId);
oldRead = c.getInt(EmailContent.Message.LIST_READ_COLUMN)
== EmailContent.Message.READ;
oldFlag = c.getInt(EmailContent.Message.LIST_FAVORITE_COLUMN) == 1;
flags = c.getInt(EmailContent.Message.LIST_FLAGS_COLUMN);
id = c.getLong(EmailContent.Message.LIST_ID_COLUMN);
}
} finally {
c.close();
}
break;
case Tags.SYNC_APPLICATION_DATA:
changeApplicationDataParser(changes, oldRead, oldFlag, flags, id);
break;
default:
skipTag();
}
}
}
private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
Boolean oldFlag, int oldFlags, long id) throws IOException {
Boolean read = null;
Boolean flag = null;
Integer flags = null;
while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
switch (tag) {
case Tags.EMAIL_READ:
read = getValueInt() == 1;
break;
case Tags.EMAIL_FLAG:
flag = flagParser();
break;
case Tags.EMAIL2_LAST_VERB_EXECUTED:
int val = getValueInt();
// Clear out the old replied/forward flags and add in the new flag
flags = oldFlags & ~(EmailContent.Message.FLAG_REPLIED_TO
| EmailContent.Message.FLAG_FORWARDED);
if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
// We aren't required to distinguish between reply and reply all here
flags |= EmailContent.Message.FLAG_REPLIED_TO;
} else if (val == LAST_VERB_FORWARD) {
flags |= EmailContent.Message.FLAG_FORWARDED;
}
break;
default:
skipTag();
}
}
// See if there are flag changes re: read, flag (favorite) or replied/forwarded
if (((read != null) && !oldRead.equals(read)) ||
((flag != null) && !oldFlag.equals(flag)) || (flags != null)) {
changes.add(new ServerChange(id, read, flag, flags));
}
}
/* (non-Javadoc)
* @see com.android.exchange.adapter.EasContentParser#commandsParser()
*/
@Override
public void commandsParser() throws IOException, CommandStatusException {
while (nextTag(Tags.SYNC_COMMANDS) != END) {
if (tag == Tags.SYNC_ADD) {
newEmails.add(addParser());
} else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
deleteParser(deletedEmails, tag);
} else if (tag == Tags.SYNC_CHANGE) {
changeParser(changedEmails);
} else
skipTag();
}
}
// EAS values for status element of sync responses.
// TODO: Not all are used yet, but I wanted to transcribe all possible values.
public static final int EAS_SYNC_STATUS_SUCCESS = 1;
public static final int EAS_SYNC_STATUS_BAD_SYNC_KEY = 3;
public static final int EAS_SYNC_STATUS_PROTOCOL_ERROR = 4;
public static final int EAS_SYNC_STATUS_SERVER_ERROR = 5;
public static final int EAS_SYNC_STATUS_BAD_CLIENT_DATA = 6;
public static final int EAS_SYNC_STATUS_CONFLICT = 7;
public static final int EAS_SYNC_STATUS_OBJECT_NOT_FOUND = 8;
public static final int EAS_SYNC_STATUS_CANNOT_COMPLETE = 9;
public static final int EAS_SYNC_STATUS_FOLDER_SYNC_NEEDED = 12;
public static final int EAS_SYNC_STATUS_INCOMPLETE_REQUEST = 13;
public static final int EAS_SYNC_STATUS_BAD_HEARTBEAT_VALUE = 14;
public static final int EAS_SYNC_STATUS_TOO_MANY_COLLECTIONS = 15;
public static final int EAS_SYNC_STATUS_RETRY = 16;
public static boolean shouldRetry(final int status) {
return status == EAS_SYNC_STATUS_SERVER_ERROR || status == EAS_SYNC_STATUS_RETRY;
}
/**
* Parse the status for a single message update.
* @param endTag the tag we end with
* @throws IOException
*/
public void messageUpdateParser(int endTag) throws IOException {
// We get serverId and status in the responses
String serverId = null;
int status = -1;
while (nextTag(endTag) != END) {
if (tag == Tags.SYNC_STATUS) {
status = getValueInt();
} else if (tag == Tags.SYNC_SERVER_ID) {
serverId = getValue();
} else {
skipTag();
}
}
if (serverId != null && status != -1) {
mMessageUpdateStatus.put(serverId, status);
}
}
@Override
public void responsesParser() throws IOException {
while (nextTag(Tags.SYNC_RESPONSES) != END) {
if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
messageUpdateParser(tag);
} else if (tag == Tags.SYNC_FETCH) {
try {
fetchedEmails.add(addParser());
} catch (CommandStatusException sse) {
if (sse.mStatus == 8) {
// 8 = object not found; delete the message from EmailProvider
// No other status should be seen in a fetch response, except, perhaps,
// for some temporary server failure
mContentResolver.delete(EmailContent.Message.CONTENT_URI,
WHERE_SERVER_ID_AND_MAILBOX_KEY,
new String[] {sse.mItemId, mMailboxIdAsString});
}
}
}
}
}
@Override
protected void wipe() {
LogUtils.i(TAG, "Wiping mailbox " + mMailbox);
Mailbox.resyncMailbox(mContentResolver, new android.accounts.Account(mAccount.mEmailAddress,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mMailbox.mId);
}
@Override
public boolean parse() throws IOException, CommandStatusException {
final boolean result = super.parse();
return result || fetchNeeded();
}
/**
* Commit all changes. This results in a Binder IPC call which has constraint on the size of
* the data, the docs say it currently 1MB. We set a limit to the size of the message we fetch
* with {@link Eas#EAS12_TRUNCATION_SIZE} & {@link Eas#EAS12_TRUNCATION_SIZE} which are at 200k
* or bellow. As long as these limits are bellow 500k, we should be able to apply a single
* message (the transaction size is about double the message size because Java strings are 16
* bit.
* <b/>
* We first try to apply the changes in normal chunk size {@link #MAX_OPS_PER_BATCH}. If we get
* a {@link TransactionTooLargeException} we try again with but this time, we apply each change
* immediately.
*/
@Override
public void commit() throws RemoteException, OperationApplicationException {
try {
commitImpl(MAX_OPS_PER_BATCH);
} catch (TransactionTooLargeException e) {
// Try again but apply batch after every message. The max message size defined in
// Eas.EAS12_TRUNCATION_SIZE or Eas.EAS2_5_TRUNCATION_SIZE is small enough to fit
// in a single Binder call.
LogUtils.w(TAG, "Transaction too large, retrying in single mode", e);
commitImpl(1);
}
}
public void commitImpl(int maxOpsPerBatch)
throws RemoteException, OperationApplicationException {
// Use a batch operation to handle the changes
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// Maximum size of message text per fetch
int numFetched = fetchedEmails.size();
LogUtils.d(TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d "
+ "numDeleted=%d numChanged=%d",
maxOpsPerBatch,
numFetched,
newEmails.size(),
deletedEmails.size(),
changedEmails.size());
for (EmailContent.Message msg: fetchedEmails) {
// Find the original message's id (by serverId and mailbox)
Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
String id = null;
try {
if (c.moveToFirst()) {
id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
while (c.moveToNext()) {
// This shouldn't happen, but clean up if it does
Long dupId =
Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
userLog("Delete duplicate with id: " + dupId);
deletedEmails.add(dupId);
}
}
} finally {
c.close();
}
// If we find one, we do two things atomically: 1) set the body text for the
// message, and 2) mark the message loaded (i.e. completely loaded)
if (id != null) {
LogUtils.i(TAG, "Fetched body successfully for %s", id);
final String[] bindArgument = new String[] {id};
ops.add(ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
.withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument)
.withValue(EmailContent.Body.TEXT_CONTENT, msg.mText)
.build());
ops.add(ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI)
.withSelection(EmailContent.RECORD_ID + "=?", bindArgument)
.withValue(EmailContent.Message.FLAG_LOADED,
EmailContent.Message.FLAG_LOADED_COMPLETE)
.build());
}
applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
for (EmailContent.Message msg: newEmails) {
msg.addSaveOps(ops);
applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
for (Long id : deletedEmails) {
ops.add(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, id)).build());
AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
if (!changedEmails.isEmpty()) {
// Server wins in a conflict...
for (ServerChange change : changedEmails) {
ContentValues cv = new ContentValues();
if (change.read != null) {
cv.put(EmailContent.MessageColumns.FLAG_READ, change.read);
}
if (change.flag != null) {
cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag);
}
if (change.flags != null) {
cv.put(EmailContent.MessageColumns.FLAGS, change.flags);
}
ops.add(ContentProviderOperation.newUpdate(
ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id))
.withValues(cv)
.build());
}
applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
// We only want to update the sync key here
ContentValues mailboxValues = new ContentValues();
mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
ops.add(ContentProviderOperation.newUpdate(
ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
.withValues(mailboxValues).build());
applyBatchIfNeeded(ops, maxOpsPerBatch, true);
userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
}
// Check if there at least MAX_OPS_PER_BATCH ops in queue and flush if there are.
// If force is true, flush regardless of size.
private void applyBatchIfNeeded(ArrayList<ContentProviderOperation> ops, int maxOpsPerBatch,
boolean force)
throws RemoteException, OperationApplicationException {
if (force || ops.size() >= maxOpsPerBatch) {
// STOPSHIP Remove calculating size of data before ship
if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
final Parcel parcel = Parcel.obtain();
for (ContentProviderOperation op : ops) {
op.writeToParcel(parcel, 0);
}
Log.d(TAG, String.format("Committing %d ops total size=%d",
ops.size(), parcel.dataSize()));
parcel.recycle();
}
mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
ops.clear();
}
}
}