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