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