• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
EmailSyncParser(final Context context, final ContentResolver resolver, final InputStream in, final Mailbox mailbox, final Account account)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 
EmailSyncParser(final Parser parser, final Context context, final ContentResolver resolver, final Mailbox mailbox, final Account account)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 
EmailSyncParser(final Context context, final InputStream in, final Mailbox mailbox, final Account account)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 
fetchNeeded()125     public boolean fetchNeeded() {
126         return mFetchNeeded;
127     }
128 
getMessageStatuses()129     public Map<String, Integer> getMessageStatuses() {
130         return mMessageUpdateStatus;
131     }
132 
addData(EmailContent.Message msg, int endingTag)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 
putFromMeeting(PackedString ps, String field, ContentValues values, String column)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      */
meetingRequestParser(EmailContent.Message msg)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 
recurrencesParser()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      */
addParser(final int endingTag)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
flagParser()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 
bodyParser(EmailContent.Message msg)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      */
mimeBodyParser(EmailContent.Message msg, String mimeData)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 
attachmentsParser(final ArrayList<EmailContent.Attachment> atts, final EmailContent.Message msg, final int endingTag)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 
attachmentParser(final ArrayList<EmailContent.Attachment> atts, final EmailContent.Message msg, final int endingTag)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.
getMimeTypeFromFileName(String fileName)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 
getServerIdCursor(String serverId, String[] projection)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
deleteParser(ArrayList<Long> deletes, int entryTag)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 
ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags)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
changeParser(ArrayList<ServerChange> changes)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 
changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead, Boolean oldFlag, int oldFlags, long id)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
commandsParser()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 
shouldRetry(final int status)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      */
messageUpdateParser(int endTag)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
responsesParser()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
wipe()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
parse()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
commit()787     public void commit() throws RemoteException, OperationApplicationException {
788         try {
789             commitImpl(MAX_OPS_PER_BATCH);
790         } catch (TransactionTooLargeException e) {
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, "Transaction too large, retrying in single mode", e);
795             commitImpl(1);
796         }
797     }
798 
commitImpl(int maxOpsPerBatch)799     public void commitImpl(int maxOpsPerBatch)
800             throws RemoteException, OperationApplicationException {
801         // Use a batch operation to handle the changes
802         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
803 
804         // Maximum size of message text per fetch
805         int numFetched = fetchedEmails.size();
806         LogUtils.d(TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d "
807                 + "numDeleted=%d numChanged=%d",
808                 maxOpsPerBatch,
809                 numFetched,
810                 newEmails.size(),
811                 deletedEmails.size(),
812                 changedEmails.size());
813         for (EmailContent.Message msg: fetchedEmails) {
814             // Find the original message's id (by serverId and mailbox)
815             Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
816             String id = null;
817             try {
818                 if (c.moveToFirst()) {
819                     id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
820                     while (c.moveToNext()) {
821                         // This shouldn't happen, but clean up if it does
822                         Long dupId =
823                                 Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
824                         userLog("Delete duplicate with id: " + dupId);
825                         deletedEmails.add(dupId);
826                     }
827                 }
828             } finally {
829                 c.close();
830             }
831 
832             // If we find one, we do two things atomically: 1) set the body text for the
833             // message, and 2) mark the message loaded (i.e. completely loaded)
834             if (id != null) {
835                 LogUtils.i(TAG, "Fetched body successfully for %s", id);
836                 final String[] bindArgument = new String[] {id};
837                 ops.add(ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
838                         .withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument)
839                         .withValue(EmailContent.BodyColumns.TEXT_CONTENT, msg.mText)
840                         .build());
841                 ops.add(ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI)
842                         .withSelection(MessageColumns._ID + "=?", bindArgument)
843                         .withValue(MessageColumns.FLAG_LOADED,
844                                 EmailContent.Message.FLAG_LOADED_COMPLETE)
845                         .build());
846             }
847             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
848         }
849 
850         for (EmailContent.Message msg: newEmails) {
851             msg.addSaveOps(ops);
852             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
853         }
854 
855         for (Long id : deletedEmails) {
856             ops.add(ContentProviderOperation.newDelete(
857                     ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, id)).build());
858             AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
859             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
860         }
861 
862         if (!changedEmails.isEmpty()) {
863             // Server wins in a conflict...
864             for (ServerChange change : changedEmails) {
865                 ContentValues cv = new ContentValues();
866                 if (change.read != null) {
867                     cv.put(EmailContent.MessageColumns.FLAG_READ, change.read);
868                 }
869                 if (change.flag != null) {
870                     cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag);
871                 }
872                 if (change.flags != null) {
873                     cv.put(EmailContent.MessageColumns.FLAGS, change.flags);
874                 }
875                 ops.add(ContentProviderOperation.newUpdate(
876                         ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id))
877                         .withValues(cv)
878                         .build());
879             }
880             applyBatchIfNeeded(ops, maxOpsPerBatch, false);
881         }
882 
883         // We only want to update the sync key here
884         ContentValues mailboxValues = new ContentValues();
885         mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
886         ops.add(ContentProviderOperation.newUpdate(
887                 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
888                 .withValues(mailboxValues).build());
889 
890         applyBatchIfNeeded(ops, maxOpsPerBatch, true);
891         userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
892     }
893 
894     // Check if there at least MAX_OPS_PER_BATCH ops in queue and flush if there are.
895     // If force is true, flush regardless of size.
applyBatchIfNeeded(ArrayList<ContentProviderOperation> ops, int maxOpsPerBatch, boolean force)896     private void applyBatchIfNeeded(ArrayList<ContentProviderOperation> ops, int maxOpsPerBatch,
897             boolean force)
898             throws RemoteException, OperationApplicationException {
899         if (force ||  ops.size() >= maxOpsPerBatch) {
900             // STOPSHIP Remove calculating size of data before ship
901             if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
902                 final Parcel parcel = Parcel.obtain();
903                 for (ContentProviderOperation op : ops) {
904                     op.writeToParcel(parcel, 0);
905                 }
906                 Log.d(TAG, String.format("Committing %d ops total size=%d",
907                         ops.size(), parcel.dataSize()));
908                 parcel.recycle();
909             }
910             mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
911             ops.clear();
912         }
913     }
914 }
915