Home | History | Annotate | Download | only in adapter
      1 package com.android.exchange.adapter;
      2 
      3 import android.content.ContentProviderOperation;
      4 import android.content.ContentResolver;
      5 import android.content.ContentUris;
      6 import android.content.ContentValues;
      7 import android.content.Context;
      8 import android.content.OperationApplicationException;
      9 import android.database.Cursor;
     10 import android.os.Parcel;
     11 import android.os.RemoteException;
     12 import android.os.TransactionTooLargeException;
     13 import android.provider.CalendarContract;
     14 import android.text.Html;
     15 import android.text.SpannedString;
     16 import android.text.TextUtils;
     17 import android.util.Base64;
     18 import android.util.Log;
     19 import android.webkit.MimeTypeMap;
     20 
     21 import com.android.emailcommon.internet.MimeMessage;
     22 import com.android.emailcommon.internet.MimeUtility;
     23 import com.android.emailcommon.mail.Address;
     24 import com.android.emailcommon.mail.MeetingInfo;
     25 import com.android.emailcommon.mail.MessagingException;
     26 import com.android.emailcommon.mail.PackedString;
     27 import com.android.emailcommon.mail.Part;
     28 import com.android.emailcommon.provider.Account;
     29 import com.android.emailcommon.provider.EmailContent;
     30 import com.android.emailcommon.provider.Mailbox;
     31 import com.android.emailcommon.provider.Policy;
     32 import com.android.emailcommon.provider.ProviderUnavailableException;
     33 import com.android.emailcommon.utility.AttachmentUtilities;
     34 import com.android.emailcommon.utility.ConversionUtilities;
     35 import com.android.emailcommon.utility.TextUtilities;
     36 import com.android.emailcommon.utility.Utility;
     37 import com.android.exchange.CommandStatusException;
     38 import com.android.exchange.Eas;
     39 import com.android.exchange.utility.CalendarUtilities;
     40 import com.android.mail.utils.LogUtils;
     41 import com.google.common.annotations.VisibleForTesting;
     42 
     43 import java.io.ByteArrayInputStream;
     44 import java.io.IOException;
     45 import java.io.InputStream;
     46 import java.util.ArrayList;
     47 import java.util.HashMap;
     48 import java.util.Map;
     49 
     50 /**
     51  * Parser for Sync on an email collection.
     52  */
     53 public class EmailSyncParser extends AbstractSyncParser {
     54     private static final String TAG = Eas.LOG_TAG;
     55 
     56     private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = EmailContent.SyncColumns.SERVER_ID
     57             + "=? and " + EmailContent.MessageColumns.MAILBOX_KEY + "=?";
     58 
     59     private final String mMailboxIdAsString;
     60 
     61     private final ArrayList<EmailContent.Message>
     62             newEmails = new ArrayList<EmailContent.Message>();
     63     private final ArrayList<EmailContent.Message> fetchedEmails =
     64             new ArrayList<EmailContent.Message>();
     65     private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
     66     private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
     67 
     68     private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
     69     private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
     70     private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
     71             new String[] { EmailContent.Message.RECORD_ID, EmailContent.MessageColumns.SUBJECT };
     72 
     73     @VisibleForTesting
     74     static final int LAST_VERB_REPLY = 1;
     75     @VisibleForTesting
     76     static final int LAST_VERB_REPLY_ALL = 2;
     77     @VisibleForTesting
     78     static final int LAST_VERB_FORWARD = 3;
     79 
     80     private final Policy mPolicy;
     81 
     82     // Max times to retry when we get a TransactionTooLargeException exception
     83     private static final int MAX_RETRIES = 10;
     84 
     85     // Max number of ops per batch. It could end up more than this but once we detect we are at or
     86     // above this number, we flush.
     87     private static final int MAX_OPS_PER_BATCH = 50;
     88 
     89     private boolean mFetchNeeded = false;
     90 
     91     private final Map<String, Integer> mMessageUpdateStatus = new HashMap();
     92 
     93     public EmailSyncParser(final Context context, final ContentResolver resolver,
     94             final InputStream in, final Mailbox mailbox, final Account account)
     95             throws IOException {
     96         super(context, resolver, in, mailbox, account);
     97         mMailboxIdAsString = Long.toString(mMailbox.mId);
     98         if (mAccount.mPolicyKey != 0) {
     99             mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
    100         } else {
    101             mPolicy = null;
    102         }
    103     }
    104 
    105     public boolean fetchNeeded() {
    106         return mFetchNeeded;
    107     }
    108 
    109     public Map<String, Integer> getMessageStatuses() {
    110         return mMessageUpdateStatus;
    111     }
    112 
    113     public void addData (EmailContent.Message msg, int endingTag) throws IOException {
    114         ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>();
    115         boolean truncated = false;
    116 
    117         while (nextTag(endingTag) != END) {
    118             switch (tag) {
    119                 case Tags.EMAIL_ATTACHMENTS:
    120                 case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
    121                     attachmentsParser(atts, msg);
    122                     break;
    123                 case Tags.EMAIL_TO:
    124                     msg.mTo = Address.pack(Address.parse(getValue()));
    125                     break;
    126                 case Tags.EMAIL_FROM:
    127                     Address[] froms = Address.parse(getValue());
    128                     if (froms != null && froms.length > 0) {
    129                         msg.mDisplayName = froms[0].toFriendly();
    130                     }
    131                     msg.mFrom = Address.pack(froms);
    132                     break;
    133                 case Tags.EMAIL_CC:
    134                     msg.mCc = Address.pack(Address.parse(getValue()));
    135                     break;
    136                 case Tags.EMAIL_REPLY_TO:
    137                     msg.mReplyTo = Address.pack(Address.parse(getValue()));
    138                     break;
    139                 case Tags.EMAIL_DATE_RECEIVED:
    140                     msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
    141                     break;
    142                 case Tags.EMAIL_SUBJECT:
    143                     msg.mSubject = getValue();
    144                     break;
    145                 case Tags.EMAIL_READ:
    146                     msg.mFlagRead = getValueInt() == 1;
    147                     break;
    148                 case Tags.BASE_BODY:
    149                     bodyParser(msg);
    150                     break;
    151                 case Tags.EMAIL_FLAG:
    152                     msg.mFlagFavorite = flagParser();
    153                     break;
    154                 case Tags.EMAIL_MIME_TRUNCATED:
    155                     truncated = getValueInt() == 1;
    156                     break;
    157                 case Tags.EMAIL_MIME_DATA:
    158                     // We get MIME data for EAS 2.5.  First we parse it, then we take the
    159                     // html and/or plain text data and store it in the message
    160                     if (truncated) {
    161                         // If the MIME data is truncated, don't bother parsing it, because
    162                         // it will take time and throw an exception anyway when EOF is reached
    163                         // In this case, we will load the body separately by tagging the message
    164                         // "partially loaded".
    165                         // Get the data (and ignore it)
    166                         getValue();
    167                         userLog("Partially loaded: ", msg.mServerId);
    168                         msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
    169                         mFetchNeeded = true;
    170                     } else {
    171                         mimeBodyParser(msg, getValue());
    172                     }
    173                     break;
    174                 case Tags.EMAIL_BODY:
    175                     String text = getValue();
    176                     msg.mText = text;
    177                     break;
    178                 case Tags.EMAIL_MESSAGE_CLASS:
    179                     String messageClass = getValue();
    180                     if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
    181                         msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_INVITE;
    182                     } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
    183                         msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_CANCEL;
    184                     }
    185                     break;
    186                 case Tags.EMAIL_MEETING_REQUEST:
    187                     meetingRequestParser(msg);
    188                     break;
    189                 case Tags.EMAIL_THREAD_TOPIC:
    190                     msg.mThreadTopic = getValue();
    191                     break;
    192                 case Tags.RIGHTS_LICENSE:
    193                     skipParser(tag);
    194                     break;
    195                 case Tags.EMAIL2_CONVERSATION_ID:
    196                     msg.mServerConversationId =
    197                             Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
    198                     break;
    199                 case Tags.EMAIL2_CONVERSATION_INDEX:
    200                     // Ignore this byte array since we're not constructing a tree.
    201                     getValueBytes();
    202                     break;
    203                 case Tags.EMAIL2_LAST_VERB_EXECUTED:
    204                     int val = getValueInt();
    205                     if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
    206                         // We aren't required to distinguish between reply and reply all here
    207                         msg.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
    208                     } else if (val == LAST_VERB_FORWARD) {
    209                         msg.mFlags |= EmailContent.Message.FLAG_FORWARDED;
    210                     }
    211                     break;
    212                 default:
    213                     skipTag();
    214             }
    215         }
    216 
    217         if (atts.size() > 0) {
    218             msg.mAttachments = atts;
    219         }
    220 
    221         if ((msg.mFlags & EmailContent.Message.FLAG_INCOMING_MEETING_MASK) != 0) {
    222             String text = TextUtilities.makeSnippetFromHtmlText(
    223                     msg.mText != null ? msg.mText : msg.mHtml);
    224             if (TextUtils.isEmpty(text)) {
    225                 // Create text for this invitation
    226                 String meetingInfo = msg.mMeetingInfo;
    227                 if (!TextUtils.isEmpty(meetingInfo)) {
    228                     PackedString ps = new PackedString(meetingInfo);
    229                     ContentValues values = new ContentValues();
    230                     putFromMeeting(ps, MeetingInfo.MEETING_LOCATION, values,
    231                             CalendarContract.Events.EVENT_LOCATION);
    232                     String dtstart = ps.get(MeetingInfo.MEETING_DTSTART);
    233                     if (!TextUtils.isEmpty(dtstart)) {
    234                         long startTime = Utility.parseEmailDateTimeToMillis(dtstart);
    235                         values.put(CalendarContract.Events.DTSTART, startTime);
    236                     }
    237                     putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values,
    238                             CalendarContract.Events.ALL_DAY);
    239                     msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(
    240                             mContext, values, null);
    241                     msg.mHtml = Html.toHtml(new SpannedString(msg.mText));
    242                 }
    243             }
    244         }
    245     }
    246 
    247     private static void putFromMeeting(PackedString ps, String field, ContentValues values,
    248             String column) {
    249         String val = ps.get(field);
    250         if (!TextUtils.isEmpty(val)) {
    251             values.put(column, val);
    252         }
    253     }
    254 
    255     /**
    256      * Set up the meetingInfo field in the message with various pieces of information gleaned
    257      * from MeetingRequest tags.  This information will be used later to generate an appropriate
    258      * reply email if the user chooses to respond
    259      * @param msg the Message being built
    260      * @throws IOException
    261      */
    262     private void meetingRequestParser(EmailContent.Message msg) throws IOException {
    263         PackedString.Builder packedString = new PackedString.Builder();
    264         while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
    265             switch (tag) {
    266                 case Tags.EMAIL_DTSTAMP:
    267                     packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
    268                     break;
    269                 case Tags.EMAIL_START_TIME:
    270                     packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
    271                     break;
    272                 case Tags.EMAIL_END_TIME:
    273                     packedString.put(MeetingInfo.MEETING_DTEND, getValue());
    274                     break;
    275                 case Tags.EMAIL_ORGANIZER:
    276                     packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
    277                     break;
    278                 case Tags.EMAIL_LOCATION:
    279                     packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
    280                     break;
    281                 case Tags.EMAIL_GLOBAL_OBJID:
    282                     packedString.put(MeetingInfo.MEETING_UID,
    283                             CalendarUtilities.getUidFromGlobalObjId(getValue()));
    284                     break;
    285                 case Tags.EMAIL_CATEGORIES:
    286                     skipParser(tag);
    287                     break;
    288                 case Tags.EMAIL_RECURRENCES:
    289                     recurrencesParser();
    290                     break;
    291                 case Tags.EMAIL_RESPONSE_REQUESTED:
    292                     packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
    293                     break;
    294                 case Tags.EMAIL_ALL_DAY_EVENT:
    295                     if (getValueInt() == 1) {
    296                         packedString.put(MeetingInfo.MEETING_ALL_DAY, "1");
    297                     }
    298                     break;
    299                 default:
    300                     skipTag();
    301             }
    302         }
    303         if (msg.mSubject != null) {
    304             packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
    305         }
    306         msg.mMeetingInfo = packedString.toString();
    307     }
    308 
    309     private void recurrencesParser() throws IOException {
    310         while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
    311             switch (tag) {
    312                 case Tags.EMAIL_RECURRENCE:
    313                     skipParser(tag);
    314                     break;
    315                 default:
    316                     skipTag();
    317             }
    318         }
    319     }
    320 
    321     /**
    322      * Parse a message from the server stream.
    323      * @return the parsed Message
    324      * @throws IOException
    325      */
    326     private EmailContent.Message addParser() throws IOException, CommandStatusException {
    327         EmailContent.Message msg = new EmailContent.Message();
    328         msg.mAccountKey = mAccount.mId;
    329         msg.mMailboxKey = mMailbox.mId;
    330         msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
    331         // Default to 1 (success) in case we don't get this tag
    332         int status = 1;
    333 
    334         while (nextTag(Tags.SYNC_ADD) != END) {
    335             switch (tag) {
    336                 case Tags.SYNC_SERVER_ID:
    337                     msg.mServerId = getValue();
    338                     break;
    339                 case Tags.SYNC_STATUS:
    340                     status = getValueInt();
    341                     break;
    342                 case Tags.SYNC_APPLICATION_DATA:
    343                     addData(msg, tag);
    344                     break;
    345                 default:
    346                     skipTag();
    347             }
    348         }
    349         // For sync, status 1 = success
    350         if (status != 1) {
    351             throw new CommandStatusException(status, msg.mServerId);
    352         }
    353         return msg;
    354     }
    355 
    356     // For now, we only care about the "active" state
    357     private Boolean flagParser() throws IOException {
    358         Boolean state = false;
    359         while (nextTag(Tags.EMAIL_FLAG) != END) {
    360             switch (tag) {
    361                 case Tags.EMAIL_FLAG_STATUS:
    362                     state = getValueInt() == 2;
    363                     break;
    364                 default:
    365                     skipTag();
    366             }
    367         }
    368         return state;
    369     }
    370 
    371     private void bodyParser(EmailContent.Message msg) throws IOException {
    372         String bodyType = Eas.BODY_PREFERENCE_TEXT;
    373         String body = "";
    374         while (nextTag(Tags.EMAIL_BODY) != END) {
    375             switch (tag) {
    376                 case Tags.BASE_TYPE:
    377                     bodyType = getValue();
    378                     break;
    379                 case Tags.BASE_DATA:
    380                     body = getValue();
    381                     break;
    382                 default:
    383                     skipTag();
    384             }
    385         }
    386         // We always ask for TEXT or HTML; there's no third option
    387         if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
    388             msg.mHtml = body;
    389         } else {
    390             msg.mText = body;
    391         }
    392     }
    393 
    394     /**
    395      * Parses untruncated MIME data, saving away the text parts
    396      * @param msg the message we're building
    397      * @param mimeData the MIME data we've received from the server
    398      * @throws IOException
    399      */
    400     private static void mimeBodyParser(EmailContent.Message msg, String mimeData)
    401             throws IOException {
    402         try {
    403             ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
    404             // The constructor parses the message
    405             MimeMessage mimeMessage = new MimeMessage(in);
    406             // Now process body parts & attachments
    407             ArrayList<Part> viewables = new ArrayList<Part>();
    408             // We'll ignore the attachments, as we'll get them directly from EAS
    409             ArrayList<Part> attachments = new ArrayList<Part>();
    410             MimeUtility.collectParts(mimeMessage, viewables, attachments);
    411             // parseBodyFields fills in the content fields of the Body
    412             ConversionUtilities.BodyFieldData data =
    413                     ConversionUtilities.parseBodyFields(viewables);
    414             // But we need them in the message itself for handling during commit()
    415             msg.setFlags(data.isQuotedReply, data.isQuotedForward);
    416             msg.mSnippet = data.snippet;
    417             msg.mHtml = data.htmlContent;
    418             msg.mText = data.textContent;
    419         } catch (MessagingException e) {
    420             // This would most likely indicate a broken stream
    421             throw new IOException(e);
    422         }
    423     }
    424 
    425     private void attachmentsParser(ArrayList<EmailContent.Attachment> atts,
    426             EmailContent.Message msg) throws IOException {
    427         while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
    428             switch (tag) {
    429                 case Tags.EMAIL_ATTACHMENT:
    430                 case Tags.BASE_ATTACHMENT:  // BASE_ATTACHMENT is used in EAS 12.0 and up
    431                     attachmentParser(atts, msg);
    432                     break;
    433                 default:
    434                     skipTag();
    435             }
    436         }
    437     }
    438 
    439     private void attachmentParser(ArrayList<EmailContent.Attachment> atts,
    440             EmailContent.Message msg) throws IOException {
    441         String fileName = null;
    442         String length = null;
    443         String location = null;
    444         boolean isInline = false;
    445         String contentId = null;
    446 
    447         while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
    448             switch (tag) {
    449                 // We handle both EAS 2.5 and 12.0+ attachments here
    450                 case Tags.EMAIL_DISPLAY_NAME:
    451                 case Tags.BASE_DISPLAY_NAME:
    452                     fileName = getValue();
    453                     break;
    454                 case Tags.EMAIL_ATT_NAME:
    455                 case Tags.BASE_FILE_REFERENCE:
    456                     location = getValue();
    457                     break;
    458                 case Tags.EMAIL_ATT_SIZE:
    459                 case Tags.BASE_ESTIMATED_DATA_SIZE:
    460                     length = getValue();
    461                     break;
    462                 case Tags.BASE_IS_INLINE:
    463                     isInline = getValueInt() == 1;
    464                     break;
    465                 case Tags.BASE_CONTENT_ID:
    466                     contentId = getValue();
    467                     break;
    468                 default:
    469                     skipTag();
    470             }
    471         }
    472 
    473         if ((fileName != null) && (length != null) && (location != null)) {
    474             EmailContent.Attachment att = new EmailContent.Attachment();
    475             att.mEncoding = "base64";
    476             att.mSize = Long.parseLong(length);
    477             att.mFileName = fileName;
    478             att.mLocation = location;
    479             att.mMimeType = getMimeTypeFromFileName(fileName);
    480             att.mAccountKey = mAccount.mId;
    481             // Save away the contentId, if we've got one (for inline images); note that the
    482             // EAS docs appear to be wrong about the tags used; inline images come with
    483             // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
    484             if (isInline && !TextUtils.isEmpty(contentId)) {
    485                 att.mContentId = contentId;
    486             }
    487             // Check if this attachment can't be downloaded due to an account policy
    488             if (mPolicy != null) {
    489                 if (mPolicy.mDontAllowAttachments ||
    490                         (mPolicy.mMaxAttachmentSize > 0 &&
    491                                 (att.mSize > mPolicy.mMaxAttachmentSize))) {
    492                     att.mFlags = EmailContent.Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
    493                 }
    494             }
    495             atts.add(att);
    496             msg.mFlagAttachment = true;
    497         }
    498     }
    499 
    500     /**
    501      * Returns an appropriate mimetype for the given file name's extension. If a mimetype
    502      * cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension,
    503      * if it exists or {@code application/octet-stream}].
    504      * At the moment, this is somewhat lame, since many file types aren't recognized
    505      * @param fileName the file name to ponder
    506      */
    507     // Note: The MimeTypeMap method currently uses a very limited set of mime types
    508     // A bug has been filed against this issue.
    509     public String getMimeTypeFromFileName(String fileName) {
    510         String mimeType;
    511         int lastDot = fileName.lastIndexOf('.');
    512         String extension = null;
    513         if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
    514             extension = fileName.substring(lastDot + 1).toLowerCase();
    515         }
    516         if (extension == null) {
    517             // A reasonable default for now.
    518             mimeType = "application/octet-stream";
    519         } else {
    520             mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    521             if (mimeType == null) {
    522                 mimeType = "application/" + extension;
    523             }
    524         }
    525         return mimeType;
    526     }
    527 
    528     private Cursor getServerIdCursor(String serverId, String[] projection) {
    529         Cursor c = mContentResolver.query(EmailContent.Message.CONTENT_URI, projection,
    530                 WHERE_SERVER_ID_AND_MAILBOX_KEY, new String[] {serverId, mMailboxIdAsString},
    531                 null);
    532         if (c == null) throw new ProviderUnavailableException();
    533         if (c.getCount() > 1) {
    534             userLog("Multiple messages with the same serverId/mailbox: " + serverId);
    535         }
    536         return c;
    537     }
    538 
    539     @VisibleForTesting
    540     void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
    541         while (nextTag(entryTag) != END) {
    542             switch (tag) {
    543                 case Tags.SYNC_SERVER_ID:
    544                     String serverId = getValue();
    545                     // Find the message in this mailbox with the given serverId
    546                     Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
    547                     try {
    548                         if (c.moveToFirst()) {
    549                             deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
    550                             if (Eas.USER_LOG) {
    551                                 userLog("Deleting ", serverId + ", "
    552                                         + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
    553                             }
    554                         }
    555                     } finally {
    556                         c.close();
    557                     }
    558                     break;
    559                 default:
    560                     skipTag();
    561             }
    562         }
    563     }
    564 
    565     @VisibleForTesting
    566     class ServerChange {
    567         final long id;
    568         final Boolean read;
    569         final Boolean flag;
    570         final Integer flags;
    571 
    572         ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) {
    573             id = _id;
    574             read = _read;
    575             flag = _flag;
    576             flags = _flags;
    577         }
    578     }
    579 
    580     @VisibleForTesting
    581     void changeParser(ArrayList<ServerChange> changes) throws IOException {
    582         String serverId = null;
    583         Boolean oldRead = false;
    584         Boolean oldFlag = false;
    585         int flags = 0;
    586         long id = 0;
    587         while (nextTag(Tags.SYNC_CHANGE) != END) {
    588             switch (tag) {
    589                 case Tags.SYNC_SERVER_ID:
    590                     serverId = getValue();
    591                     Cursor c = getServerIdCursor(serverId, EmailContent.Message.LIST_PROJECTION);
    592                     try {
    593                         if (c.moveToFirst()) {
    594                             userLog("Changing ", serverId);
    595                             oldRead = c.getInt(EmailContent.Message.LIST_READ_COLUMN)
    596                                     == EmailContent.Message.READ;
    597                             oldFlag = c.getInt(EmailContent.Message.LIST_FAVORITE_COLUMN) == 1;
    598                             flags = c.getInt(EmailContent.Message.LIST_FLAGS_COLUMN);
    599                             id = c.getLong(EmailContent.Message.LIST_ID_COLUMN);
    600                         }
    601                     } finally {
    602                         c.close();
    603                     }
    604                     break;
    605                 case Tags.SYNC_APPLICATION_DATA:
    606                     changeApplicationDataParser(changes, oldRead, oldFlag, flags, id);
    607                     break;
    608                 default:
    609                     skipTag();
    610             }
    611         }
    612     }
    613 
    614     private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
    615             Boolean oldFlag, int oldFlags, long id) throws IOException {
    616         Boolean read = null;
    617         Boolean flag = null;
    618         Integer flags = null;
    619         while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
    620             switch (tag) {
    621                 case Tags.EMAIL_READ:
    622                     read = getValueInt() == 1;
    623                     break;
    624                 case Tags.EMAIL_FLAG:
    625                     flag = flagParser();
    626                     break;
    627                 case Tags.EMAIL2_LAST_VERB_EXECUTED:
    628                     int val = getValueInt();
    629                     // Clear out the old replied/forward flags and add in the new flag
    630                     flags = oldFlags & ~(EmailContent.Message.FLAG_REPLIED_TO
    631                             | EmailContent.Message.FLAG_FORWARDED);
    632                     if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
    633                         // We aren't required to distinguish between reply and reply all here
    634                         flags |= EmailContent.Message.FLAG_REPLIED_TO;
    635                     } else if (val == LAST_VERB_FORWARD) {
    636                         flags |= EmailContent.Message.FLAG_FORWARDED;
    637                     }
    638                     break;
    639                 default:
    640                     skipTag();
    641             }
    642         }
    643         // See if there are flag changes re: read, flag (favorite) or replied/forwarded
    644         if (((read != null) && !oldRead.equals(read)) ||
    645                 ((flag != null) && !oldFlag.equals(flag)) || (flags != null)) {
    646             changes.add(new ServerChange(id, read, flag, flags));
    647         }
    648     }
    649 
    650     /* (non-Javadoc)
    651      * @see com.android.exchange.adapter.EasContentParser#commandsParser()
    652      */
    653     @Override
    654     public void commandsParser() throws IOException, CommandStatusException {
    655         while (nextTag(Tags.SYNC_COMMANDS) != END) {
    656             if (tag == Tags.SYNC_ADD) {
    657                 newEmails.add(addParser());
    658             } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
    659                 deleteParser(deletedEmails, tag);
    660             } else if (tag == Tags.SYNC_CHANGE) {
    661                 changeParser(changedEmails);
    662             } else
    663                 skipTag();
    664         }
    665     }
    666 
    667     // EAS values for status element of sync responses.
    668     // TODO: Not all are used yet, but I wanted to transcribe all possible values.
    669     public static final int EAS_SYNC_STATUS_SUCCESS = 1;
    670     public static final int EAS_SYNC_STATUS_BAD_SYNC_KEY = 3;
    671     public static final int EAS_SYNC_STATUS_PROTOCOL_ERROR = 4;
    672     public static final int EAS_SYNC_STATUS_SERVER_ERROR = 5;
    673     public static final int EAS_SYNC_STATUS_BAD_CLIENT_DATA = 6;
    674     public static final int EAS_SYNC_STATUS_CONFLICT = 7;
    675     public static final int EAS_SYNC_STATUS_OBJECT_NOT_FOUND = 8;
    676     public static final int EAS_SYNC_STATUS_CANNOT_COMPLETE = 9;
    677     public static final int EAS_SYNC_STATUS_FOLDER_SYNC_NEEDED = 12;
    678     public static final int EAS_SYNC_STATUS_INCOMPLETE_REQUEST = 13;
    679     public static final int EAS_SYNC_STATUS_BAD_HEARTBEAT_VALUE = 14;
    680     public static final int EAS_SYNC_STATUS_TOO_MANY_COLLECTIONS = 15;
    681     public static final int EAS_SYNC_STATUS_RETRY = 16;
    682 
    683     public static boolean shouldRetry(final int status) {
    684         return status == EAS_SYNC_STATUS_SERVER_ERROR || status == EAS_SYNC_STATUS_RETRY;
    685     }
    686 
    687     /**
    688      * Parse the status for a single message update.
    689      * @param endTag the tag we end with
    690      * @throws IOException
    691      */
    692     public void messageUpdateParser(int endTag) throws IOException {
    693         // We get serverId and status in the responses
    694         String serverId = null;
    695         int status = -1;
    696         while (nextTag(endTag) != END) {
    697             if (tag == Tags.SYNC_STATUS) {
    698                 status = getValueInt();
    699             } else if (tag == Tags.SYNC_SERVER_ID) {
    700                 serverId = getValue();
    701             } else {
    702                 skipTag();
    703             }
    704         }
    705         if (serverId != null && status != -1) {
    706             mMessageUpdateStatus.put(serverId, status);
    707         }
    708     }
    709 
    710     @Override
    711     public void responsesParser() throws IOException {
    712         while (nextTag(Tags.SYNC_RESPONSES) != END) {
    713             if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
    714                 messageUpdateParser(tag);
    715             } else if (tag == Tags.SYNC_FETCH) {
    716                 try {
    717                     fetchedEmails.add(addParser());
    718                 } catch (CommandStatusException sse) {
    719                     if (sse.mStatus == 8) {
    720                         // 8 = object not found; delete the message from EmailProvider
    721                         // No other status should be seen in a fetch response, except, perhaps,
    722                         // for some temporary server failure
    723                         mContentResolver.delete(EmailContent.Message.CONTENT_URI,
    724                                 WHERE_SERVER_ID_AND_MAILBOX_KEY,
    725                                 new String[] {sse.mItemId, mMailboxIdAsString});
    726                     }
    727                 }
    728             }
    729         }
    730     }
    731 
    732     @Override
    733     protected void wipe() {
    734         LogUtils.i(TAG, "Wiping mailbox " + mMailbox);
    735         Mailbox.resyncMailbox(mContentResolver, new android.accounts.Account(mAccount.mEmailAddress,
    736                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mMailbox.mId);
    737     }
    738 
    739     @Override
    740     public boolean parse() throws IOException, CommandStatusException {
    741         final boolean result = super.parse();
    742         return result || fetchNeeded();
    743     }
    744 
    745     /**
    746      * Commit all changes. This results in a Binder IPC call which has constraint on the size of
    747      * the data, the docs say it currently 1MB. We set a limit to the size of the message we fetch
    748      * with {@link Eas#EAS12_TRUNCATION_SIZE} & {@link Eas#EAS12_TRUNCATION_SIZE} which are at 200k
    749      * or bellow. As long as these limits are bellow 500k, we should be able to apply a single
    750      * message (the transaction size is about double the message size because Java strings are 16
    751      * bit.
    752      * <b/>
    753      * We first try to apply the changes in normal chunk size {@link #MAX_OPS_PER_BATCH}. If we get
    754      * a {@link TransactionTooLargeException} we try again with but this time, we apply each change
    755      * immediately.
    756      */
    757     @Override
    758     public void commit() throws RemoteException, OperationApplicationException {
    759         try {
    760             commitImpl(MAX_OPS_PER_BATCH);
    761         } catch (TransactionTooLargeException e) {
    762             // Try again but apply batch after every message. The max message size defined in
    763             // Eas.EAS12_TRUNCATION_SIZE or Eas.EAS2_5_TRUNCATION_SIZE is small enough to fit
    764             // in a single Binder call.
    765             LogUtils.w(TAG, "Transaction too large, retrying in single mode", e);
    766             commitImpl(1);
    767         }
    768     }
    769 
    770     public void commitImpl(int maxOpsPerBatch)
    771             throws RemoteException, OperationApplicationException {
    772         // Use a batch operation to handle the changes
    773         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
    774 
    775         // Maximum size of message text per fetch
    776         int numFetched = fetchedEmails.size();
    777         LogUtils.d(TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d "
    778                 + "numDeleted=%d numChanged=%d",
    779                 maxOpsPerBatch,
    780                 numFetched,
    781                 newEmails.size(),
    782                 deletedEmails.size(),
    783                 changedEmails.size());
    784         for (EmailContent.Message msg: fetchedEmails) {
    785             // Find the original message's id (by serverId and mailbox)
    786             Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
    787             String id = null;
    788             try {
    789                 if (c.moveToFirst()) {
    790                     id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
    791                     while (c.moveToNext()) {
    792                         // This shouldn't happen, but clean up if it does
    793                         Long dupId =
    794                                 Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
    795                         userLog("Delete duplicate with id: " + dupId);
    796                         deletedEmails.add(dupId);
    797                     }
    798                 }
    799             } finally {
    800                 c.close();
    801             }
    802 
    803             // If we find one, we do two things atomically: 1) set the body text for the
    804             // message, and 2) mark the message loaded (i.e. completely loaded)
    805             if (id != null) {
    806                 LogUtils.i(TAG, "Fetched body successfully for %s", id);
    807                 final String[] bindArgument = new String[] {id};
    808                 ops.add(ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
    809                         .withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument)
    810                         .withValue(EmailContent.Body.TEXT_CONTENT, msg.mText)
    811                         .build());
    812                 ops.add(ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI)
    813                         .withSelection(EmailContent.RECORD_ID + "=?", bindArgument)
    814                         .withValue(EmailContent.Message.FLAG_LOADED,
    815                                 EmailContent.Message.FLAG_LOADED_COMPLETE)
    816                         .build());
    817             }
    818             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    819         }
    820 
    821         for (EmailContent.Message msg: newEmails) {
    822             msg.addSaveOps(ops);
    823             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    824         }
    825 
    826         for (Long id : deletedEmails) {
    827             ops.add(ContentProviderOperation.newDelete(
    828                     ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, id)).build());
    829             AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
    830             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    831         }
    832 
    833         if (!changedEmails.isEmpty()) {
    834             // Server wins in a conflict...
    835             for (ServerChange change : changedEmails) {
    836                 ContentValues cv = new ContentValues();
    837                 if (change.read != null) {
    838                     cv.put(EmailContent.MessageColumns.FLAG_READ, change.read);
    839                 }
    840                 if (change.flag != null) {
    841                     cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag);
    842                 }
    843                 if (change.flags != null) {
    844                     cv.put(EmailContent.MessageColumns.FLAGS, change.flags);
    845                 }
    846                 ops.add(ContentProviderOperation.newUpdate(
    847                         ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id))
    848                         .withValues(cv)
    849                         .build());
    850             }
    851             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    852         }
    853 
    854         // We only want to update the sync key here
    855         ContentValues mailboxValues = new ContentValues();
    856         mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
    857         ops.add(ContentProviderOperation.newUpdate(
    858                 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
    859                 .withValues(mailboxValues).build());
    860 
    861         applyBatchIfNeeded(ops, maxOpsPerBatch, true);
    862         userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
    863     }
    864 
    865     // Check if there at least MAX_OPS_PER_BATCH ops in queue and flush if there are.
    866     // If force is true, flush regardless of size.
    867     private void applyBatchIfNeeded(ArrayList<ContentProviderOperation> ops, int maxOpsPerBatch,
    868             boolean force)
    869             throws RemoteException, OperationApplicationException {
    870         if (force ||  ops.size() >= maxOpsPerBatch) {
    871             // STOPSHIP Remove calculating size of data before ship
    872             if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
    873                 final Parcel parcel = Parcel.obtain();
    874                 for (ContentProviderOperation op : ops) {
    875                     op.writeToParcel(parcel, 0);
    876                 }
    877                 Log.d(TAG, String.format("Committing %d ops total size=%d",
    878                         ops.size(), parcel.dataSize()));
    879                 parcel.recycle();
    880             }
    881             mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
    882             ops.clear();
    883         }
    884     }
    885 }