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