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