EmailSyncAdapter.java revision 0dfbd9efda459c7eab716a8eca5f908973bc585f
1/* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange.adapter; 19 20import android.content.ContentProviderOperation; 21import android.content.ContentResolver; 22import android.content.ContentUris; 23import android.content.ContentValues; 24import android.content.OperationApplicationException; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.RemoteException; 28import android.util.Log; 29import android.webkit.MimeTypeMap; 30 31import com.android.emailcommon.internet.MimeMessage; 32import com.android.emailcommon.internet.MimeUtility; 33import com.android.emailcommon.mail.Address; 34import com.android.emailcommon.mail.MeetingInfo; 35import com.android.emailcommon.mail.MessagingException; 36import com.android.emailcommon.mail.PackedString; 37import com.android.emailcommon.mail.Part; 38import com.android.emailcommon.provider.Account; 39import com.android.emailcommon.provider.EmailContent; 40import com.android.emailcommon.provider.EmailContent.AccountColumns; 41import com.android.emailcommon.provider.EmailContent.Attachment; 42import com.android.emailcommon.provider.EmailContent.Body; 43import com.android.emailcommon.provider.EmailContent.MailboxColumns; 44import com.android.emailcommon.provider.EmailContent.Message; 45import com.android.emailcommon.provider.EmailContent.MessageColumns; 46import com.android.emailcommon.provider.EmailContent.SyncColumns; 47import com.android.emailcommon.provider.Mailbox; 48import com.android.emailcommon.provider.Policy; 49import com.android.emailcommon.service.SyncWindow; 50import com.android.emailcommon.utility.AttachmentUtilities; 51import com.android.emailcommon.utility.ConversionUtilities; 52import com.android.emailcommon.utility.Utility; 53import com.android.exchange.CommandStatusException; 54import com.android.exchange.Eas; 55import com.android.exchange.EasResponse; 56import com.android.exchange.EasSyncService; 57import com.android.exchange.MessageMoveRequest; 58import com.android.exchange.R; 59import com.android.exchange.utility.CalendarUtilities; 60import com.google.common.annotations.VisibleForTesting; 61 62import org.apache.http.HttpStatus; 63import org.apache.http.entity.ByteArrayEntity; 64 65import java.io.ByteArrayInputStream; 66import java.io.IOException; 67import java.io.InputStream; 68import java.util.ArrayList; 69import java.util.Calendar; 70import java.util.GregorianCalendar; 71import java.util.TimeZone; 72 73/** 74 * Sync adapter for EAS email 75 * 76 */ 77public class EmailSyncAdapter extends AbstractSyncAdapter { 78 79 private static final int UPDATES_READ_COLUMN = 0; 80 private static final int UPDATES_MAILBOX_KEY_COLUMN = 1; 81 private static final int UPDATES_SERVER_ID_COLUMN = 2; 82 private static final int UPDATES_FLAG_COLUMN = 3; 83 private static final String[] UPDATES_PROJECTION = 84 {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID, 85 MessageColumns.FLAG_FAVORITE}; 86 87 private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0; 88 private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1; 89 private static final String[] MESSAGE_ID_SUBJECT_PROJECTION = 90 new String[] { Message.RECORD_ID, MessageColumns.SUBJECT }; 91 92 private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?"; 93 private static final String WHERE_MAILBOX_KEY_AND_MOVED = 94 MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" + 95 EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE + ")!=0"; 96 private static final String[] FETCH_REQUEST_PROJECTION = 97 new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID}; 98 private static final int FETCH_REQUEST_RECORD_ID = 0; 99 private static final int FETCH_REQUEST_SERVER_ID = 1; 100 101 private static final String EMAIL_WINDOW_SIZE = "5"; 102 103 @VisibleForTesting 104 static final int LAST_VERB_REPLY = 1; 105 @VisibleForTesting 106 static final int LAST_VERB_REPLY_ALL = 2; 107 @VisibleForTesting 108 static final int LAST_VERB_FORWARD = 3; 109 110 private final String[] mBindArguments = new String[2]; 111 private final String[] mBindArgument = new String[1]; 112 113 @VisibleForTesting 114 ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); 115 @VisibleForTesting 116 ArrayList<Long> mUpdatedIdList = new ArrayList<Long>(); 117 private final ArrayList<FetchRequest> mFetchRequestList = new ArrayList<FetchRequest>(); 118 private boolean mFetchNeeded = false; 119 120 // Holds the parser's value for isLooping() 121 private boolean mIsLooping = false; 122 123 // The policy (if any) for this adapter's Account 124 private final Policy mPolicy; 125 126 public EmailSyncAdapter(EasSyncService service) { 127 super(service); 128 // If we've got an account with a policy, cache it now 129 if (mAccount.mPolicyKey != 0) { 130 mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey); 131 } else { 132 mPolicy = null; 133 } 134 } 135 136 @Override 137 public void wipe() { 138 mContentResolver.delete(Message.CONTENT_URI, 139 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 140 mContentResolver.delete(Message.DELETED_CONTENT_URI, 141 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 142 mContentResolver.delete(Message.UPDATED_CONTENT_URI, 143 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 144 mService.clearRequests(); 145 mFetchRequestList.clear(); 146 // Delete attachments... 147 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId); 148 } 149 150 private String getEmailFilter() { 151 int syncLookback = mMailbox.mSyncLookback; 152 if (syncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN 153 || mMailbox.mType == Mailbox.TYPE_INBOX) { 154 syncLookback = mAccount.mSyncLookback; 155 } 156 switch (syncLookback) { 157 case SyncWindow.SYNC_WINDOW_AUTO: 158 return Eas.FILTER_AUTO; 159 case SyncWindow.SYNC_WINDOW_1_DAY: 160 return Eas.FILTER_1_DAY; 161 case SyncWindow.SYNC_WINDOW_3_DAYS: 162 return Eas.FILTER_3_DAYS; 163 case SyncWindow.SYNC_WINDOW_1_WEEK: 164 return Eas.FILTER_1_WEEK; 165 case SyncWindow.SYNC_WINDOW_2_WEEKS: 166 return Eas.FILTER_2_WEEKS; 167 case SyncWindow.SYNC_WINDOW_1_MONTH: 168 return Eas.FILTER_1_MONTH; 169 case SyncWindow.SYNC_WINDOW_ALL: 170 return Eas.FILTER_ALL; 171 default: 172 return Eas.FILTER_1_WEEK; 173 } 174 } 175 176 /** 177 * Holder for fetch request information (record id and server id) 178 */ 179 private static class FetchRequest { 180 @SuppressWarnings("unused") 181 final long messageId; 182 final String serverId; 183 184 FetchRequest(long _messageId, String _serverId) { 185 messageId = _messageId; 186 serverId = _serverId; 187 } 188 } 189 190 @Override 191 public void sendSyncOptions(Double protocolVersion, Serializer s) 192 throws IOException { 193 mFetchRequestList.clear(); 194 // Find partially loaded messages; this should typically be a rare occurrence 195 Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI, 196 FETCH_REQUEST_PROJECTION, 197 MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " + 198 MessageColumns.MAILBOX_KEY + "=?", 199 new String[] {Long.toString(mMailbox.mId)}, null); 200 try { 201 // Put all of these messages into a list; we'll need both id and server id 202 while (c.moveToNext()) { 203 mFetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID), 204 c.getString(FETCH_REQUEST_SERVER_ID))); 205 } 206 } finally { 207 c.close(); 208 } 209 210 // The "empty" case is typical; we send a request for changes, and also specify a sync 211 // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and 212 // truncation 213 // If there are fetch requests, we only want the fetches (i.e. no changes from the server) 214 // so we turn MIME support off. Note that we are always using EAS 2.5 if there are fetch 215 // requests 216 if (mFetchRequestList.isEmpty()) { 217 s.tag(Tags.SYNC_DELETES_AS_MOVES); 218 s.tag(Tags.SYNC_GET_CHANGES); 219 s.data(Tags.SYNC_WINDOW_SIZE, EMAIL_WINDOW_SIZE); 220 s.start(Tags.SYNC_OPTIONS); 221 // Set the lookback appropriately (EAS calls this a "filter") 222 String filter = getEmailFilter(); 223 // We shouldn't get FILTER_AUTO here, but if we do, make it something legal... 224 if (filter.equals(Eas.FILTER_AUTO)) { 225 filter = Eas.FILTER_3_DAYS; 226 } 227 s.data(Tags.SYNC_FILTER_TYPE, filter); 228 // Set the truncation amount for all classes 229 if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 230 s.start(Tags.BASE_BODY_PREFERENCE); 231 // HTML for email 232 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML); 233 s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE); 234 s.end(); 235 } else { 236 // Use MIME data for EAS 2.5 237 s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_MIME); 238 s.data(Tags.SYNC_MIME_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 239 } 240 s.end(); 241 } else { 242 s.start(Tags.SYNC_OPTIONS); 243 // Ask for plain text, rather than MIME data. This guarantees that we'll get a usable 244 // text body 245 s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT); 246 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 247 s.end(); 248 } 249 } 250 251 @Override 252 public boolean parse(InputStream is) throws IOException, CommandStatusException { 253 EasEmailSyncParser p = new EasEmailSyncParser(is, this); 254 mFetchNeeded = false; 255 boolean res = p.parse(); 256 // Hold on to the parser's value for isLooping() to pass back to the service 257 mIsLooping = p.isLooping(); 258 // If we've need a body fetch, or we've just finished one, return true in order to continue 259 if (mFetchNeeded || !mFetchRequestList.isEmpty()) { 260 return true; 261 } 262 263 // Don't check for "auto" on the initial sync 264 if (!("0".equals(mMailbox.mSyncKey))) { 265 // We've completed the first successful sync 266 if (getEmailFilter().equals(Eas.FILTER_AUTO)) { 267 getAutomaticLookback(); 268 } 269 } 270 271 return res; 272 } 273 274 private void getAutomaticLookback() throws IOException { 275 // If we're using an auto lookback, check how many items in the past week 276 // TODO Make the literal ints below constants once we twiddle them a bit 277 int items = getEstimate(Eas.FILTER_1_WEEK); 278 int lookback; 279 if (items > 1050) { 280 // Over 150/day, just use one day (smallest) 281 lookback = SyncWindow.SYNC_WINDOW_1_DAY; 282 } else if (items > 350 || (items == -1)) { 283 // 50-150/day, use 3 days (150 to 450 messages synced) 284 lookback = SyncWindow.SYNC_WINDOW_3_DAYS; 285 } else if (items > 150) { 286 // 20-50/day, use 1 week (140 to 350 messages synced) 287 lookback = SyncWindow.SYNC_WINDOW_1_WEEK; 288 } else if (items > 75) { 289 // 10-25/day, use 1 week (140 to 350 messages synced) 290 lookback = SyncWindow.SYNC_WINDOW_2_WEEKS; 291 } else if (items < 5) { 292 // If there are only a couple, see if it makes sense to get everything 293 items = getEstimate(Eas.FILTER_ALL); 294 if (items >= 0 && items < 100) { 295 lookback = SyncWindow.SYNC_WINDOW_ALL; 296 } else { 297 lookback = SyncWindow.SYNC_WINDOW_1_MONTH; 298 } 299 } else { 300 lookback = SyncWindow.SYNC_WINDOW_1_MONTH; 301 } 302 303 // Store the new lookback and persist it 304 // TODO Code similar to this is used elsewhere (e.g. MailboxSettings); try to clean this up 305 ContentValues cv = new ContentValues(); 306 Uri uri; 307 if (mMailbox.mType == Mailbox.TYPE_INBOX) { 308 mAccount.mSyncLookback = lookback; 309 cv.put(AccountColumns.SYNC_LOOKBACK, lookback); 310 uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId); 311 } else { 312 mMailbox.mSyncLookback = lookback; 313 cv.put(MailboxColumns.SYNC_LOOKBACK, lookback); 314 uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId); 315 } 316 mContentResolver.update(uri, cv, null, null); 317 318 // STOPSHIP Temporary UI - Let the user know 319 CharSequence[] windowEntries = mContext.getResources().getTextArray( 320 R.array.account_settings_mail_window_entries); 321 Utility.showToast(mContext, "Auto lookback: " + windowEntries[lookback]); 322 } 323 324 private static class GetItemEstimateParser extends Parser { 325 private static final String TAG = "GetItemEstimateParser"; 326 private int mEstimate = -1; 327 328 public GetItemEstimateParser(InputStream in) throws IOException { 329 super(in); 330 } 331 332 @Override 333 public boolean parse() throws IOException { 334 // Loop here through the remaining xml 335 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 336 if (tag == Tags.GIE_GET_ITEM_ESTIMATE) { 337 parseGetItemEstimate(); 338 } else { 339 skipTag(); 340 } 341 } 342 return true; 343 } 344 345 public void parseGetItemEstimate() throws IOException { 346 while (nextTag(Tags.GIE_GET_ITEM_ESTIMATE) != END) { 347 if (tag == Tags.GIE_RESPONSE) { 348 parseResponse(); 349 } else { 350 skipTag(); 351 } 352 } 353 } 354 355 public void parseResponse() throws IOException { 356 while (nextTag(Tags.GIE_RESPONSE) != END) { 357 if (tag == Tags.GIE_STATUS) { 358 Log.d(TAG, "GIE status: " + getValue()); 359 } else if (tag == Tags.GIE_COLLECTION) { 360 parseCollection(); 361 } else { 362 skipTag(); 363 } 364 } 365 } 366 367 public void parseCollection() throws IOException { 368 while (nextTag(Tags.GIE_COLLECTION) != END) { 369 if (tag == Tags.GIE_CLASS) { 370 Log.d(TAG, "GIE class: " + getValue()); 371 } else if (tag == Tags.GIE_COLLECTION_ID) { 372 Log.d(TAG, "GIE collectionId: " + getValue()); 373 } else if (tag == Tags.GIE_ESTIMATE) { 374 mEstimate = getValueInt(); 375 Log.d(TAG, "GIE estimate: " + mEstimate); 376 } else { 377 skipTag(); 378 } 379 } 380 } 381 } 382 383 /** 384 * Return the estimated number of items to be synced in the current mailbox, based on the 385 * passed in filter argument 386 * @param filter an EAS "window" filter 387 * @return the estimated number of items to be synced, or -1 if unknown 388 * @throws IOException 389 */ 390 private int getEstimate(String filter) throws IOException { 391 Serializer s = new Serializer(); 392 boolean ex10 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE; 393 boolean ex03 = mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE; 394 boolean ex07 = !ex10 && !ex03; 395 396 String className = getCollectionName(); 397 String syncKey = getSyncKey(); 398 userLog("gie, sending ", className, " syncKey: ", syncKey); 399 400 s.start(Tags.GIE_GET_ITEM_ESTIMATE).start(Tags.GIE_COLLECTIONS); 401 s.start(Tags.GIE_COLLECTION); 402 if (ex07) { 403 // Exchange 2007 likes collection id first 404 s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId); 405 s.data(Tags.SYNC_FILTER_TYPE, filter); 406 s.data(Tags.SYNC_SYNC_KEY, syncKey); 407 } else if (ex03) { 408 // Exchange 2003 needs the "class" element 409 s.data(Tags.GIE_CLASS, className); 410 s.data(Tags.SYNC_SYNC_KEY, syncKey); 411 s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId); 412 s.data(Tags.SYNC_FILTER_TYPE, filter); 413 } else { 414 // Exchange 2010 requires the filter inside an OPTIONS container and sync key first 415 s.data(Tags.SYNC_SYNC_KEY, syncKey); 416 s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId); 417 s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, filter).end(); 418 } 419 s.end().end().end().done(); // GIE_COLLECTION, GIE_COLLECTIONS, GIE_GET_ITEM_ESTIMATE 420 421 EasResponse resp = mService.sendHttpClientPost("GetItemEstimate", 422 new ByteArrayEntity(s.toByteArray()), EasSyncService.COMMAND_TIMEOUT); 423 int code = resp.getStatus(); 424 if (code == HttpStatus.SC_OK) { 425 if (!resp.isEmpty()) { 426 InputStream is = resp.getInputStream(); 427 GetItemEstimateParser gieParser = new GetItemEstimateParser(is); 428 gieParser.parse(); 429 // Return the estimated number of items 430 return gieParser.mEstimate; 431 } 432 } 433 // If we can't get an estimate, indicate this... 434 return -1; 435 } 436 437 /** 438 * Return the value of isLooping() as returned from the parser 439 */ 440 @Override 441 public boolean isLooping() { 442 return mIsLooping; 443 } 444 445 @Override 446 public boolean isSyncable() { 447 return true; 448 } 449 450 public class EasEmailSyncParser extends AbstractSyncParser { 451 452 private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = 453 SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?"; 454 455 private final String mMailboxIdAsString; 456 457 private final ArrayList<Message> newEmails = new ArrayList<Message>(); 458 private final ArrayList<Message> fetchedEmails = new ArrayList<Message>(); 459 private final ArrayList<Long> deletedEmails = new ArrayList<Long>(); 460 private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>(); 461 462 public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException { 463 super(in, adapter); 464 mMailboxIdAsString = Long.toString(mMailbox.mId); 465 } 466 467 public EasEmailSyncParser(Parser parser, EmailSyncAdapter adapter) throws IOException { 468 super(parser, adapter); 469 mMailboxIdAsString = Long.toString(mMailbox.mId); 470 } 471 472 public void addData (Message msg, int endingTag) throws IOException { 473 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 474 boolean truncated = false; 475 476 while (nextTag(endingTag) != END) { 477 switch (tag) { 478 case Tags.EMAIL_ATTACHMENTS: 479 case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up 480 attachmentsParser(atts, msg); 481 break; 482 case Tags.EMAIL_TO: 483 msg.mTo = Address.pack(Address.parse(getValue())); 484 break; 485 case Tags.EMAIL_FROM: 486 Address[] froms = Address.parse(getValue()); 487 if (froms != null && froms.length > 0) { 488 msg.mDisplayName = froms[0].toFriendly(); 489 } 490 msg.mFrom = Address.pack(froms); 491 break; 492 case Tags.EMAIL_CC: 493 msg.mCc = Address.pack(Address.parse(getValue())); 494 break; 495 case Tags.EMAIL_REPLY_TO: 496 msg.mReplyTo = Address.pack(Address.parse(getValue())); 497 break; 498 case Tags.EMAIL_DATE_RECEIVED: 499 msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue()); 500 break; 501 case Tags.EMAIL_SUBJECT: 502 msg.mSubject = getValue(); 503 break; 504 case Tags.EMAIL_READ: 505 msg.mFlagRead = getValueInt() == 1; 506 break; 507 case Tags.BASE_BODY: 508 bodyParser(msg); 509 break; 510 case Tags.EMAIL_FLAG: 511 msg.mFlagFavorite = flagParser(); 512 break; 513 case Tags.EMAIL_MIME_TRUNCATED: 514 truncated = getValueInt() == 1; 515 break; 516 case Tags.EMAIL_MIME_DATA: 517 // We get MIME data for EAS 2.5. First we parse it, then we take the 518 // html and/or plain text data and store it in the message 519 if (truncated) { 520 // If the MIME data is truncated, don't bother parsing it, because 521 // it will take time and throw an exception anyway when EOF is reached 522 // In this case, we will load the body separately by tagging the message 523 // "partially loaded". 524 // Get the data (and ignore it) 525 getValue(); 526 userLog("Partially loaded: ", msg.mServerId); 527 msg.mFlagLoaded = Message.FLAG_LOADED_PARTIAL; 528 mFetchNeeded = true; 529 } else { 530 mimeBodyParser(msg, getValue()); 531 } 532 break; 533 case Tags.EMAIL_BODY: 534 String text = getValue(); 535 msg.mText = text; 536 break; 537 case Tags.EMAIL_MESSAGE_CLASS: 538 String messageClass = getValue(); 539 if (messageClass.equals("IPM.Schedule.Meeting.Request")) { 540 msg.mFlags |= Message.FLAG_INCOMING_MEETING_INVITE; 541 } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) { 542 msg.mFlags |= Message.FLAG_INCOMING_MEETING_CANCEL; 543 } 544 break; 545 case Tags.EMAIL_MEETING_REQUEST: 546 meetingRequestParser(msg); 547 break; 548 case Tags.RIGHTS_LICENSE: 549 skipParser(tag); 550 break; 551 case Tags.EMAIL2_CONVERSATION_ID: 552 case Tags.EMAIL2_CONVERSATION_INDEX: 553 // Note that the value of these two tags is a byte array 554 getValueBytes(); 555 break; 556 case Tags.EMAIL2_LAST_VERB_EXECUTED: 557 int val = getValueInt(); 558 if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) { 559 // We aren't required to distinguish between reply and reply all here 560 msg.mFlags |= Message.FLAG_REPLIED_TO; 561 } else if (val == LAST_VERB_FORWARD) { 562 msg.mFlags |= Message.FLAG_FORWARDED; 563 } 564 break; 565 default: 566 skipTag(); 567 } 568 } 569 570 if (atts.size() > 0) { 571 msg.mAttachments = atts; 572 } 573 } 574 575 /** 576 * Set up the meetingInfo field in the message with various pieces of information gleaned 577 * from MeetingRequest tags. This information will be used later to generate an appropriate 578 * reply email if the user chooses to respond 579 * @param msg the Message being built 580 * @throws IOException 581 */ 582 private void meetingRequestParser(Message msg) throws IOException { 583 PackedString.Builder packedString = new PackedString.Builder(); 584 while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) { 585 switch (tag) { 586 case Tags.EMAIL_DTSTAMP: 587 packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue()); 588 break; 589 case Tags.EMAIL_START_TIME: 590 packedString.put(MeetingInfo.MEETING_DTSTART, getValue()); 591 break; 592 case Tags.EMAIL_END_TIME: 593 packedString.put(MeetingInfo.MEETING_DTEND, getValue()); 594 break; 595 case Tags.EMAIL_ORGANIZER: 596 packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue()); 597 break; 598 case Tags.EMAIL_LOCATION: 599 packedString.put(MeetingInfo.MEETING_LOCATION, getValue()); 600 break; 601 case Tags.EMAIL_GLOBAL_OBJID: 602 packedString.put(MeetingInfo.MEETING_UID, 603 CalendarUtilities.getUidFromGlobalObjId(getValue())); 604 break; 605 case Tags.EMAIL_CATEGORIES: 606 skipParser(tag); 607 break; 608 case Tags.EMAIL_RECURRENCES: 609 recurrencesParser(); 610 break; 611 case Tags.EMAIL_RESPONSE_REQUESTED: 612 packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue()); 613 break; 614 default: 615 skipTag(); 616 } 617 } 618 if (msg.mSubject != null) { 619 packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject); 620 } 621 msg.mMeetingInfo = packedString.toString(); 622 } 623 624 private void recurrencesParser() throws IOException { 625 while (nextTag(Tags.EMAIL_RECURRENCES) != END) { 626 switch (tag) { 627 case Tags.EMAIL_RECURRENCE: 628 skipParser(tag); 629 break; 630 default: 631 skipTag(); 632 } 633 } 634 } 635 636 /** 637 * Parse a message from the server stream. 638 * @return the parsed Message 639 * @throws IOException 640 */ 641 private Message addParser() throws IOException, CommandStatusException { 642 Message msg = new Message(); 643 msg.mAccountKey = mAccount.mId; 644 msg.mMailboxKey = mMailbox.mId; 645 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 646 // Default to 1 (success) in case we don't get this tag 647 int status = 1; 648 649 while (nextTag(Tags.SYNC_ADD) != END) { 650 switch (tag) { 651 case Tags.SYNC_SERVER_ID: 652 msg.mServerId = getValue(); 653 break; 654 case Tags.SYNC_STATUS: 655 status = getValueInt(); 656 break; 657 case Tags.SYNC_APPLICATION_DATA: 658 addData(msg, tag); 659 break; 660 default: 661 skipTag(); 662 } 663 } 664 // For sync, status 1 = success 665 if (status != 1) { 666 throw new CommandStatusException(status, msg.mServerId); 667 } 668 return msg; 669 } 670 671 // For now, we only care about the "active" state 672 private Boolean flagParser() throws IOException { 673 Boolean state = false; 674 while (nextTag(Tags.EMAIL_FLAG) != END) { 675 switch (tag) { 676 case Tags.EMAIL_FLAG_STATUS: 677 state = getValueInt() == 2; 678 break; 679 default: 680 skipTag(); 681 } 682 } 683 return state; 684 } 685 686 private void bodyParser(Message msg) throws IOException { 687 String bodyType = Eas.BODY_PREFERENCE_TEXT; 688 String body = ""; 689 while (nextTag(Tags.EMAIL_BODY) != END) { 690 switch (tag) { 691 case Tags.BASE_TYPE: 692 bodyType = getValue(); 693 break; 694 case Tags.BASE_DATA: 695 body = getValue(); 696 break; 697 default: 698 skipTag(); 699 } 700 } 701 // We always ask for TEXT or HTML; there's no third option 702 if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) { 703 msg.mHtml = body; 704 } else { 705 msg.mText = body; 706 } 707 } 708 709 /** 710 * Parses untruncated MIME data, saving away the text parts 711 * @param msg the message we're building 712 * @param mimeData the MIME data we've received from the server 713 * @throws IOException 714 */ 715 private void mimeBodyParser(Message msg, String mimeData) throws IOException { 716 try { 717 ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes()); 718 // The constructor parses the message 719 MimeMessage mimeMessage = new MimeMessage(in); 720 // Now process body parts & attachments 721 ArrayList<Part> viewables = new ArrayList<Part>(); 722 // We'll ignore the attachments, as we'll get them directly from EAS 723 ArrayList<Part> attachments = new ArrayList<Part>(); 724 MimeUtility.collectParts(mimeMessage, viewables, attachments); 725 Body tempBody = new Body(); 726 // updateBodyFields fills in the content fields of the Body 727 ConversionUtilities.updateBodyFields(tempBody, msg, viewables); 728 // But we need them in the message itself for handling during commit() 729 msg.mHtml = tempBody.mHtmlContent; 730 msg.mText = tempBody.mTextContent; 731 } catch (MessagingException e) { 732 // This would most likely indicate a broken stream 733 throw new IOException(e); 734 } 735 } 736 737 private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException { 738 while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) { 739 switch (tag) { 740 case Tags.EMAIL_ATTACHMENT: 741 case Tags.BASE_ATTACHMENT: // BASE_ATTACHMENT is used in EAS 12.0 and up 742 attachmentParser(atts, msg); 743 break; 744 default: 745 skipTag(); 746 } 747 } 748 } 749 750 private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException { 751 String fileName = null; 752 String length = null; 753 String location = null; 754 755 while (nextTag(Tags.EMAIL_ATTACHMENT) != END) { 756 switch (tag) { 757 // We handle both EAS 2.5 and 12.0+ attachments here 758 case Tags.EMAIL_DISPLAY_NAME: 759 case Tags.BASE_DISPLAY_NAME: 760 fileName = getValue(); 761 break; 762 case Tags.EMAIL_ATT_NAME: 763 case Tags.BASE_FILE_REFERENCE: 764 location = getValue(); 765 break; 766 case Tags.EMAIL_ATT_SIZE: 767 case Tags.BASE_ESTIMATED_DATA_SIZE: 768 length = getValue(); 769 break; 770 default: 771 skipTag(); 772 } 773 } 774 775 if ((fileName != null) && (length != null) && (location != null)) { 776 Attachment att = new Attachment(); 777 att.mEncoding = "base64"; 778 att.mSize = Long.parseLong(length); 779 att.mFileName = fileName; 780 att.mLocation = location; 781 att.mMimeType = getMimeTypeFromFileName(fileName); 782 att.mAccountKey = mService.mAccount.mId; 783 // Check if this attachment can't be downloaded due to an account policy 784 if (mPolicy != null) { 785 if (mPolicy.mDontAllowAttachments || 786 (mPolicy.mMaxAttachmentSize > 0 && 787 (att.mSize > mPolicy.mMaxAttachmentSize))) { 788 att.mFlags = Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD; 789 } 790 } 791 atts.add(att); 792 msg.mFlagAttachment = true; 793 } 794 } 795 796 /** 797 * Returns an appropriate mimetype for the given file name's extension. If a mimetype 798 * cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension, 799 * if it exists or {@code application/octet-stream}]. 800 * At the moment, this is somewhat lame, since many file types aren't recognized 801 * @param fileName the file name to ponder 802 */ 803 // Note: The MimeTypeMap method currently uses a very limited set of mime types 804 // A bug has been filed against this issue. 805 public String getMimeTypeFromFileName(String fileName) { 806 String mimeType; 807 int lastDot = fileName.lastIndexOf('.'); 808 String extension = null; 809 if ((lastDot > 0) && (lastDot < fileName.length() - 1)) { 810 extension = fileName.substring(lastDot + 1).toLowerCase(); 811 } 812 if (extension == null) { 813 // A reasonable default for now. 814 mimeType = "application/octet-stream"; 815 } else { 816 mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 817 if (mimeType == null) { 818 mimeType = "application/" + extension; 819 } 820 } 821 return mimeType; 822 } 823 824 private Cursor getServerIdCursor(String serverId, String[] projection) { 825 mBindArguments[0] = serverId; 826 mBindArguments[1] = mMailboxIdAsString; 827 return mContentResolver.query(Message.CONTENT_URI, projection, 828 WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null); 829 } 830 831 @VisibleForTesting 832 void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException { 833 while (nextTag(entryTag) != END) { 834 switch (tag) { 835 case Tags.SYNC_SERVER_ID: 836 String serverId = getValue(); 837 // Find the message in this mailbox with the given serverId 838 Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION); 839 try { 840 if (c.moveToFirst()) { 841 deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN)); 842 if (Eas.USER_LOG) { 843 userLog("Deleting ", serverId + ", " 844 + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN)); 845 } 846 } 847 } finally { 848 c.close(); 849 } 850 break; 851 default: 852 skipTag(); 853 } 854 } 855 } 856 857 @VisibleForTesting 858 class ServerChange { 859 final long id; 860 final Boolean read; 861 final Boolean flag; 862 final Integer flags; 863 864 ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) { 865 id = _id; 866 read = _read; 867 flag = _flag; 868 flags = _flags; 869 } 870 } 871 872 @VisibleForTesting 873 void changeParser(ArrayList<ServerChange> changes) throws IOException { 874 String serverId = null; 875 Boolean oldRead = false; 876 Boolean oldFlag = false; 877 int flags = 0; 878 long id = 0; 879 while (nextTag(Tags.SYNC_CHANGE) != END) { 880 switch (tag) { 881 case Tags.SYNC_SERVER_ID: 882 serverId = getValue(); 883 Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION); 884 try { 885 if (c.moveToFirst()) { 886 userLog("Changing ", serverId); 887 oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ; 888 oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1; 889 flags = c.getInt(Message.LIST_FLAGS_COLUMN); 890 id = c.getLong(Message.LIST_ID_COLUMN); 891 } 892 } finally { 893 c.close(); 894 } 895 break; 896 case Tags.SYNC_APPLICATION_DATA: 897 changeApplicationDataParser(changes, oldRead, oldFlag, flags, id); 898 break; 899 default: 900 skipTag(); 901 } 902 } 903 } 904 905 private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead, 906 Boolean oldFlag, int oldFlags, long id) throws IOException { 907 Boolean read = null; 908 Boolean flag = null; 909 Integer flags = null; 910 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 911 switch (tag) { 912 case Tags.EMAIL_READ: 913 read = getValueInt() == 1; 914 break; 915 case Tags.EMAIL_FLAG: 916 flag = flagParser(); 917 break; 918 case Tags.EMAIL2_LAST_VERB_EXECUTED: 919 int val = getValueInt(); 920 // Clear out the old replied/forward flags and add in the new flag 921 flags = oldFlags & ~(Message.FLAG_REPLIED_TO | Message.FLAG_FORWARDED); 922 if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) { 923 // We aren't required to distinguish between reply and reply all here 924 flags |= Message.FLAG_REPLIED_TO; 925 } else if (val == LAST_VERB_FORWARD) { 926 flags |= Message.FLAG_FORWARDED; 927 } 928 break; 929 default: 930 skipTag(); 931 } 932 } 933 // See if there are flag changes re: read, flag (favorite) or replied/forwarded 934 if (((read != null) && !oldRead.equals(read)) || 935 ((flag != null) && !oldFlag.equals(flag)) || (flags != null)) { 936 changes.add(new ServerChange(id, read, flag, flags)); 937 } 938 } 939 940 /* (non-Javadoc) 941 * @see com.android.exchange.adapter.EasContentParser#commandsParser() 942 */ 943 @Override 944 public void commandsParser() throws IOException, CommandStatusException { 945 while (nextTag(Tags.SYNC_COMMANDS) != END) { 946 if (tag == Tags.SYNC_ADD) { 947 newEmails.add(addParser()); 948 incrementChangeCount(); 949 } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) { 950 deleteParser(deletedEmails, tag); 951 incrementChangeCount(); 952 } else if (tag == Tags.SYNC_CHANGE) { 953 changeParser(changedEmails); 954 incrementChangeCount(); 955 } else 956 skipTag(); 957 } 958 } 959 960 @Override 961 public void responsesParser() throws IOException { 962 while (nextTag(Tags.SYNC_RESPONSES) != END) { 963 if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) { 964 // We can ignore all of these 965 } else if (tag == Tags.SYNC_FETCH) { 966 try { 967 fetchedEmails.add(addParser()); 968 } catch (CommandStatusException sse) { 969 if (sse.mStatus == 8) { 970 // 8 = object not found; delete the message from EmailProvider 971 // No other status should be seen in a fetch response, except, perhaps, 972 // for some temporary server failure 973 mBindArguments[0] = sse.mItemId; 974 mBindArguments[1] = mMailboxIdAsString; 975 mContentResolver.delete(Message.CONTENT_URI, 976 WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments); 977 } 978 } 979 } 980 } 981 } 982 983 @Override 984 public void commit() { 985 // Use a batch operation to handle the changes 986 // TODO New mail notifications? Who looks for these? 987 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 988 989 for (Message msg: fetchedEmails) { 990 // Find the original message's id (by serverId and mailbox) 991 Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION); 992 String id = null; 993 try { 994 if (c.moveToFirst()) { 995 id = c.getString(EmailContent.ID_PROJECTION_COLUMN); 996 } 997 } finally { 998 c.close(); 999 } 1000 1001 // If we find one, we do two things atomically: 1) set the body text for the 1002 // message, and 2) mark the message loaded (i.e. completely loaded) 1003 if (id != null) { 1004 userLog("Fetched body successfully for ", id); 1005 mBindArgument[0] = id; 1006 ops.add(ContentProviderOperation.newUpdate(Body.CONTENT_URI) 1007 .withSelection(Body.MESSAGE_KEY + "=?", mBindArgument) 1008 .withValue(Body.TEXT_CONTENT, msg.mText) 1009 .build()); 1010 ops.add(ContentProviderOperation.newUpdate(Message.CONTENT_URI) 1011 .withSelection(EmailContent.RECORD_ID + "=?", mBindArgument) 1012 .withValue(Message.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE) 1013 .build()); 1014 } 1015 } 1016 1017 for (Message msg: newEmails) { 1018 msg.addSaveOps(ops); 1019 } 1020 1021 for (Long id : deletedEmails) { 1022 ops.add(ContentProviderOperation.newDelete( 1023 ContentUris.withAppendedId(Message.CONTENT_URI, id)).build()); 1024 AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id); 1025 } 1026 1027 if (!changedEmails.isEmpty()) { 1028 // Server wins in a conflict... 1029 for (ServerChange change : changedEmails) { 1030 ContentValues cv = new ContentValues(); 1031 if (change.read != null) { 1032 cv.put(MessageColumns.FLAG_READ, change.read); 1033 } 1034 if (change.flag != null) { 1035 cv.put(MessageColumns.FLAG_FAVORITE, change.flag); 1036 } 1037 if (change.flags != null) { 1038 cv.put(MessageColumns.FLAGS, change.flags); 1039 } 1040 ops.add(ContentProviderOperation.newUpdate( 1041 ContentUris.withAppendedId(Message.CONTENT_URI, change.id)) 1042 .withValues(cv) 1043 .build()); 1044 } 1045 } 1046 1047 // We only want to update the sync key here 1048 ContentValues mailboxValues = new ContentValues(); 1049 mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey); 1050 ops.add(ContentProviderOperation.newUpdate( 1051 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)) 1052 .withValues(mailboxValues).build()); 1053 1054 addCleanupOps(ops); 1055 1056 // No commits if we're stopped 1057 synchronized (mService.getSynchronizer()) { 1058 if (mService.isStopped()) return; 1059 try { 1060 mContentResolver.applyBatch(EmailContent.AUTHORITY, ops); 1061 userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey); 1062 } catch (RemoteException e) { 1063 // There is nothing to be done here; fail by returning null 1064 } catch (OperationApplicationException e) { 1065 // There is nothing to be done here; fail by returning null 1066 } 1067 } 1068 } 1069 } 1070 1071 @Override 1072 public String getCollectionName() { 1073 return "Email"; 1074 } 1075 1076 private void addCleanupOps(ArrayList<ContentProviderOperation> ops) { 1077 // If we've sent local deletions, clear out the deleted table 1078 for (Long id: mDeletedIdList) { 1079 ops.add(ContentProviderOperation.newDelete( 1080 ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build()); 1081 } 1082 // And same with the updates 1083 for (Long id: mUpdatedIdList) { 1084 ops.add(ContentProviderOperation.newDelete( 1085 ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build()); 1086 } 1087 // Delete any moved messages (since we've just synced the mailbox, and no longer need the 1088 // placeholder message); this prevents duplicates from appearing in the mailbox. 1089 mBindArgument[0] = Long.toString(mMailbox.mId); 1090 ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI) 1091 .withSelection(WHERE_MAILBOX_KEY_AND_MOVED, mBindArgument).build()); 1092 } 1093 1094 @Override 1095 public void cleanup() { 1096 if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) { 1097 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 1098 addCleanupOps(ops); 1099 try { 1100 mContext.getContentResolver() 1101 .applyBatch(EmailContent.AUTHORITY, ops); 1102 } catch (RemoteException e) { 1103 // There is nothing to be done here; fail by returning null 1104 } catch (OperationApplicationException e) { 1105 // There is nothing to be done here; fail by returning null 1106 } 1107 } 1108 } 1109 1110 private String formatTwo(int num) { 1111 if (num < 10) { 1112 return "0" + (char)('0' + num); 1113 } else 1114 return Integer.toString(num); 1115 } 1116 1117 /** 1118 * Create date/time in RFC8601 format. Oddly enough, for calendar date/time, Microsoft uses 1119 * a different format that excludes the punctuation (this is why I'm not putting this in a 1120 * parent class) 1121 */ 1122 public String formatDateTime(Calendar calendar) { 1123 StringBuilder sb = new StringBuilder(); 1124 //YYYY-MM-DDTHH:MM:SS.MSSZ 1125 sb.append(calendar.get(Calendar.YEAR)); 1126 sb.append('-'); 1127 sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1)); 1128 sb.append('-'); 1129 sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH))); 1130 sb.append('T'); 1131 sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY))); 1132 sb.append(':'); 1133 sb.append(formatTwo(calendar.get(Calendar.MINUTE))); 1134 sb.append(':'); 1135 sb.append(formatTwo(calendar.get(Calendar.SECOND))); 1136 sb.append(".000Z"); 1137 return sb.toString(); 1138 } 1139 1140 /** 1141 * Note that messages in the deleted database preserve the message's unique id; therefore, we 1142 * can utilize this id to find references to the message. The only reference situation at this 1143 * point is in the Body table; it is when sending messages via SmartForward and SmartReply 1144 */ 1145 private boolean messageReferenced(ContentResolver cr, long id) { 1146 mBindArgument[0] = Long.toString(id); 1147 // See if this id is referenced in a body 1148 Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY, 1149 mBindArgument, null); 1150 try { 1151 return c.moveToFirst(); 1152 } finally { 1153 c.close(); 1154 } 1155 } 1156 1157 /*private*/ /** 1158 * Serialize commands to delete items from the server; as we find items to delete, add their 1159 * id's to the deletedId's array 1160 * 1161 * @param s the Serializer we're using to create post data 1162 * @param deletedIds ids whose deletions are being sent to the server 1163 * @param first whether or not this is the first command being sent 1164 * @return true if SYNC_COMMANDS hasn't been sent (false otherwise) 1165 * @throws IOException 1166 */ 1167 @VisibleForTesting 1168 boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first) 1169 throws IOException { 1170 ContentResolver cr = mContext.getContentResolver(); 1171 1172 // Find any of our deleted items 1173 Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION, 1174 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); 1175 // We keep track of the list of deleted item id's so that we can remove them from the 1176 // deleted table after the server receives our command 1177 deletedIds.clear(); 1178 try { 1179 while (c.moveToNext()) { 1180 String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN); 1181 // Keep going if there's no serverId 1182 if (serverId == null) { 1183 continue; 1184 // Also check if this message is referenced elsewhere 1185 } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) { 1186 userLog("Postponing deletion of referenced message: ", serverId); 1187 continue; 1188 } else if (first) { 1189 s.start(Tags.SYNC_COMMANDS); 1190 first = false; 1191 } 1192 // Send the command to delete this message 1193 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 1194 deletedIds.add(c.getLong(Message.LIST_ID_COLUMN)); 1195 } 1196 } finally { 1197 c.close(); 1198 } 1199 1200 return first; 1201 } 1202 1203 @Override 1204 public boolean sendLocalChanges(Serializer s) throws IOException { 1205 ContentResolver cr = mContext.getContentResolver(); 1206 1207 if (getSyncKey().equals("0")) { 1208 return false; 1209 } 1210 1211 // Never upsync from these folders 1212 if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) { 1213 return false; 1214 } 1215 1216 // This code is split out for unit testing purposes 1217 boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true); 1218 1219 if (!mFetchRequestList.isEmpty()) { 1220 // Add FETCH commands for messages that need a body (i.e. we didn't find it during 1221 // our earlier sync; this happens only in EAS 2.5 where the body couldn't be found 1222 // after parsing the message's MIME data) 1223 if (firstCommand) { 1224 s.start(Tags.SYNC_COMMANDS); 1225 firstCommand = false; 1226 } 1227 for (FetchRequest req: mFetchRequestList) { 1228 s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, req.serverId).end(); 1229 } 1230 } 1231 1232 // Find our trash mailbox, since deletions will have been moved there... 1233 long trashMailboxId = 1234 Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH); 1235 1236 // Do the same now for updated items 1237 Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION, 1238 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); 1239 1240 // We keep track of the list of updated item id's as we did above with deleted items 1241 mUpdatedIdList.clear(); 1242 try { 1243 while (c.moveToNext()) { 1244 long id = c.getLong(Message.LIST_ID_COLUMN); 1245 // Say we've handled this update 1246 mUpdatedIdList.add(id); 1247 // We have the id of the changed item. But first, we have to find out its current 1248 // state, since the updated table saves the opriginal state 1249 Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id), 1250 UPDATES_PROJECTION, null, null, null); 1251 try { 1252 // If this item no longer exists (shouldn't be possible), just move along 1253 if (!currentCursor.moveToFirst()) { 1254 continue; 1255 } 1256 // Keep going if there's no serverId 1257 String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN); 1258 if (serverId == null) { 1259 continue; 1260 } 1261 // If the message is now in the trash folder, it has been deleted by the user 1262 if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) { 1263 if (firstCommand) { 1264 s.start(Tags.SYNC_COMMANDS); 1265 firstCommand = false; 1266 } 1267 // Send the command to delete this message 1268 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 1269 continue; 1270 } 1271 1272 boolean flagChange = false; 1273 boolean readChange = false; 1274 1275 long mailbox = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN); 1276 if (mailbox != c.getLong(Message.LIST_MAILBOX_KEY_COLUMN)) { 1277 // The message has moved to another mailbox; add a request for this 1278 // Note: The Sync command doesn't handle moving messages, so we need 1279 // to handle this as a "request" (similar to meeting response and 1280 // attachment load) 1281 mService.addRequest(new MessageMoveRequest(id, mailbox)); 1282 // Regardless of other changes that might be made, we don't want to indicate 1283 // that this message has been updated until the move request has been 1284 // handled (without this, a crash between the flag upsync and the move 1285 // would cause the move to be lost) 1286 mUpdatedIdList.remove(id); 1287 } 1288 1289 // We can only send flag changes to the server in 12.0 or later 1290 int flag = 0; 1291 if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 1292 flag = currentCursor.getInt(UPDATES_FLAG_COLUMN); 1293 if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) { 1294 flagChange = true; 1295 } 1296 } 1297 1298 int read = currentCursor.getInt(UPDATES_READ_COLUMN); 1299 if (read != c.getInt(Message.LIST_READ_COLUMN)) { 1300 readChange = true; 1301 } 1302 1303 if (!flagChange && !readChange) { 1304 // In this case, we've got nothing to send to the server 1305 continue; 1306 } 1307 1308 if (firstCommand) { 1309 s.start(Tags.SYNC_COMMANDS); 1310 firstCommand = false; 1311 } 1312 // Send the change to "read" and "favorite" (flagged) 1313 s.start(Tags.SYNC_CHANGE) 1314 .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN)) 1315 .start(Tags.SYNC_APPLICATION_DATA); 1316 if (readChange) { 1317 s.data(Tags.EMAIL_READ, Integer.toString(read)); 1318 } 1319 // "Flag" is a relatively complex concept in EAS 12.0 and above. It is not only 1320 // the boolean "favorite" that we think of in Gmail, but it also represents a 1321 // follow up action, which can include a subject, start and due dates, and even 1322 // recurrences. We don't support any of this as yet, but EAS 12.0 and higher 1323 // require that a flag contain a status, a type, and four date fields, two each 1324 // for start date and end (due) date. 1325 if (flagChange) { 1326 if (flag != 0) { 1327 // Status 2 = set flag 1328 s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2"); 1329 // "FollowUp" is the standard type 1330 s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp"); 1331 long now = System.currentTimeMillis(); 1332 Calendar calendar = 1333 GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT")); 1334 calendar.setTimeInMillis(now); 1335 // Flags are required to have a start date and end date (duplicated) 1336 // First, we'll set the current date/time in GMT as the start time 1337 String utc = formatDateTime(calendar); 1338 s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc); 1339 // And then we'll use one week from today for completion date 1340 calendar.setTimeInMillis(now + 1*WEEKS); 1341 utc = formatDateTime(calendar); 1342 s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc); 1343 s.end(); 1344 } else { 1345 s.tag(Tags.EMAIL_FLAG); 1346 } 1347 } 1348 s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE 1349 } finally { 1350 currentCursor.close(); 1351 } 1352 } 1353 } finally { 1354 c.close(); 1355 } 1356 1357 if (!firstCommand) { 1358 s.end(); // SYNC_COMMANDS 1359 } 1360 return false; 1361 } 1362} 1363