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 18 package com.android.exchange.adapter; 19 20 import com.android.email.Utility; 21 import com.android.email.mail.Address; 22 import com.android.email.mail.MeetingInfo; 23 import com.android.email.mail.PackedString; 24 import com.android.email.provider.AttachmentProvider; 25 import com.android.email.provider.EmailContent; 26 import com.android.email.provider.EmailProvider; 27 import com.android.email.provider.EmailContent.Account; 28 import com.android.email.provider.EmailContent.AccountColumns; 29 import com.android.email.provider.EmailContent.Attachment; 30 import com.android.email.provider.EmailContent.Body; 31 import com.android.email.provider.EmailContent.Mailbox; 32 import com.android.email.provider.EmailContent.Message; 33 import com.android.email.provider.EmailContent.MessageColumns; 34 import com.android.email.provider.EmailContent.SyncColumns; 35 import com.android.email.service.MailService; 36 import com.android.exchange.Eas; 37 import com.android.exchange.EasSyncService; 38 import com.android.exchange.utility.CalendarUtilities; 39 40 import android.content.ContentProviderOperation; 41 import android.content.ContentResolver; 42 import android.content.ContentUris; 43 import android.content.ContentValues; 44 import android.content.OperationApplicationException; 45 import android.database.Cursor; 46 import android.net.Uri; 47 import android.os.RemoteException; 48 import android.webkit.MimeTypeMap; 49 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.util.ArrayList; 53 import java.util.Calendar; 54 import java.util.GregorianCalendar; 55 import java.util.TimeZone; 56 57 /** 58 * Sync adapter for EAS email 59 * 60 */ 61 public class EmailSyncAdapter extends AbstractSyncAdapter { 62 63 private static final int UPDATES_READ_COLUMN = 0; 64 private static final int UPDATES_MAILBOX_KEY_COLUMN = 1; 65 private static final int UPDATES_SERVER_ID_COLUMN = 2; 66 private static final int UPDATES_FLAG_COLUMN = 3; 67 private static final String[] UPDATES_PROJECTION = 68 {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID, 69 MessageColumns.FLAG_FAVORITE}; 70 71 private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0; 72 private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1; 73 private static final String[] MESSAGE_ID_SUBJECT_PROJECTION = 74 new String[] { Message.RECORD_ID, MessageColumns.SUBJECT }; 75 76 private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?"; 77 78 String[] mBindArguments = new String[2]; 79 String[] mBindArgument = new String[1]; 80 81 ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); 82 ArrayList<Long> mUpdatedIdList = new ArrayList<Long>(); 83 84 // Holds the parser's value for isLooping() 85 boolean mIsLooping = false; 86 87 public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) { 88 super(mailbox, service); 89 } 90 91 @Override 92 public boolean parse(InputStream is) throws IOException { 93 EasEmailSyncParser p = new EasEmailSyncParser(is, this); 94 boolean res = p.parse(); 95 // Hold on to the parser's value for isLooping() to pass back to the service 96 mIsLooping = p.isLooping(); 97 return res; 98 } 99 100 /** 101 * Return the value of isLooping() as returned from the parser 102 */ 103 @Override 104 public boolean isLooping() { 105 return mIsLooping; 106 } 107 108 @Override 109 public boolean isSyncable() { 110 return true; 111 } 112 113 public class EasEmailSyncParser extends AbstractSyncParser { 114 115 private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = 116 SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?"; 117 118 private String mMailboxIdAsString; 119 120 ArrayList<Message> newEmails = new ArrayList<Message>(); 121 ArrayList<Long> deletedEmails = new ArrayList<Long>(); 122 ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>(); 123 124 public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException { 125 super(in, adapter); 126 mMailboxIdAsString = Long.toString(mMailbox.mId); 127 } 128 129 @Override 130 public void wipe() { 131 mContentResolver.delete(Message.CONTENT_URI, 132 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 133 mContentResolver.delete(Message.DELETED_CONTENT_URI, 134 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 135 mContentResolver.delete(Message.UPDATED_CONTENT_URI, 136 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 137 } 138 139 public void addData (Message msg) throws IOException { 140 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 141 142 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 143 switch (tag) { 144 case Tags.EMAIL_ATTACHMENTS: 145 case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up 146 attachmentsParser(atts, msg); 147 break; 148 case Tags.EMAIL_TO: 149 msg.mTo = Address.pack(Address.parse(getValue())); 150 break; 151 case Tags.EMAIL_FROM: 152 Address[] froms = Address.parse(getValue()); 153 if (froms != null && froms.length > 0) { 154 msg.mDisplayName = froms[0].toFriendly(); 155 } 156 msg.mFrom = Address.pack(froms); 157 break; 158 case Tags.EMAIL_CC: 159 msg.mCc = Address.pack(Address.parse(getValue())); 160 break; 161 case Tags.EMAIL_REPLY_TO: 162 msg.mReplyTo = Address.pack(Address.parse(getValue())); 163 break; 164 case Tags.EMAIL_DATE_RECEIVED: 165 msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue()); 166 break; 167 case Tags.EMAIL_SUBJECT: 168 msg.mSubject = getValue(); 169 break; 170 case Tags.EMAIL_READ: 171 msg.mFlagRead = getValueInt() == 1; 172 break; 173 case Tags.BASE_BODY: 174 bodyParser(msg); 175 break; 176 case Tags.EMAIL_FLAG: 177 msg.mFlagFavorite = flagParser(); 178 break; 179 case Tags.EMAIL_BODY: 180 String text = getValue(); 181 msg.mText = text; 182 break; 183 case Tags.EMAIL_MESSAGE_CLASS: 184 String messageClass = getValue(); 185 if (messageClass.equals("IPM.Schedule.Meeting.Request")) { 186 msg.mFlags |= Message.FLAG_INCOMING_MEETING_INVITE; 187 } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) { 188 msg.mFlags |= Message.FLAG_INCOMING_MEETING_CANCEL; 189 } 190 break; 191 case Tags.EMAIL_MEETING_REQUEST: 192 meetingRequestParser(msg); 193 break; 194 default: 195 skipTag(); 196 } 197 } 198 199 if (atts.size() > 0) { 200 msg.mAttachments = atts; 201 } 202 } 203 204 /** 205 * Set up the meetingInfo field in the message with various pieces of information gleaned 206 * from MeetingRequest tags. This information will be used later to generate an appropriate 207 * reply email if the user chooses to respond 208 * @param msg the Message being built 209 * @throws IOException 210 */ 211 private void meetingRequestParser(Message msg) throws IOException { 212 PackedString.Builder packedString = new PackedString.Builder(); 213 while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) { 214 switch (tag) { 215 case Tags.EMAIL_DTSTAMP: 216 packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue()); 217 break; 218 case Tags.EMAIL_START_TIME: 219 packedString.put(MeetingInfo.MEETING_DTSTART, getValue()); 220 break; 221 case Tags.EMAIL_END_TIME: 222 packedString.put(MeetingInfo.MEETING_DTEND, getValue()); 223 break; 224 case Tags.EMAIL_ORGANIZER: 225 packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue()); 226 break; 227 case Tags.EMAIL_LOCATION: 228 packedString.put(MeetingInfo.MEETING_LOCATION, getValue()); 229 break; 230 case Tags.EMAIL_GLOBAL_OBJID: 231 packedString.put(MeetingInfo.MEETING_UID, 232 CalendarUtilities.getUidFromGlobalObjId(getValue())); 233 break; 234 case Tags.EMAIL_CATEGORIES: 235 nullParser(); 236 break; 237 case Tags.EMAIL_RECURRENCES: 238 recurrencesParser(); 239 break; 240 default: 241 skipTag(); 242 } 243 } 244 if (msg.mSubject != null) { 245 packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject); 246 } 247 msg.mMeetingInfo = packedString.toString(); 248 } 249 250 private void nullParser() throws IOException { 251 while (nextTag(Tags.EMAIL_CATEGORIES) != END) { 252 skipTag(); 253 } 254 } 255 256 private void recurrencesParser() throws IOException { 257 while (nextTag(Tags.EMAIL_RECURRENCES) != END) { 258 switch (tag) { 259 case Tags.EMAIL_RECURRENCE: 260 nullParser(); 261 break; 262 default: 263 skipTag(); 264 } 265 } 266 } 267 268 private void addParser(ArrayList<Message> emails) throws IOException { 269 Message msg = new Message(); 270 msg.mAccountKey = mAccount.mId; 271 msg.mMailboxKey = mMailbox.mId; 272 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 273 274 while (nextTag(Tags.SYNC_ADD) != END) { 275 switch (tag) { 276 case Tags.SYNC_SERVER_ID: 277 msg.mServerId = getValue(); 278 break; 279 case Tags.SYNC_APPLICATION_DATA: 280 addData(msg); 281 break; 282 default: 283 skipTag(); 284 } 285 } 286 emails.add(msg); 287 } 288 289 // For now, we only care about the "active" state 290 private Boolean flagParser() throws IOException { 291 Boolean state = false; 292 while (nextTag(Tags.EMAIL_FLAG) != END) { 293 switch (tag) { 294 case Tags.EMAIL_FLAG_STATUS: 295 state = getValueInt() == 2; 296 break; 297 default: 298 skipTag(); 299 } 300 } 301 return state; 302 } 303 304 private void bodyParser(Message msg) throws IOException { 305 String bodyType = Eas.BODY_PREFERENCE_TEXT; 306 String body = ""; 307 while (nextTag(Tags.EMAIL_BODY) != END) { 308 switch (tag) { 309 case Tags.BASE_TYPE: 310 bodyType = getValue(); 311 break; 312 case Tags.BASE_DATA: 313 body = getValue(); 314 break; 315 default: 316 skipTag(); 317 } 318 } 319 // We always ask for TEXT or HTML; there's no third option 320 if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) { 321 msg.mHtml = body; 322 } else { 323 msg.mText = body; 324 } 325 } 326 327 private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException { 328 while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) { 329 switch (tag) { 330 case Tags.EMAIL_ATTACHMENT: 331 case Tags.BASE_ATTACHMENT: // BASE_ATTACHMENT is used in EAS 12.0 and up 332 attachmentParser(atts, msg); 333 break; 334 default: 335 skipTag(); 336 } 337 } 338 } 339 340 private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException { 341 String fileName = null; 342 String length = null; 343 String location = null; 344 345 while (nextTag(Tags.EMAIL_ATTACHMENT) != END) { 346 switch (tag) { 347 // We handle both EAS 2.5 and 12.0+ attachments here 348 case Tags.EMAIL_DISPLAY_NAME: 349 case Tags.BASE_DISPLAY_NAME: 350 fileName = getValue(); 351 break; 352 case Tags.EMAIL_ATT_NAME: 353 case Tags.BASE_FILE_REFERENCE: 354 location = getValue(); 355 break; 356 case Tags.EMAIL_ATT_SIZE: 357 case Tags.BASE_ESTIMATED_DATA_SIZE: 358 length = getValue(); 359 break; 360 default: 361 skipTag(); 362 } 363 } 364 365 if ((fileName != null) && (length != null) && (location != null)) { 366 Attachment att = new Attachment(); 367 att.mEncoding = "base64"; 368 att.mSize = Long.parseLong(length); 369 att.mFileName = fileName; 370 att.mLocation = location; 371 att.mMimeType = getMimeTypeFromFileName(fileName); 372 atts.add(att); 373 msg.mFlagAttachment = true; 374 } 375 } 376 377 /** 378 * Try to determine a mime type from a file name, defaulting to application/x, where x 379 * is either the extension or (if none) octet-stream 380 * At the moment, this is somewhat lame, since many file types aren't recognized 381 * @param fileName the file name to ponder 382 * @return 383 */ 384 // Note: The MimeTypeMap method currently uses a very limited set of mime types 385 // A bug has been filed against this issue. 386 public String getMimeTypeFromFileName(String fileName) { 387 String mimeType; 388 int lastDot = fileName.lastIndexOf('.'); 389 String extension = null; 390 if ((lastDot > 0) && (lastDot < fileName.length() - 1)) { 391 extension = fileName.substring(lastDot + 1).toLowerCase(); 392 } 393 if (extension == null) { 394 // A reasonable default for now. 395 mimeType = "application/octet-stream"; 396 } else { 397 mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 398 if (mimeType == null) { 399 mimeType = "application/" + extension; 400 } 401 } 402 return mimeType; 403 } 404 405 private Cursor getServerIdCursor(String serverId, String[] projection) { 406 mBindArguments[0] = serverId; 407 mBindArguments[1] = mMailboxIdAsString; 408 return mContentResolver.query(Message.CONTENT_URI, projection, 409 WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null); 410 } 411 412 /*package*/ void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException { 413 while (nextTag(entryTag) != END) { 414 switch (tag) { 415 case Tags.SYNC_SERVER_ID: 416 String serverId = getValue(); 417 // Find the message in this mailbox with the given serverId 418 Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION); 419 try { 420 if (c.moveToFirst()) { 421 deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN)); 422 if (Eas.USER_LOG) { 423 userLog("Deleting ", serverId + ", " 424 + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN)); 425 } 426 } 427 } finally { 428 c.close(); 429 } 430 break; 431 default: 432 skipTag(); 433 } 434 } 435 } 436 437 class ServerChange { 438 long id; 439 Boolean read; 440 Boolean flag; 441 442 ServerChange(long _id, Boolean _read, Boolean _flag) { 443 id = _id; 444 read = _read; 445 flag = _flag; 446 } 447 } 448 449 /*package*/ void changeParser(ArrayList<ServerChange> changes) throws IOException { 450 String serverId = null; 451 Boolean oldRead = false; 452 Boolean oldFlag = false; 453 long id = 0; 454 while (nextTag(Tags.SYNC_CHANGE) != END) { 455 switch (tag) { 456 case Tags.SYNC_SERVER_ID: 457 serverId = getValue(); 458 Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION); 459 try { 460 if (c.moveToFirst()) { 461 userLog("Changing ", serverId); 462 oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ; 463 oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1; 464 id = c.getLong(Message.LIST_ID_COLUMN); 465 } 466 } finally { 467 c.close(); 468 } 469 break; 470 case Tags.SYNC_APPLICATION_DATA: 471 changeApplicationDataParser(changes, oldRead, oldFlag, id); 472 break; 473 default: 474 skipTag(); 475 } 476 } 477 } 478 479 private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead, 480 Boolean oldFlag, long id) throws IOException { 481 Boolean read = null; 482 Boolean flag = null; 483 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 484 switch (tag) { 485 case Tags.EMAIL_READ: 486 read = getValueInt() == 1; 487 break; 488 case Tags.EMAIL_FLAG: 489 flag = flagParser(); 490 break; 491 default: 492 skipTag(); 493 } 494 } 495 if (((read != null) && !oldRead.equals(read)) || 496 ((flag != null) && !oldFlag.equals(flag))) { 497 changes.add(new ServerChange(id, read, flag)); 498 } 499 } 500 501 /* (non-Javadoc) 502 * @see com.android.exchange.adapter.EasContentParser#commandsParser() 503 */ 504 @Override 505 public void commandsParser() throws IOException { 506 while (nextTag(Tags.SYNC_COMMANDS) != END) { 507 if (tag == Tags.SYNC_ADD) { 508 addParser(newEmails); 509 incrementChangeCount(); 510 } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) { 511 deleteParser(deletedEmails, tag); 512 incrementChangeCount(); 513 } else if (tag == Tags.SYNC_CHANGE) { 514 changeParser(changedEmails); 515 incrementChangeCount(); 516 } else 517 skipTag(); 518 } 519 } 520 521 @Override 522 public void responsesParser() { 523 } 524 525 @Override 526 public void commit() { 527 int notifyCount = 0; 528 529 // Use a batch operation to handle the changes 530 // TODO New mail notifications? Who looks for these? 531 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 532 for (Message msg: newEmails) { 533 if (!msg.mFlagRead) { 534 notifyCount++; 535 } 536 msg.addSaveOps(ops); 537 } 538 for (Long id : deletedEmails) { 539 ops.add(ContentProviderOperation.newDelete( 540 ContentUris.withAppendedId(Message.CONTENT_URI, id)).build()); 541 AttachmentProvider.deleteAllAttachmentFiles(mContext, mAccount.mId, id); 542 } 543 if (!changedEmails.isEmpty()) { 544 // Server wins in a conflict... 545 for (ServerChange change : changedEmails) { 546 ContentValues cv = new ContentValues(); 547 if (change.read != null) { 548 cv.put(MessageColumns.FLAG_READ, change.read); 549 } 550 if (change.flag != null) { 551 cv.put(MessageColumns.FLAG_FAVORITE, change.flag); 552 } 553 ops.add(ContentProviderOperation.newUpdate( 554 ContentUris.withAppendedId(Message.CONTENT_URI, change.id)) 555 .withValues(cv) 556 .build()); 557 } 558 } 559 560 // We only want to update the sync key here 561 ContentValues mailboxValues = new ContentValues(); 562 mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey); 563 ops.add(ContentProviderOperation.newUpdate( 564 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)) 565 .withValues(mailboxValues).build()); 566 567 addCleanupOps(ops); 568 569 // No commits if we're stopped 570 synchronized (mService.getSynchronizer()) { 571 if (mService.isStopped()) return; 572 try { 573 mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); 574 userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey); 575 } catch (RemoteException e) { 576 // There is nothing to be done here; fail by returning null 577 } catch (OperationApplicationException e) { 578 // There is nothing to be done here; fail by returning null 579 } 580 } 581 582 if (notifyCount > 0) { 583 // Use the new atomic add URI in EmailProvider 584 // We could add this to the operations being done, but it's not strictly 585 // speaking necessary, as the previous batch preserves the integrity of the 586 // database, whereas this is purely for notification purposes, and is itself atomic 587 ContentValues cv = new ContentValues(); 588 cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT); 589 cv.put(EmailContent.ADD_COLUMN_NAME, notifyCount); 590 Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, mAccount.mId); 591 mContentResolver.update(uri, cv, null, null); 592 MailService.actionNotifyNewMessages(mContext, mAccount.mId); 593 } 594 } 595 } 596 597 @Override 598 public String getCollectionName() { 599 return "Email"; 600 } 601 602 private void addCleanupOps(ArrayList<ContentProviderOperation> ops) { 603 // If we've sent local deletions, clear out the deleted table 604 for (Long id: mDeletedIdList) { 605 ops.add(ContentProviderOperation.newDelete( 606 ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build()); 607 } 608 // And same with the updates 609 for (Long id: mUpdatedIdList) { 610 ops.add(ContentProviderOperation.newDelete( 611 ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build()); 612 } 613 } 614 615 @Override 616 public void cleanup() { 617 if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) { 618 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 619 addCleanupOps(ops); 620 try { 621 mContext.getContentResolver() 622 .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); 623 } catch (RemoteException e) { 624 // There is nothing to be done here; fail by returning null 625 } catch (OperationApplicationException e) { 626 // There is nothing to be done here; fail by returning null 627 } 628 } 629 } 630 631 private String formatTwo(int num) { 632 if (num < 10) { 633 return "0" + (char)('0' + num); 634 } else 635 return Integer.toString(num); 636 } 637 638 /** 639 * Create date/time in RFC8601 format. Oddly enough, for calendar date/time, Microsoft uses 640 * a different format that excludes the punctuation (this is why I'm not putting this in a 641 * parent class) 642 */ 643 public String formatDateTime(Calendar calendar) { 644 StringBuilder sb = new StringBuilder(); 645 //YYYY-MM-DDTHH:MM:SS.MSSZ 646 sb.append(calendar.get(Calendar.YEAR)); 647 sb.append('-'); 648 sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1)); 649 sb.append('-'); 650 sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH))); 651 sb.append('T'); 652 sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY))); 653 sb.append(':'); 654 sb.append(formatTwo(calendar.get(Calendar.MINUTE))); 655 sb.append(':'); 656 sb.append(formatTwo(calendar.get(Calendar.SECOND))); 657 sb.append(".000Z"); 658 return sb.toString(); 659 } 660 661 /** 662 * Note that messages in the deleted database preserve the message's unique id; therefore, we 663 * can utilize this id to find references to the message. The only reference situation at this 664 * point is in the Body table; it is when sending messages via SmartForward and SmartReply 665 */ 666 private boolean messageReferenced(ContentResolver cr, long id) { 667 mBindArgument[0] = Long.toString(id); 668 // See if this id is referenced in a body 669 Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY, 670 mBindArgument, null); 671 try { 672 return c.moveToFirst(); 673 } finally { 674 c.close(); 675 } 676 } 677 678 /*private*/ /** 679 * Serialize commands to delete items from the server; as we find items to delete, add their 680 * id's to the deletedId's array 681 * 682 * @param s the Serializer we're using to create post data 683 * @param deletedIds ids whose deletions are being sent to the server 684 * @param first whether or not this is the first command being sent 685 * @return true if SYNC_COMMANDS hasn't been sent (false otherwise) 686 * @throws IOException 687 */ 688 boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first) 689 throws IOException { 690 ContentResolver cr = mContext.getContentResolver(); 691 692 // Find any of our deleted items 693 Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION, 694 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); 695 // We keep track of the list of deleted item id's so that we can remove them from the 696 // deleted table after the server receives our command 697 deletedIds.clear(); 698 try { 699 while (c.moveToNext()) { 700 String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN); 701 // Keep going if there's no serverId 702 if (serverId == null) { 703 continue; 704 // Also check if this message is referenced elsewhere 705 } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) { 706 userLog("Postponing deletion of referenced message: ", serverId); 707 continue; 708 } else if (first) { 709 s.start(Tags.SYNC_COMMANDS); 710 first = false; 711 } 712 // Send the command to delete this message 713 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 714 deletedIds.add(c.getLong(Message.LIST_ID_COLUMN)); 715 } 716 } finally { 717 c.close(); 718 } 719 720 return first; 721 } 722 723 @Override 724 public boolean sendLocalChanges(Serializer s) throws IOException { 725 ContentResolver cr = mContext.getContentResolver(); 726 727 // Never upsync from these folders 728 if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) { 729 return false; 730 } 731 732 // This code is split out for unit testing purposes 733 boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true); 734 735 // Find our trash mailbox, since deletions will have been moved there... 736 long trashMailboxId = 737 Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH); 738 739 // Do the same now for updated items 740 Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION, 741 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); 742 743 // We keep track of the list of updated item id's as we did above with deleted items 744 mUpdatedIdList.clear(); 745 try { 746 while (c.moveToNext()) { 747 long id = c.getLong(Message.LIST_ID_COLUMN); 748 // Say we've handled this update 749 mUpdatedIdList.add(id); 750 // We have the id of the changed item. But first, we have to find out its current 751 // state, since the updated table saves the opriginal state 752 Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id), 753 UPDATES_PROJECTION, null, null, null); 754 try { 755 // If this item no longer exists (shouldn't be possible), just move along 756 if (!currentCursor.moveToFirst()) { 757 continue; 758 } 759 // Keep going if there's no serverId 760 String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN); 761 if (serverId == null) { 762 continue; 763 } 764 // If the message is now in the trash folder, it has been deleted by the user 765 if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) { 766 if (firstCommand) { 767 s.start(Tags.SYNC_COMMANDS); 768 firstCommand = false; 769 } 770 // Send the command to delete this message 771 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 772 continue; 773 } 774 775 boolean flagChange = false; 776 boolean readChange = false; 777 778 int flag = 0; 779 780 // We can only send flag changes to the server in 12.0 or later 781 if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 782 flag = currentCursor.getInt(UPDATES_FLAG_COLUMN); 783 if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) { 784 flagChange = true; 785 } 786 } 787 788 int read = currentCursor.getInt(UPDATES_READ_COLUMN); 789 if (read != c.getInt(Message.LIST_READ_COLUMN)) { 790 readChange = true; 791 } 792 793 if (!flagChange && !readChange) { 794 // In this case, we've got nothing to send to the server 795 continue; 796 } 797 798 if (firstCommand) { 799 s.start(Tags.SYNC_COMMANDS); 800 firstCommand = false; 801 } 802 // Send the change to "read" and "favorite" (flagged) 803 s.start(Tags.SYNC_CHANGE) 804 .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN)) 805 .start(Tags.SYNC_APPLICATION_DATA); 806 if (readChange) { 807 s.data(Tags.EMAIL_READ, Integer.toString(read)); 808 } 809 // "Flag" is a relatively complex concept in EAS 12.0 and above. It is not only 810 // the boolean "favorite" that we think of in Gmail, but it also represents a 811 // follow up action, which can include a subject, start and due dates, and even 812 // recurrences. We don't support any of this as yet, but EAS 12.0 and higher 813 // require that a flag contain a status, a type, and four date fields, two each 814 // for start date and end (due) date. 815 if (flagChange) { 816 if (flag != 0) { 817 // Status 2 = set flag 818 s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2"); 819 // "FollowUp" is the standard type 820 s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp"); 821 long now = System.currentTimeMillis(); 822 Calendar calendar = 823 GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT")); 824 calendar.setTimeInMillis(now); 825 // Flags are required to have a start date and end date (duplicated) 826 // First, we'll set the current date/time in GMT as the start time 827 String utc = formatDateTime(calendar); 828 s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc); 829 // And then we'll use one week from today for completion date 830 calendar.setTimeInMillis(now + 1*WEEKS); 831 utc = formatDateTime(calendar); 832 s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc); 833 s.end(); 834 } else { 835 s.tag(Tags.EMAIL_FLAG); 836 } 837 } 838 s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE 839 } finally { 840 currentCursor.close(); 841 } 842 } 843 } finally { 844 c.close(); 845 } 846 847 if (!firstCommand) { 848 s.end(); // SYNC_COMMANDS 849 } 850 return false; 851 } 852 } 853