• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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