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