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