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