Pop3Service.java revision e2166f75486da0a1b70b804ea34f11f600f11cfd
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.service;
18
19import android.app.Service;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.database.Cursor;
26import android.net.TrafficStats;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.IBinder;
30import android.os.RemoteCallbackList;
31import android.os.RemoteException;
32import android.util.Log;
33
34import com.android.email.Email;
35import com.android.email.LegacyConversions;
36import com.android.email.NotificationController;
37import com.android.email.mail.Store;
38import com.android.email.provider.Utilities;
39import com.android.emailcommon.AccountManagerTypes;
40import com.android.emailcommon.Logging;
41import com.android.emailcommon.TrafficFlags;
42import com.android.emailcommon.internet.MimeUtility;
43import com.android.emailcommon.mail.AuthenticationFailedException;
44import com.android.emailcommon.mail.FetchProfile;
45import com.android.emailcommon.mail.Flag;
46import com.android.emailcommon.mail.Folder;
47import com.android.emailcommon.mail.Folder.FolderType;
48import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
49import com.android.emailcommon.mail.Folder.OpenMode;
50import com.android.emailcommon.mail.Message;
51import com.android.emailcommon.mail.MessagingException;
52import com.android.emailcommon.mail.Part;
53import com.android.emailcommon.provider.Account;
54import com.android.emailcommon.provider.EmailContent;
55import com.android.emailcommon.provider.EmailContent.MailboxColumns;
56import com.android.emailcommon.provider.EmailContent.MessageColumns;
57import com.android.emailcommon.provider.EmailContent.SyncColumns;
58import com.android.emailcommon.provider.Mailbox;
59import com.android.emailcommon.service.EmailServiceStatus;
60import com.android.emailcommon.service.IEmailServiceCallback;
61import com.android.emailcommon.utility.AttachmentUtilities;
62
63import java.util.ArrayList;
64import java.util.HashMap;
65import java.util.HashSet;
66
67public class Pop3Service extends Service {
68    private static final String TAG = "Pop3Service";
69    private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
70
71    @Override
72    public int onStartCommand(Intent intent, int flags, int startId) {
73        return Service.START_STICKY;
74    }
75
76    // Callbacks as set up via setCallback
77    private static final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
78            new RemoteCallbackList<IEmailServiceCallback>();
79
80    private interface ServiceCallbackWrapper {
81        public void call(IEmailServiceCallback cb) throws RemoteException;
82    }
83
84    /**
85     * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
86     * Used this way:  ExchangeService.callback().callbackMethod(args...);
87     * The proxy wraps checking for existence of a ExchangeService instance
88     * Failures of these callbacks can be safely ignored.
89     */
90    static private final IEmailServiceCallback.Stub sCallbackProxy =
91            new IEmailServiceCallback.Stub() {
92
93        /**
94         * Broadcast a callback to the everyone that's registered
95         *
96         * @param wrapper the ServiceCallbackWrapper used in the broadcast
97         */
98        private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
99            RemoteCallbackList<IEmailServiceCallback> callbackList = mCallbackList;
100            if (callbackList != null) {
101                // Call everyone on our callback list
102                int count = callbackList.beginBroadcast();
103                try {
104                    for (int i = 0; i < count; i++) {
105                        try {
106                            wrapper.call(callbackList.getBroadcastItem(i));
107                        } catch (RemoteException e) {
108                            // Safe to ignore
109                        } catch (RuntimeException e) {
110                            // We don't want an exception in one call to prevent other calls, so
111                            // we'll just log this and continue
112                            Log.e(TAG, "Caught RuntimeException in broadcast", e);
113                        }
114                    }
115                } finally {
116                    // No matter what, we need to finish the broadcast
117                    callbackList.finishBroadcast();
118                }
119            }
120        }
121
122        @Override
123        public void loadAttachmentStatus(final long messageId, final long attachmentId,
124                final int status, final int progress) {
125            broadcastCallback(new ServiceCallbackWrapper() {
126                @Override
127                public void call(IEmailServiceCallback cb) throws RemoteException {
128                    cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
129                }
130            });
131        }
132
133        @Override
134        public void loadMessageStatus(final long messageId, final int status, final int progress) {
135            broadcastCallback(new ServiceCallbackWrapper() {
136                @Override
137                public void call(IEmailServiceCallback cb) throws RemoteException {
138                    cb.loadMessageStatus(messageId, status, progress);
139                }
140            });
141        }
142
143        @Override
144        public void sendMessageStatus(final long messageId, final String subject, final int status,
145                final int progress) {
146            broadcastCallback(new ServiceCallbackWrapper() {
147                @Override
148                public void call(IEmailServiceCallback cb) throws RemoteException {
149                    cb.sendMessageStatus(messageId, subject, status, progress);
150                }
151            });
152        }
153
154        @Override
155        public void syncMailboxListStatus(final long accountId, final int status,
156                final int progress) {
157            broadcastCallback(new ServiceCallbackWrapper() {
158                @Override
159                public void call(IEmailServiceCallback cb) throws RemoteException {
160                    cb.syncMailboxListStatus(accountId, status, progress);
161                }
162            });
163        }
164
165        @Override
166        public void syncMailboxStatus(final long mailboxId, final int status,
167                final int progress) {
168            broadcastCallback(new ServiceCallbackWrapper() {
169                @Override
170                public void call(IEmailServiceCallback cb) throws RemoteException {
171                    cb.syncMailboxStatus(mailboxId, status, progress);
172                }
173            });
174        }
175    };
176
177    /**
178     * Create our EmailService implementation here.
179     */
180    private final EmailServiceStub mBinder = new EmailServiceStub() {
181
182        @Override
183        public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
184            Context context = getApplicationContext();
185            Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
186            if (mailbox == null) return;
187            Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
188            if (account == null) return;
189            android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
190                    AccountManagerTypes.TYPE_POP_IMAP);
191            Log.d(TAG, "startSync API requesting sync");
192            Bundle extras = new Bundle();
193            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
194            ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
195        }
196
197        @Override
198        public void setCallback(IEmailServiceCallback cb) throws RemoteException {
199            mCallbackList.register(cb);
200        }
201    };
202
203    @Override
204    public IBinder onBind(Intent intent) {
205        mBinder.init(this, sCallbackProxy);
206        return mBinder;
207    }
208
209    private static void sendMailboxStatus(Mailbox mailbox, int status) {
210        try {
211            sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
212        } catch (RemoteException e) {
213        }
214    }
215
216    /**
217     * Start foreground synchronization of the specified folder. This is called by
218     * synchronizeMailbox or checkMail.
219     * TODO this should use ID's instead of fully-restored objects
220     * @param account
221     * @param folder
222     * @throws MessagingException
223     */
224    public static void synchronizeMailboxSynchronous(Context context, final Account account,
225            final Mailbox folder) throws MessagingException {
226        sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS);
227
228        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
229        if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
230            sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
231        }
232        NotificationController nc = NotificationController.getInstance(context);
233        try {
234            processPendingActionsSynchronous(context, account);
235            synchronizeMailboxGeneric(context, account, folder);
236            // Clear authentication notification for this account
237            nc.cancelLoginFailedNotification(account.mId);
238            sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
239        } catch (MessagingException e) {
240            if (Logging.LOGD) {
241                Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
242            }
243            if (e instanceof AuthenticationFailedException) {
244                // Generate authentication notification
245                nc.showLoginFailedNotification(account.mId);
246            }
247            sendMailboxStatus(folder, e.getExceptionType());
248            throw e;
249        }
250    }
251
252    /**
253     * Lightweight record for the first pass of message sync, where I'm just seeing if
254     * the local message requires sync.  Later (for messages that need syncing) we'll do a full
255     * readout from the DB.
256     */
257    private static class LocalMessageInfo {
258        private static final int COLUMN_ID = 0;
259        private static final int COLUMN_FLAG_READ = 1;
260        private static final int COLUMN_FLAG_FAVORITE = 2;
261        private static final int COLUMN_FLAG_LOADED = 3;
262        private static final int COLUMN_SERVER_ID = 4;
263        private static final int COLUMN_FLAGS =  7;
264        private static final String[] PROJECTION = new String[] {
265            EmailContent.RECORD_ID,
266            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
267            SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
268            MessageColumns.FLAGS
269        };
270
271        final long mId;
272        final boolean mFlagRead;
273        final boolean mFlagFavorite;
274        final int mFlagLoaded;
275        final String mServerId;
276        final int mFlags;
277
278        public LocalMessageInfo(Cursor c) {
279            mId = c.getLong(COLUMN_ID);
280            mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
281            mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
282            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
283            mServerId = c.getString(COLUMN_SERVER_ID);
284            mFlags = c.getInt(COLUMN_FLAGS);
285            // Note: mailbox key and account key not needed - they are projected for the SELECT
286        }
287    }
288
289    private static void saveOrUpdate(EmailContent content, Context context) {
290        if (content.isSaved()) {
291            content.update(context, content.toContentValues());
292        } else {
293            content.save(context);
294        }
295    }
296
297    /**
298     * Load the structure and body of messages not yet synced
299     * @param account the account we're syncing
300     * @param remoteFolder the (open) Folder we're working on
301     * @param unsyncedMessages an array of Message's we've got headers for
302     * @param toMailbox the destination mailbox we're syncing
303     * @throws MessagingException
304     */
305    static void loadUnsyncedMessages(final Context context, final Account account,
306            Folder remoteFolder, ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)
307            throws MessagingException {
308
309        // 1. Divide the unsynced messages into small & large (by size)
310
311        // TODO doing this work here (synchronously) is problematic because it prevents the UI
312        // from affecting the order (e.g. download a message because the user requested it.)  Much
313        // of this logic should move out to a different sync loop that attempts to update small
314        // groups of messages at a time, as a background task.  However, we can't just return
315        // (yet) because POP messages don't have an envelope yet....
316
317        ArrayList<Message> largeMessages = new ArrayList<Message>();
318        ArrayList<Message> smallMessages = new ArrayList<Message>();
319        for (Message message : unsyncedMessages) {
320            if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
321                largeMessages.add(message);
322            } else {
323                smallMessages.add(message);
324            }
325        }
326
327        // 2. Download small messages
328
329        // TODO Problems with this implementation.  1. For IMAP, where we get a real envelope,
330        // this is going to be inefficient and duplicate work we've already done.  2.  It's going
331        // back to the DB for a local message that we already had (and discarded).
332
333        // For small messages, we specify "body", which returns everything (incl. attachments)
334        FetchProfile fp = new FetchProfile();
335        fp.add(FetchProfile.Item.BODY);
336        remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
337                new MessageRetrievalListener() {
338                    @Override
339                    public void messageRetrieved(Message message) {
340                        // Store the updated message locally and mark it fully loaded
341                        Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
342                                EmailContent.Message.FLAG_LOADED_COMPLETE);
343                    }
344
345                    @Override
346                    public void loadAttachmentProgress(int progress) {
347                    }
348        });
349
350        // 3. Download large messages.  We ask the server to give us the message structure,
351        // but not all of the attachments.
352        fp.clear();
353        fp.add(FetchProfile.Item.STRUCTURE);
354        remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
355        for (Message message : largeMessages) {
356            if (message.getBody() == null) {
357                // POP doesn't support STRUCTURE mode, so we'll just do a partial download
358                // (hopefully enough to see some/all of the body) and mark the message for
359                // further download.
360                fp.clear();
361                fp.add(FetchProfile.Item.BODY_SANE);
362                //  TODO a good optimization here would be to make sure that all Stores set
363                //  the proper size after this fetch and compare the before and after size. If
364                //  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
365                remoteFolder.fetch(new Message[] { message }, fp, null);
366
367                // Store the partially-loaded message and mark it partially loaded
368                Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
369                        EmailContent.Message.FLAG_LOADED_PARTIAL);
370            } else {
371                // We have a structure to deal with, from which
372                // we can pull down the parts we want to actually store.
373                // Build a list of parts we are interested in. Text parts will be downloaded
374                // right now, attachments will be left for later.
375                ArrayList<Part> viewables = new ArrayList<Part>();
376                ArrayList<Part> attachments = new ArrayList<Part>();
377                MimeUtility.collectParts(message, viewables, attachments);
378                // Download the viewables immediately
379                for (Part part : viewables) {
380                    fp.clear();
381                    fp.add(part);
382                    // TODO what happens if the network connection dies? We've got partial
383                    // messages with incorrect status stored.
384                    remoteFolder.fetch(new Message[] { message }, fp, null);
385                }
386                // Store the updated message locally and mark it fully loaded
387                Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
388                        EmailContent.Message.FLAG_LOADED_COMPLETE);
389            }
390        }
391
392    }
393
394    public static void downloadFlagAndEnvelope(final Context context, final Account account,
395            final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
396            HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
397            throws MessagingException {
398        FetchProfile fp = new FetchProfile();
399        fp.add(FetchProfile.Item.FLAGS);
400        fp.add(FetchProfile.Item.ENVELOPE);
401
402        final HashMap<String, LocalMessageInfo> localMapCopy;
403        if (localMessageMap != null)
404            localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
405        else {
406            localMapCopy = new HashMap<String, LocalMessageInfo>();
407        }
408
409        remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
410                new MessageRetrievalListener() {
411                    @Override
412                    public void messageRetrieved(Message message) {
413                        try {
414                            // Determine if the new message was already known (e.g. partial)
415                            // And create or reload the full message info
416                            LocalMessageInfo localMessageInfo =
417                                localMapCopy.get(message.getUid());
418                            EmailContent.Message localMessage = null;
419                            if (localMessageInfo == null) {
420                                localMessage = new EmailContent.Message();
421                            } else {
422                                localMessage = EmailContent.Message.restoreMessageWithId(
423                                        context, localMessageInfo.mId);
424                            }
425
426                            if (localMessage != null) {
427                                try {
428                                    // Copy the fields that are available into the message
429                                    LegacyConversions.updateMessageFields(localMessage,
430                                            message, account.mId, mailbox.mId);
431                                    // Commit the message to the local store
432                                    saveOrUpdate(localMessage, context);
433                                    // Track the "new" ness of the downloaded message
434                                    if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
435                                        unseenMessages.add(localMessage.mId);
436                                    }
437                                } catch (MessagingException me) {
438                                    Log.e(Logging.LOG_TAG,
439                                            "Error while copying downloaded message." + me);
440                                }
441
442                            }
443                        }
444                        catch (Exception e) {
445                            Log.e(Logging.LOG_TAG,
446                                    "Error while storing downloaded message." + e.toString());
447                        }
448                    }
449
450                    @Override
451                    public void loadAttachmentProgress(int progress) {
452                    }
453                });
454
455    }
456
457    /**
458     * Synchronizer for IMAP.
459     *
460     * TODO Break this method up into smaller chunks.
461     *
462     * @param account the account to sync
463     * @param mailbox the mailbox to sync
464     * @return results of the sync pass
465     * @throws MessagingException
466     */
467    private static void synchronizeMailboxGeneric(final Context context,
468            final Account account, final Mailbox mailbox) throws MessagingException {
469
470        /*
471         * A list of IDs for messages that were downloaded and did not have the seen flag set.
472         * This serves as the "true" new message count reported to the user via notification.
473         */
474        final ArrayList<Long> unseenMessages = new ArrayList<Long>();
475
476        ContentResolver resolver = context.getContentResolver();
477
478        // 0.  We do not ever sync DRAFTS or OUTBOX (down or up)
479        if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
480            return;
481        }
482
483        // 1.  Get the message list from the local store and create an index of the uids
484
485        Cursor localUidCursor = null;
486        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
487
488        try {
489            localUidCursor = resolver.query(
490                    EmailContent.Message.CONTENT_URI,
491                    LocalMessageInfo.PROJECTION,
492                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
493                    " AND " + MessageColumns.MAILBOX_KEY + "=?",
494                    new String[] {
495                            String.valueOf(account.mId),
496                            String.valueOf(mailbox.mId)
497                    },
498                    null);
499            while (localUidCursor.moveToNext()) {
500                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
501                localMessageMap.put(info.mServerId, info);
502            }
503        } finally {
504            if (localUidCursor != null) {
505                localUidCursor.close();
506            }
507        }
508
509        // 2.  Open the remote folder and create the remote folder if necessary
510
511        Store remoteStore = Store.getInstance(account, context);
512        // The account might have been deleted
513        if (remoteStore == null) return;
514        Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
515
516        /*
517         * If the folder is a "special" folder we need to see if it exists
518         * on the remote server. It if does not exist we'll try to create it. If we
519         * can't create we'll abort. This will happen on every single Pop3 folder as
520         * designed and on Imap folders during error conditions. This allows us
521         * to treat Pop3 and Imap the same in this code.
522         */
523        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
524                || mailbox.mType == Mailbox.TYPE_DRAFTS) {
525            if (!remoteFolder.exists()) {
526                if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
527                    return;
528                }
529            }
530        }
531
532        // 3, Open the remote folder. This pre-loads certain metadata like message count.
533        remoteFolder.open(OpenMode.READ_WRITE);
534
535        // 4. Trash any remote messages that are marked as trashed locally.
536        // TODO - this comment was here, but no code was here.
537
538        // 5. Get the remote message count.
539        int remoteMessageCount = remoteFolder.getMessageCount();
540        ContentValues values = new ContentValues();
541        values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount);
542        mailbox.update(context, values);
543
544        // 6. Determine the limit # of messages to download
545        int visibleLimit = mailbox.mVisibleLimit;
546        if (visibleLimit <= 0) {
547            visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
548        }
549
550        // 7.  Create a list of messages to download
551        Message[] remoteMessages = new Message[0];
552        final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
553        HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
554
555        if (remoteMessageCount > 0) {
556            /*
557             * Message numbers start at 1.
558             */
559            int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
560            int remoteEnd = remoteMessageCount;
561            remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
562            // TODO Why are we running through the list twice? Combine w/ for loop below
563            for (Message message : remoteMessages) {
564                remoteUidMap.put(message.getUid(), message);
565            }
566
567            /*
568             * Get a list of the messages that are in the remote list but not on the
569             * local store, or messages that are in the local store but failed to download
570             * on the last sync. These are the new messages that we will download.
571             * Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
572             * because they are locally deleted and we don't need or want the old message from
573             * the server.
574             */
575            for (Message message : remoteMessages) {
576                LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
577                // localMessage == null -> message has never been created (not even headers)
578                // mFlagLoaded = UNLOADED -> message created, but none of body loaded
579                // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
580                // mFlagLoaded = COMPLETE -> message body has been completely loaded
581                // mFlagLoaded = DELETED -> message has been deleted
582                // Only the first two of these are "unsynced", so let's retrieve them
583                if (localMessage == null ||
584                        (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) {
585                    unsyncedMessages.add(message);
586                }
587            }
588        }
589
590        // 8.  Download basic info about the new/unloaded messages (if any)
591        /*
592         * Fetch the flags and envelope only of the new messages. This is intended to get us
593         * critical data as fast as possible, and then we'll fill in the details.
594         */
595        if (unsyncedMessages.size() > 0) {
596            downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
597                    localMessageMap, unseenMessages);
598        }
599
600        // 9. Refresh the flags for any messages in the local store that we didn't just download.
601        FetchProfile fp = new FetchProfile();
602        fp.add(FetchProfile.Item.FLAGS);
603        remoteFolder.fetch(remoteMessages, fp, null);
604        boolean remoteSupportsSeen = false;
605        boolean remoteSupportsFlagged = false;
606        boolean remoteSupportsAnswered = false;
607        for (Flag flag : remoteFolder.getPermanentFlags()) {
608            if (flag == Flag.SEEN) {
609                remoteSupportsSeen = true;
610            }
611            if (flag == Flag.FLAGGED) {
612                remoteSupportsFlagged = true;
613            }
614            if (flag == Flag.ANSWERED) {
615                remoteSupportsAnswered = true;
616            }
617        }
618        // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
619        if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
620            for (Message remoteMessage : remoteMessages) {
621                LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
622                if (localMessageInfo == null) {
623                    continue;
624                }
625                boolean localSeen = localMessageInfo.mFlagRead;
626                boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
627                boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
628                boolean localFlagged = localMessageInfo.mFlagFavorite;
629                boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
630                boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
631                int localFlags = localMessageInfo.mFlags;
632                boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
633                boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
634                boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
635                if (newSeen || newFlagged || newAnswered) {
636                    Uri uri = ContentUris.withAppendedId(
637                            EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
638                    ContentValues updateValues = new ContentValues();
639                    updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
640                    updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
641                    if (remoteAnswered) {
642                        localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
643                    } else {
644                        localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
645                    }
646                    updateValues.put(MessageColumns.FLAGS, localFlags);
647                    resolver.update(uri, updateValues, null, null);
648                }
649            }
650        }
651
652        // 10. Remove any messages that are in the local store but no longer on the remote store.
653        HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
654        localUidsToDelete.removeAll(remoteUidMap.keySet());
655        for (String uidToDelete : localUidsToDelete) {
656            LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
657
658            // Delete associated data (attachment files)
659            // Attachment & Body records are auto-deleted when we delete the Message record
660            AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
661                    infoToDelete.mId);
662
663            // Delete the message itself
664            Uri uriToDelete = ContentUris.withAppendedId(
665                    EmailContent.Message.CONTENT_URI, infoToDelete.mId);
666            resolver.delete(uriToDelete, null, null);
667
668            // Delete extra rows (e.g. synced or deleted)
669            Uri syncRowToDelete = ContentUris.withAppendedId(
670                    EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
671            resolver.delete(syncRowToDelete, null, null);
672            Uri deletERowToDelete = ContentUris.withAppendedId(
673                    EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
674            resolver.delete(deletERowToDelete, null, null);
675        }
676
677        loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
678
679        // 14. Clean up and report results
680        remoteFolder.close(false);
681    }
682
683    /**
684     * Find messages in the updated table that need to be written back to server.
685     *
686     * Handles:
687     *   Read/Unread
688     *   Flagged
689     *   Append (upload)
690     *   Move To Trash
691     *   Empty trash
692     * TODO:
693     *   Move
694     *
695     * @param account the account to scan for pending actions
696     * @throws MessagingException
697     */
698    private static void processPendingActionsSynchronous(Context context, Account account)
699           throws MessagingException {
700        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
701        String[] accountIdArgs = new String[] { Long.toString(account.mId) };
702
703        // Handle deletes first, it's always better to get rid of things first
704        processPendingDeletesSynchronous(context, account, accountIdArgs);
705    }
706
707    /**
708     * Scan for messages that are in the Message_Deletes table, look for differences that
709     * we can deal with, and do the work.
710     *
711     * @param account
712     * @param resolver
713     * @param accountIdArgs
714     */
715    private static void processPendingDeletesSynchronous(Context context, Account account,
716            String[] accountIdArgs) {
717        Cursor deletes = context.getContentResolver().query(
718                EmailContent.Message.DELETED_CONTENT_URI,
719                EmailContent.Message.CONTENT_PROJECTION,
720                EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
721                EmailContent.MessageColumns.MAILBOX_KEY);
722        try {
723            // loop through messages marked as deleted
724            while (deletes.moveToNext()) {
725                EmailContent.Message oldMessage =
726                    EmailContent.getContent(deletes, EmailContent.Message.class);
727
728                // Finally, delete the update
729                Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
730                        oldMessage.mId);
731                context.getContentResolver().delete(uri, null, null);
732            }
733        } finally {
734            deletes.close();
735        }
736    }
737}