MessagingController.java revision 2ac94a9cc254ff0a4c17407eb1bda31d433ef651
1/*
2 * Copyright (C) 2008 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;
18
19import com.android.email.mail.BodyPart;
20import com.android.email.mail.FetchProfile;
21import com.android.email.mail.Flag;
22import com.android.email.mail.Folder;
23import com.android.email.mail.Message;
24import com.android.email.mail.MessageRetrievalListener;
25import com.android.email.mail.MessagingException;
26import com.android.email.mail.Part;
27import com.android.email.mail.Sender;
28import com.android.email.mail.Store;
29import com.android.email.mail.StoreSynchronizer;
30import com.android.email.mail.Folder.FolderType;
31import com.android.email.mail.Folder.OpenMode;
32import com.android.email.mail.internet.MimeBodyPart;
33import com.android.email.mail.internet.MimeHeader;
34import com.android.email.mail.internet.MimeMultipart;
35import com.android.email.mail.internet.MimeUtility;
36import com.android.email.mail.store.LocalStore;
37import com.android.email.mail.store.LocalStore.LocalFolder;
38import com.android.email.mail.store.LocalStore.LocalMessage;
39import com.android.email.mail.store.LocalStore.PendingCommand;
40import com.android.email.provider.AttachmentProvider;
41import com.android.email.provider.EmailContent;
42import com.android.email.provider.EmailContent.Attachment;
43import com.android.email.provider.EmailContent.AttachmentColumns;
44import com.android.email.provider.EmailContent.Mailbox;
45import com.android.email.provider.EmailContent.MailboxColumns;
46import com.android.email.provider.EmailContent.MessageColumns;
47import com.android.email.provider.EmailContent.SyncColumns;
48
49import android.content.ContentResolver;
50import android.content.ContentUris;
51import android.content.ContentValues;
52import android.content.Context;
53import android.database.Cursor;
54import android.net.Uri;
55import android.os.Process;
56import android.util.Log;
57
58import java.io.File;
59import java.io.IOException;
60import java.util.ArrayList;
61import java.util.Date;
62import java.util.HashMap;
63import java.util.HashSet;
64import java.util.concurrent.BlockingQueue;
65import java.util.concurrent.LinkedBlockingQueue;
66
67/**
68 * Starts a long running (application) Thread that will run through commands
69 * that require remote mailbox access. This class is used to serialize and
70 * prioritize these commands. Each method that will submit a command requires a
71 * MessagingListener instance to be provided. It is expected that that listener
72 * has also been added as a registered listener using addListener(). When a
73 * command is to be executed, if the listener that was provided with the command
74 * is no longer registered the command is skipped. The design idea for the above
75 * is that when an Activity starts it registers as a listener. When it is paused
76 * it removes itself. Thus, any commands that that activity submitted are
77 * removed from the queue once the activity is no longer active.
78 */
79public class MessagingController implements Runnable {
80    /**
81     * The maximum message size that we'll consider to be "small". A small message is downloaded
82     * in full immediately instead of in pieces. Anything over this size will be downloaded in
83     * pieces with attachments being left off completely and downloaded on demand.
84     *
85     *
86     * 25k for a "small" message was picked by educated trial and error.
87     * http://answers.google.com/answers/threadview?id=312463 claims that the
88     * average size of an email is 59k, which I feel is too large for our
89     * blind download. The following tests were performed on a download of
90     * 25 random messages.
91     * <pre>
92     * 5k - 61 seconds,
93     * 25k - 51 seconds,
94     * 55k - 53 seconds,
95     * </pre>
96     * So 25k gives good performance and a reasonable data footprint. Sounds good to me.
97     */
98    private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
99
100    private static final String PENDING_COMMAND_TRASH =
101        "com.android.email.MessagingController.trash";
102    private static final String PENDING_COMMAND_MARK_READ =
103        "com.android.email.MessagingController.markRead";
104    private static final String PENDING_COMMAND_APPEND =
105        "com.android.email.MessagingController.append";
106
107    /**
108     * Projections & CVs used by pruneCachedAttachments
109     */
110    private static String[] PRUNE_ATTACHMENT_PROJECTION = new String[] {
111        AttachmentColumns.LOCATION
112    };
113    private static ContentValues PRUNE_ATTACHMENT_CV = new ContentValues();
114    static {
115        PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI);
116    }
117
118    private static MessagingController inst = null;
119    private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>();
120    private Thread mThread;
121
122    /**
123     * All access to mListeners *must* be synchronized
124     */
125    private GroupMessagingListener mListeners = new GroupMessagingListener();
126    private boolean mBusy;
127    private Context mContext;
128
129    protected MessagingController(Context _context) {
130        mContext = _context;
131        mThread = new Thread(this);
132        mThread.start();
133    }
134
135    /**
136     * Gets or creates the singleton instance of MessagingController. Application is used to
137     * provide a Context to classes that need it.
138     * @param application
139     * @return
140     */
141    public synchronized static MessagingController getInstance(Context _context) {
142        if (inst == null) {
143            inst = new MessagingController(_context);
144        }
145        return inst;
146    }
147
148    /**
149     * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
150     */
151    public static void injectMockController(MessagingController mockController) {
152        inst = mockController;
153    }
154
155    // TODO: seems that this reading of mBusy isn't thread-safe
156    public boolean isBusy() {
157        return mBusy;
158    }
159
160    public void run() {
161        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
162        // TODO: add an end test to this infinite loop
163        while (true) {
164            Command command;
165            try {
166                command = mCommands.take();
167            } catch (InterruptedException e) {
168                continue; //re-test the condition on the eclosing while
169            }
170            if (command.listener == null || isActiveListener(command.listener)) {
171                mBusy = true;
172                command.runnable.run();
173                mListeners.controllerCommandCompleted(mCommands.size() > 0);
174            }
175            mBusy = false;
176        }
177    }
178
179    private void put(String description, MessagingListener listener, Runnable runnable) {
180        try {
181            Command command = new Command();
182            command.listener = listener;
183            command.runnable = runnable;
184            command.description = description;
185            mCommands.add(command);
186        }
187        catch (IllegalStateException ie) {
188            throw new Error(ie);
189        }
190    }
191
192    public void addListener(MessagingListener listener) {
193        mListeners.addListener(listener);
194    }
195
196    public void removeListener(MessagingListener listener) {
197        mListeners.removeListener(listener);
198    }
199
200    private boolean isActiveListener(MessagingListener listener) {
201        return mListeners.isActiveListener(listener);
202    }
203
204    /**
205     * Lightweight class for capturing local mailboxes in an account.  Just the columns
206     * necessary for a sync.
207     */
208    private static class LocalMailboxInfo {
209        private static final int COLUMN_ID = 0;
210        private static final int COLUMN_DISPLAY_NAME = 1;
211        private static final int COLUMN_ACCOUNT_KEY = 2;
212
213        private static final String[] PROJECTION = new String[] {
214            EmailContent.RECORD_ID,
215            MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
216        };
217
218        long mId;
219        String mDisplayName;
220        long mAccountKey;
221
222        public LocalMailboxInfo(Cursor c) {
223            mId = c.getLong(COLUMN_ID);
224            mDisplayName = c.getString(COLUMN_DISPLAY_NAME);
225            mAccountKey = c.getLong(COLUMN_ACCOUNT_KEY);
226        }
227    }
228
229    /**
230     * Lists folders that are available locally and remotely. This method calls
231     * listFoldersCallback for local folders before it returns, and then for
232     * remote folders at some later point. If there are no local folders
233     * includeRemote is forced by this method. This method should be called from
234     * a Thread as it may take several seconds to list the local folders.
235     *
236     * TODO this needs to cache the remote folder list
237     * TODO break out an inner listFoldersSynchronized which could simplify checkMail
238     *
239     * @param account
240     * @param listener
241     * @throws MessagingException
242     */
243    public void listFolders(long accountId, MessagingListener listener) {
244        final EmailContent.Account account =
245                EmailContent.Account.restoreAccountWithId(mContext, accountId);
246        mListeners.listFoldersStarted(account);
247        put("listFolders", listener, new Runnable() {
248            public void run() {
249                Cursor localFolderCursor = null;
250                try {
251                    // Step 1:  Get remote folders, make a list, and add any local folders
252                    // that don't already exist.
253
254                    Store store = Store.getInstance(account.getStoreUri(mContext), mContext, null);
255
256                    Folder[] remoteFolders = store.getPersonalNamespaces();
257                    updateAccountFolderNames(account, remoteFolders);
258
259                    HashSet<String> remoteFolderNames = new HashSet<String>();
260                    for (int i = 0, count = remoteFolders.length; i < count; i++) {
261                        remoteFolderNames.add(remoteFolders[i].getName());
262                    }
263
264                    HashMap<String, LocalMailboxInfo> localFolders =
265                        new HashMap<String, LocalMailboxInfo>();
266                    HashSet<String> localFolderNames = new HashSet<String>();
267                    localFolderCursor = mContext.getContentResolver().query(
268                            EmailContent.Mailbox.CONTENT_URI,
269                            LocalMailboxInfo.PROJECTION,
270                            EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
271                            new String[] { String.valueOf(account.mId) },
272                            null);
273                    while (localFolderCursor.moveToNext()) {
274                        LocalMailboxInfo info = new LocalMailboxInfo(localFolderCursor);
275                        localFolders.put(info.mDisplayName, info);
276                        localFolderNames.add(info.mDisplayName);
277                    }
278
279                    // Short circuit the rest if the sets are the same (the usual case)
280                    if (!remoteFolderNames.equals(localFolderNames)) {
281
282                        // They are different, so we have to do some adds and drops
283
284                        // Drops first, to make things smaller rather than larger
285                        HashSet<String> localsToDrop = new HashSet<String>(localFolderNames);
286                        localsToDrop.removeAll(remoteFolderNames);
287                        for (String localNameToDrop : localsToDrop) {
288                            LocalMailboxInfo localInfo = localFolders.get(localNameToDrop);
289                            Uri uri = ContentUris.withAppendedId(
290                                    EmailContent.Mailbox.CONTENT_URI, localInfo.mId);
291                            mContext.getContentResolver().delete(uri, null, null);
292                        }
293
294                        // Now do the adds
295                        remoteFolderNames.removeAll(localFolderNames);
296                        for (String remoteNameToAdd : remoteFolderNames) {
297                            EmailContent.Mailbox box = new EmailContent.Mailbox();
298                            box.mDisplayName = remoteNameToAdd;
299                            // box.mServerId;
300                            // box.mParentServerId;
301                            box.mAccountKey = account.mId;
302                            box.mType = inferMailboxTypeFromName(account, remoteNameToAdd);
303                            // box.mDelimiter;
304                            // box.mSyncKey;
305                            // box.mSyncLookback;
306                            // box.mSyncFrequency;
307                            // box.mSyncTime;
308                            // box.mUnreadCount;
309                            box.mFlagVisible = true;
310                            // box.mFlags;
311                            box.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
312                            box.save(mContext);
313                        }
314                    }
315                    mListeners.listFoldersFinished(account);
316                } catch (Exception e) {
317                    mListeners.listFoldersFailed(account, "");
318                } finally {
319                    if (localFolderCursor != null) {
320                        localFolderCursor.close();
321                    }
322                }
323            }
324        });
325    }
326
327    /**
328     * Temporarily:  Infer mailbox type from mailbox name.  This should probably be
329     * mutated into something that the stores can provide directly, instead of the two-step
330     * where we scan and report.
331     */
332    public int inferMailboxTypeFromName(EmailContent.Account account, String mailboxName) {
333        if (mailboxName == null || mailboxName.length() == 0) {
334            return EmailContent.Mailbox.TYPE_MAIL;
335        }
336        if (mailboxName.equals(Email.INBOX)) {
337            return EmailContent.Mailbox.TYPE_INBOX;
338        }
339        if (mailboxName.equals(account.getTrashFolderName(mContext))) {
340            return EmailContent.Mailbox.TYPE_TRASH;
341        }
342        if (mailboxName.equals(account.getOutboxFolderName(mContext))) {
343            return EmailContent.Mailbox.TYPE_OUTBOX;
344        }
345        if (mailboxName.equals(account.getDraftsFolderName(mContext))) {
346            return EmailContent.Mailbox.TYPE_DRAFTS;
347        }
348        if (mailboxName.equals(account.getSentFolderName(mContext))) {
349            return EmailContent.Mailbox.TYPE_SENT;
350        }
351
352        return EmailContent.Mailbox.TYPE_MAIL;
353    }
354
355    /**
356     * Asks the store for a list of server-specific folder names and, if provided, updates
357     * the account record for future getFolder() operations.
358     *
359     * NOTE:  Inbox is not queried, because we require it to be INBOX, and outbox is not
360     * queried, because outbox is local-only.
361     *
362     * TODO: Rewrite this to use simple folder tagging and none of this account nonsense
363     */
364    /* package */ void updateAccountFolderNames(EmailContent.Account account,
365            Folder[] remoteFolders) {
366        String trash = null;
367        String sent = null;
368        String drafts = null;
369
370        for (Folder folder : remoteFolders) {
371            Folder.FolderRole role = folder.getRole();
372            if (role == Folder.FolderRole.TRASH) {
373                trash = folder.getName();
374            } else if (role == Folder.FolderRole.SENT) {
375                sent = folder.getName();
376            } else if (role == Folder.FolderRole.DRAFTS) {
377                drafts = folder.getName();
378            }
379        }
380/*
381        // Do not update when null (defaults are already in place)
382        boolean commit = false;
383        if (trash != null && !trash.equals(account.getTrashFolderName(mContext))) {
384            account.setTrashFolderName(trash);
385            commit = true;
386        }
387        if (sent != null && !sent.equals(account.getSentFolderName(mContext))) {
388            account.setSentFolderName(sent);
389            commit = true;
390        }
391        if (drafts != null && !drafts.equals(account.getDraftsFolderName(mContext))) {
392            account.setDraftsFolderName(drafts);
393            commit = true;
394        }
395        if (commit) {
396            account.saveOrUpdate(mContext);
397        }
398*/
399    }
400
401    /**
402     * List the local message store for the given folder. This work is done
403     * synchronously.
404     *
405     * @param account
406     * @param folder
407     * @param listener
408     * @throws MessagingException
409     */
410/*
411    public void listLocalMessages(final EmailContent.Account account, final String folder,
412            MessagingListener listener) {
413        synchronized (mListeners) {
414            for (MessagingListener l : mListeners) {
415                l.listLocalMessagesStarted(account, folder);
416            }
417        }
418
419        try {
420            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
421                    null);
422            Folder localFolder = localStore.getFolder(folder);
423            localFolder.open(OpenMode.READ_WRITE, null);
424            Message[] localMessages = localFolder.getMessages(null);
425            ArrayList<Message> messages = new ArrayList<Message>();
426            for (Message message : localMessages) {
427                if (!message.isSet(Flag.DELETED)) {
428                    messages.add(message);
429                }
430            }
431            synchronized (mListeners) {
432                for (MessagingListener l : mListeners) {
433                    l.listLocalMessages(account, folder, messages.toArray(new Message[0]));
434                }
435                for (MessagingListener l : mListeners) {
436                    l.listLocalMessagesFinished(account, folder);
437                }
438            }
439        }
440        catch (Exception e) {
441            synchronized (mListeners) {
442                for (MessagingListener l : mListeners) {
443                    l.listLocalMessagesFailed(account, folder, e.getMessage());
444                }
445            }
446        }
447    }
448*/
449
450    /**
451     * Start background synchronization of the specified folder.
452     * @param account
453     * @param folder
454     * @param listener
455     */
456    public void synchronizeMailbox(final EmailContent.Account account,
457            final EmailContent.Mailbox folder, MessagingListener listener) {
458        /*
459         * We don't ever sync the Outbox.
460         */
461        if (folder.mType == EmailContent.Mailbox.TYPE_OUTBOX) {
462            return;
463        }
464        mListeners.synchronizeMailboxStarted(account, folder);
465        put("synchronizeMailbox", listener, new Runnable() {
466            public void run() {
467                synchronizeMailboxSynchronous(account, folder);
468            }
469        });
470    }
471
472    /**
473     * Start foreground synchronization of the specified folder. This is called by
474     * synchronizeMailbox or checkMail.
475     * TODO this should use ID's instead of fully-restored objects
476     * @param account
477     * @param folder
478     * @param listener
479     */
480    private void synchronizeMailboxSynchronous(final EmailContent.Account account,
481            final EmailContent.Mailbox folder) {
482        mListeners.synchronizeMailboxStarted(account, folder);
483        try {
484            processPendingCommandsSynchronous(account);
485
486            StoreSynchronizer.SyncResults results;
487
488            // Select generic sync or store-specific sync
489            final LocalStore localStore =
490                (LocalStore) Store.getInstance(account.getLocalStoreUri(mContext), mContext, null);
491            Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
492                    localStore.getPersistentCallbacks());
493            StoreSynchronizer customSync = remoteStore.getMessageSynchronizer();
494            if (customSync == null) {
495                results = synchronizeMailboxGeneric(account, folder);
496            } else {
497                results = customSync.SynchronizeMessagesSynchronous(
498                        account, folder, mListeners, mContext);
499            }
500            mListeners.synchronizeMailboxFinished(account,
501                                                  folder,
502                                                  results.mTotalMessages,
503                                                  results.mNewMessages);
504        } catch (MessagingException e) {
505            if (Email.LOGD) {
506                Log.v(Email.LOG_TAG, "synchronizeMailbox", e);
507            }
508            mListeners.synchronizeMailboxFailed(account, folder, e);
509        }
510    }
511
512    /**
513     * Lightweight record for the first pass of message sync, where I'm just seeing if
514     * the local message requires sync.  Later (for messages that need syncing) we'll do a full
515     * readout from the DB.
516     */
517    private static class LocalMessageInfo {
518        private static final int COLUMN_ID = 0;
519        private static final int COLUMN_FLAG_READ = 1;
520        private static final int COLUMN_FLAG_FAVORITE = 2;
521        private static final int COLUMN_FLAG_LOADED = 3;
522        private static final int COLUMN_SERVER_ID = 4;
523        private static final String[] PROJECTION = new String[] {
524            EmailContent.RECORD_ID,
525            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
526            SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY
527        };
528
529        int mCursorIndex;
530        long mId;
531        boolean mFlagRead;
532        boolean mFlagFavorite;
533        int mFlagLoaded;
534        String mServerId;
535
536        public LocalMessageInfo(Cursor c) {
537            mCursorIndex = c.getPosition();
538            mId = c.getLong(COLUMN_ID);
539            mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
540            mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
541            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
542            mServerId = c.getString(COLUMN_SERVER_ID);
543            // Note: mailbox key and account key not needed - they are projected for the SELECT
544        }
545    }
546
547    private void saveOrUpdate(EmailContent content) {
548        if (content.isSaved()) {
549            content.update(mContext, content.toContentValues());
550        } else {
551            content.save(mContext);
552        }
553    }
554
555    /**
556     * Generic synchronizer - used for POP3 and IMAP.
557     *
558     * TODO Break this method up into smaller chunks.
559     *
560     * @param account the account to sync
561     * @param folder the mailbox to sync
562     * @return results of the sync pass
563     * @throws MessagingException
564     */
565    private StoreSynchronizer.SyncResults synchronizeMailboxGeneric(
566            final EmailContent.Account account, final EmailContent.Mailbox folder)
567            throws MessagingException {
568
569        Log.d(Email.LOG_TAG, "*** synchronizeMailboxGeneric ***");
570
571        // 1.  Get the message list from the local store and create an index of the uids
572
573        Cursor localUidCursor = null;
574        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
575
576        try {
577            localUidCursor = mContext.getContentResolver().query(
578                    EmailContent.Message.CONTENT_URI,
579                    LocalMessageInfo.PROJECTION,
580                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
581                    " AND " + MessageColumns.MAILBOX_KEY + "=?",
582                    new String[] {
583                            String.valueOf(account.mId),
584                            String.valueOf(folder.mId)
585                    },
586                    null);
587            while (localUidCursor.moveToNext()) {
588                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
589                localMessageMap.put(info.mServerId, info);
590            }
591        } finally {
592            if (localUidCursor != null) {
593                localUidCursor.close();
594            }
595        }
596
597        // 1a. Count the unread messages before changing anything
598        int localUnreadCount = EmailContent.count(mContext, EmailContent.Message.CONTENT_URI,
599                EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
600                " AND " + MessageColumns.MAILBOX_KEY + "=?" +
601                " AND " + MessageColumns.FLAG_READ + "=0",
602                new String[] {
603                        String.valueOf(account.mId),
604                        String.valueOf(folder.mId)
605                });
606
607        // 2.  Open the remote folder and create the remote folder if necessary
608
609        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, null);
610        Folder remoteFolder = remoteStore.getFolder(folder.mDisplayName);
611
612        /*
613         * If the folder is a "special" folder we need to see if it exists
614         * on the remote server. It if does not exist we'll try to create it. If we
615         * can't create we'll abort. This will happen on every single Pop3 folder as
616         * designed and on Imap folders during error conditions. This allows us
617         * to treat Pop3 and Imap the same in this code.
618         */
619        if (folder.equals(account.getTrashFolderName(mContext)) ||
620                folder.equals(account.getSentFolderName(mContext)) ||
621                folder.equals(account.getDraftsFolderName(mContext))) {
622            if (!remoteFolder.exists()) {
623                if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
624                    return new StoreSynchronizer.SyncResults(0, 0);
625                }
626            }
627        }
628
629        // 3, Open the remote folder. This pre-loads certain metadata like message count.
630        remoteFolder.open(OpenMode.READ_WRITE, null);
631
632        // 4. Trash any remote messages that are marked as trashed locally.
633        // TODO - this comment was here, but no code was here.
634
635        // 5. Get the remote message count.
636        int remoteMessageCount = remoteFolder.getMessageCount();
637
638        // 6. Determine the limit # of messages to download
639        int visibleLimit = folder.mVisibleLimit;
640        if (visibleLimit <= 0) {
641            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext),
642                    mContext);
643            visibleLimit = info.mVisibleLimitDefault;
644        }
645
646        // 7.  Create a list of messages to download
647        Message[] remoteMessages = new Message[0];
648        final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
649        HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
650
651        int newMessageCount = 0;
652        if (remoteMessageCount > 0) {
653            /*
654             * Message numbers start at 1.
655             */
656            int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
657            int remoteEnd = remoteMessageCount;
658            remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
659            for (Message message : remoteMessages) {
660                remoteUidMap.put(message.getUid(), message);
661            }
662
663            /*
664             * Get a list of the messages that are in the remote list but not on the
665             * local store, or messages that are in the local store but failed to download
666             * on the last sync. These are the new messages that we will download.
667             */
668            for (Message message : remoteMessages) {
669                LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
670                if (localMessage == null) {
671                    newMessageCount++;
672                }
673                if (localMessage == null ||
674                        localMessage.mFlagLoaded != EmailContent.Message.LOADED) {
675                    unsyncedMessages.add(message);
676                }
677            }
678        }
679
680        // 8.  Download basic info about the new/unloaded messages (if any)
681        /*
682         * A list of messages that were downloaded and which did not have the Seen flag set.
683         * This will serve to indicate the true "new" message count that will be reported to
684         * the user via notification.
685         */
686        final ArrayList<Message> newMessages = new ArrayList<Message>();
687
688        /*
689         * Fetch the flags and envelope only of the new messages. This is intended to get us
690         * critical data as fast as possible, and then we'll fill in the details.
691         */
692        if (unsyncedMessages.size() > 0) {
693            FetchProfile fp = new FetchProfile();
694            fp.add(FetchProfile.Item.FLAGS);
695            fp.add(FetchProfile.Item.ENVELOPE);
696            final HashMap<String, LocalMessageInfo> localMapCopy =
697                new HashMap<String, LocalMessageInfo>(localMessageMap);
698
699            remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
700                    new MessageRetrievalListener() {
701                        public void messageFinished(Message message, int number, int ofTotal) {
702                            try {
703                                // Determine if the new message was already known (e.g. partial)
704                                // And create or reload the full message info
705                                LocalMessageInfo localMessageInfo =
706                                    localMapCopy.get(message.getUid());
707                                EmailContent.Message localMessage = null;
708                                if (localMessageInfo == null) {
709                                    localMessage = new EmailContent.Message();
710                                } else {
711                                    localMessage = EmailContent.Message.restoreMessageWithId(
712                                            mContext, localMessageInfo.mId);
713                                }
714
715                                if (localMessage != null) {
716                                    try {
717                                        // Copy the fields that are available into the message
718                                        LegacyConversions.updateMessageFields(localMessage,
719                                                message, account.mId, folder.mId);
720                                        // Commit the message to the local store
721                                        saveOrUpdate(localMessage);
722                                        // Track the "new" ness of the downloaded message
723                                        if (!message.isSet(Flag.SEEN)) {
724                                            newMessages.add(message);
725                                        }
726                                    } catch (MessagingException me) {
727                                        Log.e(Email.LOG_TAG,
728                                                "Error while copying downloaded message." + me);
729                                    }
730
731                                }
732                            }
733                            catch (Exception e) {
734                                Log.e(Email.LOG_TAG,
735                                        "Error while storing downloaded message." + e.toString());
736                            }
737                        }
738
739                        public void messageStarted(String uid, int number, int ofTotal) {
740                        }
741                    });
742        }
743
744        // 9. Refresh the flags for any messages in the local store that we didn't just download.
745        FetchProfile fp = new FetchProfile();
746        fp.add(FetchProfile.Item.FLAGS);
747        remoteFolder.fetch(remoteMessages, fp, null);
748        boolean remoteSupportsSeen = false;
749        boolean remoteSupportsFlagged = false;
750        for (Flag flag : remoteFolder.getPermanentFlags()) {
751            if (flag == Flag.SEEN) {
752                remoteSupportsSeen = true;
753            }
754            if (flag == Flag.FLAGGED) {
755                remoteSupportsFlagged = true;
756            }
757        }
758        // Update the SEEN & FLAGGED (star) flags (if supported remotely - e.g. not for POP3)
759        if (remoteSupportsSeen || remoteSupportsFlagged) {
760            for (Message remoteMessage : remoteMessages) {
761                LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
762                if (localMessageInfo == null) {
763                    continue;
764                }
765                boolean localSeen = localMessageInfo.mFlagRead;
766                boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
767                boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
768                boolean localFlagged = localMessageInfo.mFlagFavorite;
769                boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
770                boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
771                if (newSeen || newFlagged) {
772                    Uri uri = ContentUris.withAppendedId(
773                            EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
774                    ContentValues updateValues = new ContentValues();
775                    updateValues.put(EmailContent.Message.FLAG_READ, remoteSeen);
776                    updateValues.put(EmailContent.Message.FLAG_FAVORITE, remoteFlagged);
777                    mContext.getContentResolver().update(uri, updateValues, null, null);
778                }
779            }
780        }
781
782        // 10. Compute and store the unread message count.
783        // -- no longer necessary - Provider uses DB triggers to keep track
784
785//        int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount();
786//        if (remoteUnreadMessageCount == -1) {
787//            if (remoteSupportsSeenFlag) {
788//                /*
789//                 * If remote folder doesn't supported unread message count but supports
790//                 * seen flag, use local folder's unread message count and the size of
791//                 * new messages. This mode is not used for POP3, or IMAP.
792//                 */
793//
794//                remoteUnreadMessageCount = folder.mUnreadCount + newMessages.size();
795//            } else {
796//                /*
797//                 * If remote folder doesn't supported unread message count and doesn't
798//                 * support seen flag, use localUnreadCount and newMessageCount which
799//                 * don't rely on remote SEEN flag.  This mode is used by POP3.
800//                 */
801//                remoteUnreadMessageCount = localUnreadCount + newMessageCount;
802//            }
803//        } else {
804//            /*
805//             * If remote folder supports unread message count, use remoteUnreadMessageCount.
806//             * This mode is used by IMAP.
807//             */
808//         }
809//        Uri uri = ContentUris.withAppendedId(EmailContent.Mailbox.CONTENT_URI, folder.mId);
810//        ContentValues updateValues = new ContentValues();
811//        updateValues.put(EmailContent.Mailbox.UNREAD_COUNT, remoteUnreadMessageCount);
812//        mContext.getContentResolver().update(uri, updateValues, null, null);
813
814        // 11. Remove any messages that are in the local store but no longer on the remote store.
815
816        HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
817        localUidsToDelete.removeAll(remoteUidMap.keySet());
818        for (String uidToDelete : localUidsToDelete) {
819            LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
820
821            Uri uriToDelete = ContentUris.withAppendedId(
822                    EmailContent.Message.CONTENT_URI, infoToDelete.mId);
823            mContext.getContentResolver().delete(uriToDelete, null, null);
824        }
825
826        // 12. Divide the unsynced messages into small & large (by size)
827
828        // TODO doing this work here (synchronously) is problematic because it prevents the UI
829        // from affecting the order (e.g. download a message because the user requested it.)  Much
830        // of this logic should move out to a different sync loop that attempts to update small
831        // groups of messages at a time, as a background task.  However, we can't just return
832        // (yet) because POP messages don't have an envelope yet....
833
834        ArrayList<Message> largeMessages = new ArrayList<Message>();
835        ArrayList<Message> smallMessages = new ArrayList<Message>();
836        for (Message message : unsyncedMessages) {
837            if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
838                largeMessages.add(message);
839            } else {
840                smallMessages.add(message);
841            }
842        }
843
844        // 13. Download small messages
845
846        // TODO Problems with this implementation.  1. For IMAP, where we get a real envelope,
847        // this is going to be inefficient and duplicate work we've already done.  2.  It's going
848        // back to the DB for a local message that we already had (and discarded).
849
850        // For small messages, we specify "body", which returns everything (incl. attachments)
851        fp = new FetchProfile();
852        fp.add(FetchProfile.Item.BODY);
853        remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
854                new MessageRetrievalListener() {
855                    public void messageFinished(Message message, int number, int ofTotal) {
856                        // Store the updated message locally and mark it fully loaded
857                        copyOneMessageToProvider(message, account, folder,
858                                EmailContent.Message.LOADED);
859                    }
860
861                    public void messageStarted(String uid, int number, int ofTotal) {
862                    }
863        });
864
865        // 14. Download large messages.  We ask the server to give us the message structure,
866        // but not all of the attachments.
867        fp.clear();
868        fp.add(FetchProfile.Item.STRUCTURE);
869        remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
870        for (Message message : largeMessages) {
871            if (message.getBody() == null) {
872                // POP doesn't support STRUCTURE mode, so we'll just do a partial download
873                // (hopefully enough to see some/all of the body) and mark the message for
874                // further download.
875                fp.clear();
876                fp.add(FetchProfile.Item.BODY_SANE);
877                //  TODO a good optimization here would be to make sure that all Stores set
878                //  the proper size after this fetch and compare the before and after size. If
879                //  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
880                remoteFolder.fetch(new Message[] { message }, fp, null);
881
882                // Store the partially-loaded message and mark it partially loaded
883                copyOneMessageToProvider(message, account, folder,
884                        EmailContent.Message.PARTIALLY_LOADED);
885            } else {
886                // We have a structure to deal with, from which
887                // we can pull down the parts we want to actually store.
888                // Build a list of parts we are interested in. Text parts will be downloaded
889                // right now, attachments will be left for later.
890                ArrayList<Part> viewables = new ArrayList<Part>();
891                ArrayList<Part> attachments = new ArrayList<Part>();
892                MimeUtility.collectParts(message, viewables, attachments);
893                // Download the viewables immediately
894                for (Part part : viewables) {
895                    fp.clear();
896                    fp.add(part);
897                    // TODO what happens if the network connection dies? We've got partial
898                    // messages with incorrect status stored.
899                    remoteFolder.fetch(new Message[] { message }, fp, null);
900                }
901                // Store the updated message locally and mark it fully loaded
902                copyOneMessageToProvider(message, account, folder, EmailContent.Message.LOADED);
903            }
904        }
905
906        // 15. Clean up and report results
907
908        remoteFolder.close(false);
909        // TODO - more
910
911        // Original sync code.  Using for reference, will delete when done.
912        if (false) {
913        /*
914         * Now do the large messages that require more round trips.
915         */
916        fp.clear();
917        fp.add(FetchProfile.Item.STRUCTURE);
918        remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]),
919                fp, null);
920        for (Message message : largeMessages) {
921            if (message.getBody() == null) {
922                /*
923                 * The provider was unable to get the structure of the message, so
924                 * we'll download a reasonable portion of the messge and mark it as
925                 * incomplete so the entire thing can be downloaded later if the user
926                 * wishes to download it.
927                 */
928                fp.clear();
929                fp.add(FetchProfile.Item.BODY_SANE);
930                /*
931                 *  TODO a good optimization here would be to make sure that all Stores set
932                 *  the proper size after this fetch and compare the before and after size. If
933                 *  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
934                 */
935
936                remoteFolder.fetch(new Message[] { message }, fp, null);
937                // Store the updated message locally
938//                localFolder.appendMessages(new Message[] {
939//                    message
940//                });
941
942//                Message localMessage = localFolder.getMessage(message.getUid());
943
944                // Set a flag indicating that the message has been partially downloaded and
945                // is ready for view.
946//                localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
947            } else {
948                /*
949                 * We have a structure to deal with, from which
950                 * we can pull down the parts we want to actually store.
951                 * Build a list of parts we are interested in. Text parts will be downloaded
952                 * right now, attachments will be left for later.
953                 */
954
955                ArrayList<Part> viewables = new ArrayList<Part>();
956                ArrayList<Part> attachments = new ArrayList<Part>();
957                MimeUtility.collectParts(message, viewables, attachments);
958
959                /*
960                 * Now download the parts we're interested in storing.
961                 */
962                for (Part part : viewables) {
963                    fp.clear();
964                    fp.add(part);
965                    // TODO what happens if the network connection dies? We've got partial
966                    // messages with incorrect status stored.
967                    remoteFolder.fetch(new Message[] { message }, fp, null);
968                }
969                // Store the updated message locally
970//                localFolder.appendMessages(new Message[] {
971//                    message
972//                });
973
974//                Message localMessage = localFolder.getMessage(message.getUid());
975
976                // Set a flag indicating this message has been fully downloaded and can be
977                // viewed.
978//                localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
979            }
980
981            // Update the listener with what we've found
982//            synchronized (mListeners) {
983//                for (MessagingListener l : mListeners) {
984//                    l.synchronizeMailboxNewMessage(
985//                            account,
986//                            folder,
987//                            localFolder.getMessage(message.getUid()));
988//                }
989//            }
990        }
991
992
993        /*
994         * Report successful sync
995         */
996        StoreSynchronizer.SyncResults results = new StoreSynchronizer.SyncResults(
997                remoteFolder.getMessageCount(), newMessages.size());
998
999        remoteFolder.close(false);
1000//        localFolder.close(false);
1001
1002        return results;
1003        }
1004
1005        return new StoreSynchronizer.SyncResults(remoteMessageCount, newMessages.size());
1006    }
1007
1008    /**
1009     * Copy one downloaded message (which may have partially-loaded sections)
1010     * into a provider message
1011     *
1012     * @param message the remote message we've just downloaded
1013     * @param account the account it will be stored into
1014     * @param folder the mailbox it will be stored into
1015     * @param loadStatus when complete, the message will be marked with this status (e.g.
1016     *        EmailContent.Message.LOADED)
1017     */
1018    private void copyOneMessageToProvider(Message message, EmailContent.Account account,
1019            EmailContent.Mailbox folder, int loadStatus) {
1020        try {
1021            EmailContent.Message localMessage = null;
1022            Cursor c = null;
1023            try {
1024                c = mContext.getContentResolver().query(
1025                        EmailContent.Message.CONTENT_URI,
1026                        EmailContent.Message.CONTENT_PROJECTION,
1027                        EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
1028                        " AND " + MessageColumns.MAILBOX_KEY + "=?" +
1029                        " AND " + SyncColumns.SERVER_ID + "=?",
1030                        new String[] {
1031                                String.valueOf(account.mId),
1032                                String.valueOf(folder.mId),
1033                                String.valueOf(message.getUid())
1034                        },
1035                        null);
1036                if (c.moveToNext()) {
1037                    localMessage = EmailContent.getContent(c, EmailContent.Message.class);
1038                }
1039            } finally {
1040                if (c != null) {
1041                    c.close();
1042                }
1043            }
1044            if (localMessage == null) {
1045                Log.d(Email.LOG_TAG, "Could not retrieve message from db, UUID="
1046                        + message.getUid());
1047                return;
1048            }
1049
1050            EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(mContext,
1051                    localMessage.mId);
1052            if (body == null) {
1053                body = new EmailContent.Body();
1054            }
1055            try {
1056                // Copy the fields that are available into the message object
1057                LegacyConversions.updateMessageFields(localMessage, message, account.mId,
1058                        folder.mId);
1059
1060                // Now process body parts & attachments
1061                ArrayList<Part> viewables = new ArrayList<Part>();
1062                ArrayList<Part> attachments = new ArrayList<Part>();
1063                MimeUtility.collectParts(message, viewables, attachments);
1064
1065                LegacyConversions.updateBodyFields(body, localMessage, viewables);
1066
1067                // Commit the message & body to the local store immediately
1068                saveOrUpdate(localMessage);
1069                saveOrUpdate(body);
1070
1071                // process (and save) attachments
1072                LegacyConversions.updateAttachments(mContext, localMessage,
1073                        attachments);
1074
1075                // One last update of message with two updated flags
1076                localMessage.mFlagLoaded = loadStatus;
1077
1078                ContentValues cv = new ContentValues();
1079                cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
1080                cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
1081                Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
1082                        localMessage.mId);
1083                mContext.getContentResolver().update(uri, cv, null, null);
1084
1085            } catch (MessagingException me) {
1086                Log.e(Email.LOG_TAG, "Error while copying downloaded message." + me);
1087            }
1088
1089        } catch (RuntimeException rte) {
1090            Log.e(Email.LOG_TAG, "Error while storing downloaded message." + rte.toString());
1091        } catch (IOException ioe) {
1092            Log.e(Email.LOG_TAG, "Error while storing attachment." + ioe.toString());
1093        }
1094    }
1095
1096    private void queuePendingCommand(EmailContent.Account account, PendingCommand command) {
1097        try {
1098            LocalStore localStore = (LocalStore) Store.getInstance(
1099                    account.getLocalStoreUri(mContext), mContext, null);
1100            localStore.addPendingCommand(command);
1101        }
1102        catch (Exception e) {
1103            throw new RuntimeException("Unable to enqueue pending command", e);
1104        }
1105    }
1106
1107    private void processPendingCommands(final EmailContent.Account account) {
1108        put("processPendingCommands", null, new Runnable() {
1109            public void run() {
1110                try {
1111                    processPendingCommandsSynchronous(account);
1112                }
1113                catch (MessagingException me) {
1114                    if (Email.LOGD) {
1115                        Log.v(Email.LOG_TAG, "processPendingCommands", me);
1116                    }
1117                    /*
1118                     * Ignore any exceptions from the commands. Commands will be processed
1119                     * on the next round.
1120                     */
1121                }
1122            }
1123        });
1124    }
1125
1126    private void processPendingCommandsSynchronous(EmailContent.Account account)
1127            throws MessagingException {
1128        LocalStore localStore = (LocalStore) Store.getInstance(
1129                account.getLocalStoreUri(mContext), mContext, null);
1130        ArrayList<PendingCommand> commands = localStore.getPendingCommands();
1131        for (PendingCommand command : commands) {
1132            /*
1133             * We specifically do not catch any exceptions here. If a command fails it is
1134             * most likely due to a server or IO error and it must be retried before any
1135             * other command processes. This maintains the order of the commands.
1136             */
1137            if (PENDING_COMMAND_APPEND.equals(command.command)) {
1138                processPendingAppend(command, account);
1139            }
1140            else if (PENDING_COMMAND_MARK_READ.equals(command.command)) {
1141                processPendingMarkRead(command, account);
1142            }
1143            else if (PENDING_COMMAND_TRASH.equals(command.command)) {
1144                processPendingTrash(command, account);
1145            }
1146            localStore.removePendingCommand(command);
1147        }
1148    }
1149
1150    /**
1151     * Process a pending append message command. This command uploads a local message to the
1152     * server, first checking to be sure that the server message is not newer than
1153     * the local message. Once the local message is successfully processed it is deleted so
1154     * that the server message will be synchronized down without an additional copy being
1155     * created.
1156     * TODO update the local message UID instead of deleteing it
1157     *
1158     * @param command arguments = (String folder, String uid)
1159     * @param account
1160     * @throws MessagingException
1161     */
1162    private void processPendingAppend(PendingCommand command, EmailContent.Account account)
1163            throws MessagingException {
1164        String folder = command.arguments[0];
1165        String uid = command.arguments[1];
1166
1167        LocalStore localStore = (LocalStore) Store.getInstance(
1168                account.getLocalStoreUri(mContext), mContext, null);
1169        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1170        LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid);
1171
1172        if (localMessage == null) {
1173            return;
1174        }
1175
1176        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1177                localStore.getPersistentCallbacks());
1178        Folder remoteFolder = remoteStore.getFolder(folder);
1179        if (!remoteFolder.exists()) {
1180            if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
1181                return;
1182            }
1183        }
1184        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1185        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1186            return;
1187        }
1188
1189        Message remoteMessage = null;
1190        if (!localMessage.getUid().startsWith("Local")
1191                && !localMessage.getUid().contains("-")) {
1192            remoteMessage = remoteFolder.getMessage(localMessage.getUid());
1193        }
1194
1195        if (remoteMessage == null) {
1196            /*
1197             * If the message does not exist remotely we just upload it and then
1198             * update our local copy with the new uid.
1199             */
1200            FetchProfile fp = new FetchProfile();
1201            fp.add(FetchProfile.Item.BODY);
1202            localFolder.fetch(new Message[] { localMessage }, fp, null);
1203            String oldUid = localMessage.getUid();
1204            remoteFolder.appendMessages(new Message[] { localMessage });
1205            localFolder.changeUid(localMessage);
1206            mListeners.messageUidChanged(account, folder, oldUid, localMessage.getUid());
1207        }
1208        else {
1209            /*
1210             * If the remote message exists we need to determine which copy to keep.
1211             */
1212            /*
1213             * See if the remote message is newer than ours.
1214             */
1215            FetchProfile fp = new FetchProfile();
1216            fp.add(FetchProfile.Item.ENVELOPE);
1217            remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1218            Date localDate = localMessage.getInternalDate();
1219            Date remoteDate = remoteMessage.getInternalDate();
1220            if (remoteDate.compareTo(localDate) > 0) {
1221                /*
1222                 * If the remote message is newer than ours we'll just
1223                 * delete ours and move on. A sync will get the server message
1224                 * if we need to be able to see it.
1225                 */
1226                localMessage.setFlag(Flag.DELETED, true);
1227            }
1228            else {
1229                /*
1230                 * Otherwise we'll upload our message and then delete the remote message.
1231                 */
1232                fp.clear();
1233                fp = new FetchProfile();
1234                fp.add(FetchProfile.Item.BODY);
1235                localFolder.fetch(new Message[] { localMessage }, fp, null);
1236                String oldUid = localMessage.getUid();
1237                remoteFolder.appendMessages(new Message[] { localMessage });
1238                localFolder.changeUid(localMessage);
1239                mListeners.messageUidChanged(account, folder, oldUid, localMessage.getUid());
1240                remoteMessage.setFlag(Flag.DELETED, true);
1241            }
1242        }
1243    }
1244
1245    /**
1246     * Process a pending trash message command.
1247     *
1248     * @param command arguments = (String folder, String uid)
1249     * @param account
1250     * @throws MessagingException
1251     */
1252    private void processPendingTrash(PendingCommand command, final EmailContent.Account account)
1253            throws MessagingException {
1254        String folder = command.arguments[0];
1255        String uid = command.arguments[1];
1256
1257        final LocalStore localStore = (LocalStore) Store.getInstance(
1258                account.getLocalStoreUri(mContext), mContext, null);
1259        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1260
1261        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1262                localStore.getPersistentCallbacks());
1263        Folder remoteFolder = remoteStore.getFolder(folder);
1264        if (!remoteFolder.exists()) {
1265            return;
1266        }
1267        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1268        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1269            remoteFolder.close(false);
1270            return;
1271        }
1272
1273        Message remoteMessage = null;
1274        if (!uid.startsWith("Local")) {
1275            remoteMessage = remoteFolder.getMessage(uid);
1276        }
1277        if (remoteMessage == null) {
1278            remoteFolder.close(false);
1279            return;
1280        }
1281
1282        Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName(mContext));
1283        /*
1284         * Attempt to copy the remote message to the remote trash folder.
1285         */
1286        if (!remoteTrashFolder.exists()) {
1287            /*
1288             * If the remote trash folder doesn't exist we try to create it.
1289             */
1290            remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
1291        }
1292
1293        if (remoteTrashFolder.exists()) {
1294            /*
1295             * Because remoteTrashFolder may be new, we need to explicitly open it
1296             * and pass in the persistence callbacks.
1297             */
1298            final LocalFolder localTrashFolder =
1299                (LocalFolder) localStore.getFolder(account.getTrashFolderName(mContext));
1300            remoteTrashFolder.open(OpenMode.READ_WRITE, localTrashFolder.getPersistentCallbacks());
1301            if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1302                remoteFolder.close(false);
1303                remoteTrashFolder.close(false);
1304                return;
1305            }
1306
1307            remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
1308                    new Folder.MessageUpdateCallbacks() {
1309                public void onMessageUidChange(Message message, String newUid)
1310                        throws MessagingException {
1311                    // update the UID in the local trash folder, because some stores will
1312                    // have to change it when copying to remoteTrashFolder
1313                    LocalMessage localMessage =
1314                        (LocalMessage) localTrashFolder.getMessage(message.getUid());
1315                    if(localMessage != null) {
1316                        localMessage.setUid(newUid);
1317                        localTrashFolder.updateMessage(localMessage);
1318                    }
1319                }
1320
1321                /**
1322                 * This will be called if the deleted message doesn't exist and can't be
1323                 * deleted (e.g. it was already deleted from the server.)  In this case,
1324                 * attempt to delete the local copy as well.
1325                 */
1326                public void onMessageNotFound(Message message) throws MessagingException {
1327                    LocalMessage localMessage =
1328                        (LocalMessage) localTrashFolder.getMessage(message.getUid());
1329                    if (localMessage != null) {
1330                        localMessage.setFlag(Flag.DELETED, true);
1331                    }
1332                }
1333
1334            }
1335            );
1336            remoteTrashFolder.close(false);
1337        }
1338
1339        remoteMessage.setFlag(Flag.DELETED, true);
1340        remoteFolder.expunge();
1341        remoteFolder.close(false);
1342    }
1343
1344    /**
1345     * Processes a pending mark read or unread command.
1346     *
1347     * @param command arguments = (String folder, String uid, boolean read)
1348     * @param account
1349     */
1350    private void processPendingMarkRead(PendingCommand command, EmailContent.Account account)
1351            throws MessagingException {
1352        String folder = command.arguments[0];
1353        String uid = command.arguments[1];
1354        boolean read = Boolean.parseBoolean(command.arguments[2]);
1355
1356        LocalStore localStore = (LocalStore) Store.getInstance(
1357                account.getLocalStoreUri(mContext), mContext, null);
1358        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1359
1360        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1361                localStore.getPersistentCallbacks());
1362        Folder remoteFolder = remoteStore.getFolder(folder);
1363        if (!remoteFolder.exists()) {
1364            return;
1365        }
1366        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1367        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1368            return;
1369        }
1370        Message remoteMessage = null;
1371        if (!uid.startsWith("Local")
1372                && !uid.contains("-")) {
1373            remoteMessage = remoteFolder.getMessage(uid);
1374        }
1375        if (remoteMessage == null) {
1376            return;
1377        }
1378        remoteMessage.setFlag(Flag.SEEN, read);
1379    }
1380
1381    /**
1382     * Mark the message with the given account, folder and uid either Seen or not Seen.
1383     * @param account
1384     * @param folder
1385     * @param uid
1386     * @param seen
1387     */
1388    public void markMessageRead(
1389            final EmailContent.Account account,
1390            final String folder,
1391            final String uid,
1392            final boolean seen) {
1393        try {
1394            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1395                    null);
1396            Folder localFolder = localStore.getFolder(folder);
1397            localFolder.open(OpenMode.READ_WRITE, null);
1398
1399            Message message = localFolder.getMessage(uid);
1400            message.setFlag(Flag.SEEN, seen);
1401            PendingCommand command = new PendingCommand();
1402            command.command = PENDING_COMMAND_MARK_READ;
1403            command.arguments = new String[] { folder, uid, Boolean.toString(seen) };
1404            queuePendingCommand(account, command);
1405            processPendingCommands(account);
1406        }
1407        catch (MessagingException me) {
1408            throw new RuntimeException(me);
1409        }
1410    }
1411
1412    /**
1413     * Finiah loading a message that have been partially downloaded.
1414     *
1415     * @param messageId the message to load
1416     * @param listener the callback by which results will be reported
1417     */
1418    public void loadMessageForView(final long messageId, MessagingListener listener) {
1419        mListeners.loadMessageForViewStarted(messageId);
1420        put("loadMessageForViewRemote", listener, new Runnable() {
1421            public void run() {
1422                try {
1423                    // 1. Resample the message, in case it disappeared or synced while
1424                    // this command was in queue
1425                    EmailContent.Message message =
1426                        EmailContent.Message.restoreMessageWithId(mContext, messageId);
1427                    if (message == null) {
1428                        mListeners.loadMessageForViewFailed(messageId, "Unknown message");
1429                        return;
1430                    }
1431                    if (message.mFlagLoaded == EmailContent.Message.LOADED) {
1432                        mListeners.loadMessageForViewFinished(messageId);
1433                        return;
1434                    }
1435
1436                    // 2. Open the remote folder.
1437                    // TODO all of these could be narrower projections
1438                    // TODO combine with common code in loadAttachment
1439                    EmailContent.Account account =
1440                        EmailContent.Account.restoreAccountWithId(mContext, message.mAccountKey);
1441                    EmailContent.Mailbox mailbox =
1442                        EmailContent.Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
1443
1444                    Store remoteStore =
1445                        Store.getInstance(account.getStoreUri(mContext), mContext, null);
1446                    Folder remoteFolder = remoteStore.getFolder(mailbox.mDisplayName);
1447                    remoteFolder.open(OpenMode.READ_WRITE, null);
1448
1449                    // 3. Not supported, because IMAP & POP don't use it: structure prefetch
1450//                  if (remoteStore.requireStructurePrefetch()) {
1451//                  // For remote stores that require it, prefetch the message structure.
1452//                  FetchProfile fp = new FetchProfile();
1453//                  fp.add(FetchProfile.Item.STRUCTURE);
1454//                  localFolder.fetch(new Message[] { message }, fp, null);
1455//
1456//                  ArrayList<Part> viewables = new ArrayList<Part>();
1457//                  ArrayList<Part> attachments = new ArrayList<Part>();
1458//                  MimeUtility.collectParts(message, viewables, attachments);
1459//                  fp.clear();
1460//                  for (Part part : viewables) {
1461//                      fp.add(part);
1462//                  }
1463//
1464//                  remoteFolder.fetch(new Message[] { message }, fp, null);
1465//
1466//                  // Store the updated message locally
1467//                  localFolder.updateMessage((LocalMessage)message);
1468
1469                    // 4. Set up to download the entire message
1470                    Message remoteMessage = remoteFolder.getMessage(message.mServerId);
1471                    FetchProfile fp = new FetchProfile();
1472                    fp.add(FetchProfile.Item.BODY);
1473                    remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1474
1475                    // 5. Write to provider
1476                    copyOneMessageToProvider(remoteMessage, account, mailbox,
1477                            EmailContent.Message.LOADED);
1478
1479                    // 6. Notify UI
1480                    mListeners.loadMessageForViewFinished(messageId);
1481
1482                } catch (MessagingException me) {
1483                    if (Email.LOGD) Log.v(Email.LOG_TAG, "", me);
1484                    mListeners.loadMessageForViewFailed(messageId, me.getMessage());
1485                } catch (RuntimeException rte) {
1486                    mListeners.loadMessageForViewFailed(messageId, rte.getMessage());
1487                }
1488            }
1489        });
1490    }
1491
1492    /**
1493     * Attempts to load the attachment specified by id from the given account and message.
1494     * @param account
1495     * @param message
1496     * @param part
1497     * @param listener
1498     */
1499    public void loadAttachment(final long accountId, final long messageId, final long mailboxId,
1500            final long attachmentId, MessagingListener listener) {
1501        mListeners.loadAttachmentStarted(accountId, messageId, attachmentId, true);
1502
1503        put("loadAttachment", listener, new Runnable() {
1504            public void run() {
1505                try {
1506                    // 1.  Pruning.  Policy is to have one downloaded attachment at a time,
1507                    // per account, to reduce disk storage pressure.
1508                    pruneCachedAttachments(accountId);
1509
1510                    // 2. Open the remote folder.
1511                    // TODO all of these could be narrower projections
1512                    EmailContent.Account account =
1513                        EmailContent.Account.restoreAccountWithId(mContext, accountId);
1514                    EmailContent.Mailbox mailbox =
1515                        EmailContent.Mailbox.restoreMailboxWithId(mContext, mailboxId);
1516                    EmailContent.Message message =
1517                        EmailContent.Message.restoreMessageWithId(mContext, messageId);
1518                    Attachment attachment =
1519                        Attachment.restoreAttachmentWithId(mContext, attachmentId);
1520
1521                    Store remoteStore =
1522                        Store.getInstance(account.getStoreUri(mContext), mContext, null);
1523                    Folder remoteFolder = remoteStore.getFolder(mailbox.mDisplayName);
1524                    remoteFolder.open(OpenMode.READ_WRITE, null);
1525
1526                    // 3. Generate a shell message in which to retrieve the attachment,
1527                    // and a shell BodyPart for the attachment.  Then glue them together.
1528                    Message storeMessage = remoteFolder.createMessage(message.mServerId);
1529                    BodyPart storePart = new MimeBodyPart();
1530                    storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
1531                            attachment.mLocation);
1532                    storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
1533                            String.format("%s;\n name=\"%s\"",
1534                            attachment.mMimeType,
1535                            attachment.mFileName));
1536                    // TODO is this always true for attachments?  I think we dropped the
1537                    // true encoding along the way
1538                    storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
1539
1540                    MimeMultipart multipart = new MimeMultipart();
1541                    multipart.setSubType("mixed");
1542                    multipart.addBodyPart(storePart);
1543
1544                    storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
1545                    storeMessage.setBody(multipart);
1546
1547                    // 4. Now ask for the attachment to be fetched
1548                    FetchProfile fp = new FetchProfile();
1549                    fp.add(storePart);
1550                    remoteFolder.fetch(new Message[] { storeMessage }, fp, null);
1551
1552                    // 5. Save the downloaded file and update the attachment as necessary
1553                    LegacyConversions.saveAttachmentBody(mContext, storePart, attachment,
1554                            accountId);
1555
1556                    // 6. Report success
1557                    mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
1558                }
1559                catch (MessagingException me) {
1560                    if (Email.LOGD) Log.v(Email.LOG_TAG, "", me);
1561                    mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
1562                            me.getMessage());
1563                } catch (IOException ioe) {
1564                    Log.e(Email.LOG_TAG, "Error while storing attachment." + ioe.toString());
1565                }
1566            }});
1567    }
1568
1569    /**
1570     * Erase all stored attachments for a given account.  Rules:
1571     *   1.  All files in attachment directory are up for deletion
1572     *   2.  If filename does not match an known attachment id, it's deleted
1573     *   3.  If the attachment has location data (implying that it's reloadable), it's deleted
1574     */
1575    /* package */ void pruneCachedAttachments(long accountId) {
1576        ContentResolver resolver = mContext.getContentResolver();
1577        File cacheDir = AttachmentProvider.getAttachmentDirectory(mContext, accountId);
1578        for (File file : cacheDir.listFiles()) {
1579            if (file.exists()) {
1580                long id;
1581                try {
1582                    // the name of the file == the attachment id
1583                    id = Long.valueOf(file.getName());
1584                    Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id);
1585                    Cursor c = resolver.query(uri, PRUNE_ATTACHMENT_PROJECTION, null, null, null);
1586                    try {
1587                        if (c.moveToNext()) {
1588                            // if there is no way to reload the attachment, don't delete it
1589                            if (c.getString(0) == null) {
1590                                continue;
1591                            }
1592                        }
1593                    } finally {
1594                        c.close();
1595                    }
1596                    // Clear the content URI field since we're losing the attachment
1597                    resolver.update(uri, PRUNE_ATTACHMENT_CV, null, null);
1598                } catch (NumberFormatException nfe) {
1599                    // ignore filename != number error, and just delete it anyway
1600                }
1601                // This file can be safely deleted
1602                if (!file.delete()) {
1603                    file.deleteOnExit();
1604                }
1605            }
1606        }
1607    }
1608
1609    /**
1610     * Attempt to send any messages that are sitting in the Outbox.
1611     * @param account
1612     * @param listener
1613     */
1614    public void sendPendingMessages(final EmailContent.Account account, final long sentFolderId,
1615            MessagingListener listener) {
1616        put("sendPendingMessages", listener, new Runnable() {
1617            public void run() {
1618                sendPendingMessagesSynchronous(account, sentFolderId);
1619            }
1620        });
1621    }
1622
1623    /**
1624     * Attempt to send any messages that are sitting in the Outbox.
1625     *
1626     * @param account
1627     * @param listener
1628     */
1629    public void sendPendingMessagesSynchronous(final EmailContent.Account account,
1630            long sentFolderId) {
1631        // 1.  Loop through all messages in the account's outbox
1632        long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
1633        if (outboxId == Mailbox.NO_MAILBOX) {
1634            return;
1635        }
1636        ContentResolver resolver = mContext.getContentResolver();
1637        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
1638                EmailContent.Message.ID_COLUMN_PROJECTION,
1639                EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) },
1640                null);
1641        try {
1642            // 2.  exit early
1643            if (c.getCount() <= 0) {
1644                return;
1645            }
1646            // 3. do one-time setup of the Sender & other stuff
1647            Sender sender = Sender.getInstance(mContext, account.getSenderUri(mContext));
1648            Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, null);
1649            boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
1650            ContentValues moveToSentValues = null;
1651            if (requireMoveMessageToSentFolder) {
1652                moveToSentValues = new ContentValues();
1653                moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolderId);
1654            }
1655
1656            // 4.  loop through the available messages and send them
1657            while (c.moveToNext()) {
1658                long messageId = -1;
1659                try {
1660                    messageId = c.getLong(0);
1661                    mListeners.sendPendingMessagesStarted(account.mId, messageId);
1662                    sender.sendMessage(messageId);
1663                } catch (MessagingException me) {
1664                    // report error for this message, but keep trying others
1665                    mListeners.sendPendingMessagesFailed(account.mId, messageId, me);
1666                    continue;
1667                }
1668                // 5. move to sent, or delete
1669                Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
1670                if (requireMoveMessageToSentFolder) {
1671                    resolver.update(uri, moveToSentValues, null, null);
1672                    // TODO: post for a pending upload
1673                } else {
1674                    AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, messageId);
1675                    resolver.delete(uri, null, null);
1676                }
1677            }
1678            // 6. report completion/success
1679            mListeners.sendPendingMessagesCompleted(account.mId);
1680
1681        } catch (MessagingException me) {
1682            mListeners.sendPendingMessagesFailed(account.mId, -1, me);
1683        } finally {
1684            c.close();
1685        }
1686    }
1687
1688    /**
1689     * We do the local portion of this synchronously because other activities may have to make
1690     * updates based on what happens here
1691     * @param account
1692     * @param folder
1693     * @param message
1694     * @param listener
1695     */
1696    public void deleteMessage(final EmailContent.Account account, final String folder,
1697            final Message message, MessagingListener listener) {
1698        if (folder.equals(account.getTrashFolderName(mContext))) {
1699            return;
1700        }
1701        try {
1702            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1703                    null);
1704            Folder localFolder = localStore.getFolder(folder);
1705            Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName(mContext));
1706
1707            localFolder.copyMessages(new Message[] { message }, localTrashFolder, null);
1708            message.setFlag(Flag.DELETED, true);
1709
1710            if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) {
1711                PendingCommand command = new PendingCommand();
1712                command.command = PENDING_COMMAND_TRASH;
1713                command.arguments = new String[] { folder, message.getUid() };
1714                queuePendingCommand(account, command);
1715                processPendingCommands(account);
1716            }
1717        }
1718        catch (MessagingException me) {
1719            throw new RuntimeException("Error deleting message from local store.", me);
1720        }
1721    }
1722
1723    public void emptyTrash(final EmailContent.Account account, MessagingListener listener) {
1724        put("emptyTrash", listener, new Runnable() {
1725            public void run() {
1726                // TODO IMAP
1727                try {
1728                    Store localStore = Store.getInstance(
1729                            account.getLocalStoreUri(mContext), mContext, null);
1730                    Folder localFolder = localStore.getFolder(account.getTrashFolderName(mContext));
1731                    localFolder.open(OpenMode.READ_WRITE, null);
1732                    Message[] messages = localFolder.getMessages(null);
1733                    localFolder.setFlags(messages, new Flag[] {
1734                        Flag.DELETED
1735                    }, true);
1736                    localFolder.close(true);
1737                    mListeners.emptyTrashCompleted(account);
1738                }
1739                catch (Exception e) {
1740                    // TODO
1741                    if (Email.LOGD) {
1742                        Log.v(Email.LOG_TAG, "emptyTrash");
1743                    }
1744                }
1745            }
1746        });
1747    }
1748
1749    /**
1750     * Checks mail for one or multiple accounts. If account is null all accounts
1751     * are checked.  This entry point is for use by the mail checking service only, because it
1752     * gives slightly different callbacks (so the service doesn't get confused by callbacks
1753     * triggered by/for the foreground UI.
1754     *
1755     * TODO clean up the execution model which is unnecessarily threaded due to legacy code
1756     *
1757     * @param context
1758     * @param accountId the account to check
1759     * @param listener
1760     */
1761    public void checkMail(final long accountId, final long tag, final MessagingListener listener) {
1762        mListeners.checkMailStarted(mContext, accountId, tag);
1763
1764        // This puts the command on the queue (not synchronous)
1765        listFolders(accountId, null);
1766
1767        // Put this on the queue as well so it follows listFolders
1768        put("checkMail", listener, new Runnable() {
1769            public void run() {
1770                // send any pending outbound messages.  note, there is a slight race condition
1771                // here if we somehow don't have a sent folder, but this should never happen
1772                // because the call to sendMessage() would have built one previously.
1773                EmailContent.Account account =
1774                    EmailContent.Account.restoreAccountWithId(mContext, accountId);
1775                long sentboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_SENT);
1776                if (sentboxId != -1) {
1777                    sendPendingMessagesSynchronous(account, sentboxId);
1778                }
1779                // find mailbox # for inbox and sync it.
1780                // TODO we already know this in Controller, can we pass it in?
1781                long inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
1782                EmailContent.Mailbox mailbox =
1783                    EmailContent.Mailbox.restoreMailboxWithId(mContext, inboxId);
1784                synchronizeMailboxSynchronous(account, mailbox);
1785
1786                mListeners.checkMailFinished(mContext, accountId, tag, inboxId);
1787            }
1788        });
1789    }
1790
1791    public void saveDraft(final EmailContent.Account account, final Message message) {
1792        try {
1793            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1794                    null);
1795            LocalFolder localFolder =
1796                (LocalFolder) localStore.getFolder(account.getDraftsFolderName(mContext));
1797            localFolder.open(OpenMode.READ_WRITE, null);
1798            localFolder.appendMessages(new Message[] {
1799                message
1800            });
1801            Message localMessage = localFolder.getMessage(message.getUid());
1802            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1803
1804            PendingCommand command = new PendingCommand();
1805            command.command = PENDING_COMMAND_APPEND;
1806            command.arguments = new String[] {
1807                    localFolder.getName(),
1808                    localMessage.getUid() };
1809            queuePendingCommand(account, command);
1810            processPendingCommands(account);
1811        }
1812        catch (MessagingException e) {
1813            Log.e(Email.LOG_TAG, "Unable to save message as draft.", e);
1814        }
1815    }
1816
1817    private static class Command {
1818        public Runnable runnable;
1819
1820        public MessagingListener listener;
1821
1822        public String description;
1823
1824        @Override
1825        public String toString() {
1826            return description;
1827        }
1828    }
1829}
1830