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