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