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