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