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