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