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