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