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