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