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