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