Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.service;
     18 
     19 import android.app.Service;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.net.TrafficStats;
     27 import android.net.Uri;
     28 import android.os.IBinder;
     29 import android.os.SystemClock;
     30 import android.text.TextUtils;
     31 import android.text.format.DateUtils;
     32 
     33 import com.android.email.DebugUtils;
     34 import com.android.email.LegacyConversions;
     35 import com.android.email.NotificationController;
     36 import com.android.email.NotificationControllerCreatorHolder;
     37 import com.android.email.R;
     38 import com.android.email.mail.Store;
     39 import com.android.email.provider.Utilities;
     40 import com.android.emailcommon.Logging;
     41 import com.android.emailcommon.TrafficFlags;
     42 import com.android.emailcommon.internet.MimeUtility;
     43 import com.android.emailcommon.mail.AuthenticationFailedException;
     44 import com.android.emailcommon.mail.FetchProfile;
     45 import com.android.emailcommon.mail.Flag;
     46 import com.android.emailcommon.mail.Folder;
     47 import com.android.emailcommon.mail.Folder.FolderType;
     48 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
     49 import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
     50 import com.android.emailcommon.mail.Folder.OpenMode;
     51 import com.android.emailcommon.mail.Message;
     52 import com.android.emailcommon.mail.MessagingException;
     53 import com.android.emailcommon.mail.Part;
     54 import com.android.emailcommon.provider.Account;
     55 import com.android.emailcommon.provider.EmailContent;
     56 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     57 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     58 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     59 import com.android.emailcommon.provider.Mailbox;
     60 import com.android.emailcommon.service.EmailServiceStatus;
     61 import com.android.emailcommon.service.SearchParams;
     62 import com.android.emailcommon.service.SyncWindow;
     63 import com.android.emailcommon.utility.AttachmentUtilities;
     64 import com.android.mail.providers.UIProvider;
     65 import com.android.mail.utils.LogUtils;
     66 
     67 import java.util.ArrayList;
     68 import java.util.Arrays;
     69 import java.util.Comparator;
     70 import java.util.Date;
     71 import java.util.HashMap;
     72 import java.util.List;
     73 
     74 public class ImapService extends Service {
     75     // TODO get these from configurations or settings.
     76     private static final long QUICK_SYNC_WINDOW_MILLIS = DateUtils.DAY_IN_MILLIS;
     77     private static final long FULL_SYNC_WINDOW_MILLIS = 7 * DateUtils.DAY_IN_MILLIS;
     78     private static final long FULL_SYNC_INTERVAL_MILLIS = 4 * DateUtils.HOUR_IN_MILLIS;
     79 
     80     // The maximum number of messages to fetch in a single command.
     81     private static final int MAX_MESSAGES_TO_FETCH = 500;
     82     private static final int MINIMUM_MESSAGES_TO_SYNC = 10;
     83     private static final int LOAD_MORE_MIN_INCREMENT = 10;
     84     private static final int LOAD_MORE_MAX_INCREMENT = 20;
     85     private static final long INITIAL_WINDOW_SIZE_INCREASE = 24 * 60 * 60 * 1000;
     86 
     87     private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN };
     88     private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
     89     private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
     90 
     91     /**
     92      * Simple cache for last search result mailbox by account and serverId, since the most common
     93      * case will be repeated use of the same mailbox
     94      */
     95     private static long mLastSearchAccountKey = Account.NO_ACCOUNT;
     96     private static String mLastSearchServerId = null;
     97     private static Mailbox mLastSearchRemoteMailbox = null;
     98 
     99     /**
    100      * Cache search results by account; this allows for "load more" support without having to
    101      * redo the search (which can be quite slow).  SortableMessage is a smallish class, so memory
    102      * shouldn't be an issue
    103      */
    104     private static final HashMap<Long, SortableMessage[]> sSearchResults =
    105             new HashMap<Long, SortableMessage[]>();
    106 
    107     /**
    108      * We write this into the serverId field of messages that will never be upsynced.
    109      */
    110     private static final String LOCAL_SERVERID_PREFIX = "Local-";
    111 
    112     private static String sMessageDecodeErrorString;
    113 
    114     /**
    115      * Used in ImapFolder for base64 errors. Cached here because ImapFolder does not have access
    116      * to a Context object.
    117      * @return Error string or empty string
    118      */
    119     public static String getMessageDecodeErrorString() {
    120         return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString;
    121     }
    122 
    123     @Override
    124     public void onCreate() {
    125         super.onCreate();
    126 
    127         sMessageDecodeErrorString = getString(R.string.message_decode_error);
    128     }
    129 
    130     @Override
    131     public int onStartCommand(Intent intent, int flags, int startId) {
    132         return Service.START_STICKY;
    133     }
    134 
    135     /**
    136      * Create our EmailService implementation here.
    137      */
    138     private final EmailServiceStub mBinder = new EmailServiceStub() {
    139         @Override
    140         public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
    141             try {
    142                 return searchMailboxImpl(getApplicationContext(), accountId, searchParams,
    143                         destMailboxId);
    144             } catch (MessagingException e) {
    145                 // Ignore
    146             }
    147             return 0;
    148         }
    149     };
    150 
    151     @Override
    152     public IBinder onBind(Intent intent) {
    153         mBinder.init(this);
    154         return mBinder;
    155     }
    156 
    157     /**
    158      * Start foreground synchronization of the specified folder. This is called by
    159      * synchronizeMailbox or checkMail.
    160      * TODO this should use ID's instead of fully-restored objects
    161      * @return The status code for whether this operation succeeded.
    162      * @throws MessagingException
    163      */
    164     public static synchronized int synchronizeMailboxSynchronous(Context context,
    165             final Account account, final Mailbox folder, final boolean loadMore,
    166             final boolean uiRefresh) throws MessagingException {
    167         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
    168         final NotificationController nc =
    169                 NotificationControllerCreatorHolder.getInstance(context);
    170         Store remoteStore = null;
    171         try {
    172             remoteStore = Store.getInstance(account, context);
    173             processPendingActionsSynchronous(context, account, remoteStore, uiRefresh);
    174             synchronizeMailboxGeneric(context, account, remoteStore, folder, loadMore, uiRefresh);
    175             // Clear authentication notification for this account
    176             nc.cancelLoginFailedNotification(account.mId);
    177         } catch (MessagingException e) {
    178             if (Logging.LOGD) {
    179                 LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxSynchronous", e);
    180             }
    181             if (e instanceof AuthenticationFailedException) {
    182                 // Generate authentication notification
    183                 nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */);
    184             }
    185             throw e;
    186         } finally {
    187             if (remoteStore != null) {
    188                 remoteStore.closeConnections();
    189             }
    190         }
    191         // TODO: Rather than use exceptions as logic above, return the status and handle it
    192         // correctly in caller.
    193         return EmailServiceStatus.SUCCESS;
    194     }
    195 
    196     /**
    197      * Lightweight record for the first pass of message sync, where I'm just seeing if
    198      * the local message requires sync.  Later (for messages that need syncing) we'll do a full
    199      * readout from the DB.
    200      */
    201     private static class LocalMessageInfo {
    202         private static final int COLUMN_ID = 0;
    203         private static final int COLUMN_FLAG_READ = 1;
    204         private static final int COLUMN_FLAG_FAVORITE = 2;
    205         private static final int COLUMN_FLAG_LOADED = 3;
    206         private static final int COLUMN_SERVER_ID = 4;
    207         private static final int COLUMN_FLAGS =  5;
    208         private static final int COLUMN_TIMESTAMP =  6;
    209         private static final String[] PROJECTION = {
    210                 MessageColumns._ID,
    211                 MessageColumns.FLAG_READ,
    212                 MessageColumns.FLAG_FAVORITE,
    213                 MessageColumns.FLAG_LOADED,
    214                 SyncColumns.SERVER_ID,
    215                 MessageColumns.FLAGS,
    216                 MessageColumns.TIMESTAMP
    217         };
    218 
    219         final long mId;
    220         final boolean mFlagRead;
    221         final boolean mFlagFavorite;
    222         final int mFlagLoaded;
    223         final String mServerId;
    224         final int mFlags;
    225         final long mTimestamp;
    226 
    227         public LocalMessageInfo(Cursor c) {
    228             mId = c.getLong(COLUMN_ID);
    229             mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
    230             mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
    231             mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
    232             mServerId = c.getString(COLUMN_SERVER_ID);
    233             mFlags = c.getInt(COLUMN_FLAGS);
    234             mTimestamp = c.getLong(COLUMN_TIMESTAMP);
    235             // Note: mailbox key and account key not needed - they are projected for the SELECT
    236         }
    237     }
    238 
    239     private static class OldestTimestampInfo {
    240         private static final int COLUMN_OLDEST_TIMESTAMP = 0;
    241         private static final String[] PROJECTION = new String[] {
    242             "MIN(" + MessageColumns.TIMESTAMP + ")"
    243         };
    244     }
    245 
    246     /**
    247      * Load the structure and body of messages not yet synced
    248      * @param account the account we're syncing
    249      * @param remoteFolder the (open) Folder we're working on
    250      * @param messages an array of Messages we've got headers for
    251      * @param toMailbox the destination mailbox we're syncing
    252      * @throws MessagingException
    253      */
    254     static void loadUnsyncedMessages(final Context context, final Account account,
    255             Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)
    256             throws MessagingException {
    257 
    258         FetchProfile fp = new FetchProfile();
    259         fp.add(FetchProfile.Item.STRUCTURE);
    260         remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null);
    261         Message [] oneMessageArray = new Message[1];
    262         for (Message message : messages) {
    263             // Build a list of parts we are interested in. Text parts will be downloaded
    264             // right now, attachments will be left for later.
    265             ArrayList<Part> viewables = new ArrayList<Part>();
    266             ArrayList<Part> attachments = new ArrayList<Part>();
    267             MimeUtility.collectParts(message, viewables, attachments);
    268             // Download the viewables immediately
    269             oneMessageArray[0] = message;
    270             for (Part part : viewables) {
    271                 fp.clear();
    272                 fp.add(part);
    273                 remoteFolder.fetch(oneMessageArray, fp, null);
    274             }
    275             // Store the updated message locally and mark it fully loaded
    276             Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
    277                     EmailContent.Message.FLAG_LOADED_COMPLETE);
    278         }
    279     }
    280 
    281     public static void downloadFlagAndEnvelope(final Context context, final Account account,
    282             final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
    283             HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
    284             throws MessagingException {
    285         FetchProfile fp = new FetchProfile();
    286         fp.add(FetchProfile.Item.FLAGS);
    287         fp.add(FetchProfile.Item.ENVELOPE);
    288 
    289         final HashMap<String, LocalMessageInfo> localMapCopy;
    290         if (localMessageMap != null)
    291             localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
    292         else {
    293             localMapCopy = new HashMap<String, LocalMessageInfo>();
    294         }
    295 
    296         remoteFolder.fetch(unsyncedMessages.toArray(new Message[unsyncedMessages.size()]), fp,
    297                 new MessageRetrievalListener() {
    298                     @Override
    299                     public void messageRetrieved(Message message) {
    300                         try {
    301                             // Determine if the new message was already known (e.g. partial)
    302                             // And create or reload the full message info
    303                             final LocalMessageInfo localMessageInfo =
    304                                     localMapCopy.get(message.getUid());
    305                             final boolean localExists = localMessageInfo != null;
    306 
    307                             if (!localExists && message.isSet(Flag.DELETED)) {
    308                                 // This is a deleted message that we don't have locally, so don't
    309                                 // create it
    310                                 return;
    311                             }
    312 
    313                             final EmailContent.Message localMessage;
    314                             if (!localExists) {
    315                                 localMessage = new EmailContent.Message();
    316                             } else {
    317                                 localMessage = EmailContent.Message.restoreMessageWithId(
    318                                         context, localMessageInfo.mId);
    319                             }
    320 
    321                             if (localMessage != null) {
    322                                 try {
    323                                     // Copy the fields that are available into the message
    324                                     LegacyConversions.updateMessageFields(localMessage,
    325                                             message, account.mId, mailbox.mId);
    326                                     // Commit the message to the local store
    327                                     Utilities.saveOrUpdate(localMessage, context);
    328                                     // Track the "new" ness of the downloaded message
    329                                     if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
    330                                         unseenMessages.add(localMessage.mId);
    331                                     }
    332                                 } catch (MessagingException me) {
    333                                     LogUtils.e(Logging.LOG_TAG,
    334                                             "Error while copying downloaded message." + me);
    335                                 }
    336                             }
    337                         }
    338                         catch (Exception e) {
    339                             LogUtils.e(Logging.LOG_TAG,
    340                                     "Error while storing downloaded message." + e.toString());
    341                         }
    342                     }
    343 
    344                     @Override
    345                     public void loadAttachmentProgress(int progress) {
    346                     }
    347                 });
    348 
    349     }
    350 
    351     /**
    352      * Synchronizer for IMAP.
    353      *
    354      * TODO Break this method up into smaller chunks.
    355      *
    356      * @param account the account to sync
    357      * @param mailbox the mailbox to sync
    358      * @param loadMore whether we should be loading more older messages
    359      * @param uiRefresh whether this request is in response to a user action
    360      * @throws MessagingException
    361      */
    362     private synchronized static void synchronizeMailboxGeneric(final Context context,
    363             final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore,
    364             final boolean uiRefresh)
    365             throws MessagingException {
    366 
    367         LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxGeneric " + account + " " + mailbox + " "
    368                 + loadMore + " " + uiRefresh);
    369 
    370         final ArrayList<Long> unseenMessages = new ArrayList<Long>();
    371 
    372         ContentResolver resolver = context.getContentResolver();
    373 
    374         // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
    375         if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
    376             return;
    377         }
    378 
    379         // 1. Figure out what our sync window should be.
    380         long endDate;
    381 
    382         // We will do a full sync if the user has actively requested a sync, or if it has been
    383         // too long since the last full sync.
    384         // If we have rebooted since the last full sync, then we may get a negative
    385         // timeSinceLastFullSync. In this case, we don't know how long it's been since the last
    386         // full sync so we should perform the full sync.
    387         final long timeSinceLastFullSync = SystemClock.elapsedRealtime() -
    388                 mailbox.mLastFullSyncTime;
    389         final boolean fullSync = (uiRefresh || loadMore ||
    390                 timeSinceLastFullSync >= FULL_SYNC_INTERVAL_MILLIS || timeSinceLastFullSync < 0);
    391 
    392         if (account.mSyncLookback == SyncWindow.SYNC_WINDOW_ALL) {
    393             // This is really for testing. There is no UI that allows setting the sync window for
    394             // IMAP, but it can be set by sending a special intent to AccountSetupFinal activity.
    395             endDate = 0;
    396         } else if (fullSync) {
    397             // Find the oldest message in the local store. We need our time window to include
    398             // all messages that are currently present locally.
    399             endDate = System.currentTimeMillis() - FULL_SYNC_WINDOW_MILLIS;
    400             Cursor localOldestCursor = null;
    401             try {
    402                 // b/11520812 Ignore message with timestamp = 0 (which includes NULL)
    403                 localOldestCursor = resolver.query(EmailContent.Message.CONTENT_URI,
    404                         OldestTimestampInfo.PROJECTION,
    405                         EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + " AND " +
    406                                 MessageColumns.MAILBOX_KEY + "=? AND " +
    407                                 MessageColumns.TIMESTAMP + "!=0",
    408                         new String[] {String.valueOf(account.mId), String.valueOf(mailbox.mId)},
    409                         null);
    410                 if (localOldestCursor != null && localOldestCursor.moveToFirst()) {
    411                     long oldestLocalMessageDate = localOldestCursor.getLong(
    412                             OldestTimestampInfo.COLUMN_OLDEST_TIMESTAMP);
    413                     if (oldestLocalMessageDate > 0) {
    414                         endDate = Math.min(endDate, oldestLocalMessageDate);
    415                         LogUtils.d(
    416                                 Logging.LOG_TAG, "oldest local message " + oldestLocalMessageDate);
    417                     }
    418                 }
    419             } finally {
    420                 if (localOldestCursor != null) {
    421                     localOldestCursor.close();
    422                 }
    423             }
    424             LogUtils.d(Logging.LOG_TAG, "full sync: original window: now - " + endDate);
    425         } else {
    426             // We are doing a frequent, quick sync. This only syncs a small time window, so that
    427             // we wil get any new messages, but not spend a lot of bandwidth downloading
    428             // messageIds that we most likely already have.
    429             endDate = System.currentTimeMillis() - QUICK_SYNC_WINDOW_MILLIS;
    430             LogUtils.d(Logging.LOG_TAG, "quick sync: original window: now - " + endDate);
    431         }
    432 
    433         // 2. Open the remote folder and create the remote folder if necessary
    434         // The account might have been deleted
    435         if (remoteStore == null) {
    436             LogUtils.d(Logging.LOG_TAG, "account is apparently deleted");
    437             return;
    438         }
    439         final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
    440 
    441         // If the folder is a "special" folder we need to see if it exists
    442         // on the remote server. It if does not exist we'll try to create it. If we
    443         // can't create we'll abort. This will happen on every single Pop3 folder as
    444         // designed and on Imap folders during error conditions. This allows us
    445         // to treat Pop3 and Imap the same in this code.
    446         if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT) {
    447             if (!remoteFolder.exists()) {
    448                 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
    449                     LogUtils.w(Logging.LOG_TAG, "could not create remote folder type %d",
    450                         mailbox.mType);
    451                     return;
    452                 }
    453             }
    454         }
    455         remoteFolder.open(OpenMode.READ_WRITE);
    456 
    457         // 3. Trash any remote messages that are marked as trashed locally.
    458         // TODO - this comment was here, but no code was here.
    459 
    460         // 4. Get the number of messages on the server.
    461         // TODO: this value includes deleted but unpurged messages, and so slightly mismatches
    462         // the contents of our DB since we drop deleted messages. Figure out what to do about this.
    463         final int remoteMessageCount = remoteFolder.getMessageCount();
    464 
    465         // 5. Save folder message count locally.
    466         mailbox.updateMessageCount(context, remoteMessageCount);
    467 
    468         // 6. Get all message Ids in our sync window:
    469         Message[] remoteMessages;
    470         remoteMessages = remoteFolder.getMessages(0, endDate, null);
    471         LogUtils.d(Logging.LOG_TAG, "received " + remoteMessages.length + " messages");
    472 
    473         // 7. See if we need any additional messages beyond our date query range results.
    474         // If we do, keep increasing the size of our query window until we have
    475         // enough, or until we have all messages in the mailbox.
    476         int totalCountNeeded;
    477         if (loadMore) {
    478             totalCountNeeded = remoteMessages.length + LOAD_MORE_MIN_INCREMENT;
    479         } else {
    480             totalCountNeeded = remoteMessages.length;
    481             if (fullSync && totalCountNeeded < MINIMUM_MESSAGES_TO_SYNC) {
    482                 totalCountNeeded = MINIMUM_MESSAGES_TO_SYNC;
    483             }
    484         }
    485         LogUtils.d(Logging.LOG_TAG, "need " + totalCountNeeded + " total");
    486 
    487         final int additionalMessagesNeeded = totalCountNeeded - remoteMessages.length;
    488         if (additionalMessagesNeeded > 0) {
    489             LogUtils.d(Logging.LOG_TAG, "trying to get " + additionalMessagesNeeded + " more");
    490             long startDate = endDate - 1;
    491             Message[] additionalMessages = new Message[0];
    492             long windowIncreaseSize = INITIAL_WINDOW_SIZE_INCREASE;
    493             while (additionalMessages.length < additionalMessagesNeeded && endDate > 0) {
    494                 endDate = endDate - windowIncreaseSize;
    495                 if (endDate < 0) {
    496                     LogUtils.d(Logging.LOG_TAG, "window size too large, this is the last attempt");
    497                     endDate = 0;
    498                 }
    499                 LogUtils.d(Logging.LOG_TAG,
    500                         "requesting additional messages from range " + startDate + " - " + endDate);
    501                 additionalMessages = remoteFolder.getMessages(startDate, endDate, null);
    502 
    503                 // If don't get enough messages with the first window size expansion,
    504                 // we need to accelerate rate at which the window expands. Otherwise,
    505                 // if there were no messages for several weeks, we'd always end up
    506                 // performing dozens of queries.
    507                 windowIncreaseSize *= 2;
    508             }
    509 
    510             LogUtils.d(Logging.LOG_TAG, "additionalMessages " + additionalMessages.length);
    511             if (additionalMessages.length < additionalMessagesNeeded) {
    512                 // We have attempted to load a window that goes all the way back to time zero,
    513                 // but we still don't have as many messages as the server says are in the inbox.
    514                 // This is not expected to happen.
    515                 LogUtils.e(Logging.LOG_TAG, "expected to find " + additionalMessagesNeeded
    516                         + " more messages, only got " + additionalMessages.length);
    517             }
    518             int additionalToKeep = additionalMessages.length;
    519             if (additionalMessages.length > LOAD_MORE_MAX_INCREMENT) {
    520                 // We have way more additional messages than intended, drop some of them.
    521                 // The last messages are the most recent, so those are the ones we need to keep.
    522                 additionalToKeep = LOAD_MORE_MAX_INCREMENT;
    523             }
    524 
    525             // Copy the messages into one array.
    526             Message[] allMessages = new Message[remoteMessages.length + additionalToKeep];
    527             System.arraycopy(remoteMessages, 0, allMessages, 0, remoteMessages.length);
    528             // additionalMessages may have more than we need, only copy the last
    529             // several. These are the most recent messages in that set because
    530             // of the way IMAP server returns messages.
    531             System.arraycopy(additionalMessages, additionalMessages.length - additionalToKeep,
    532                     allMessages, remoteMessages.length, additionalToKeep);
    533             remoteMessages = allMessages;
    534         }
    535 
    536         // 8. Get the all of the local messages within the sync window, and create
    537         // an index of the uids.
    538         // The IMAP query for messages ignores time, and only looks at the date part of the endDate.
    539         // So if we query for messages since Aug 11 at 3:00 PM, we can get messages from any time
    540         // on Aug 11. Our IMAP query results can include messages up to 24 hours older than endDate,
    541         // or up to 25 hours older at a daylight savings transition.
    542         // It is important that we have the Id of any local message that could potentially be
    543         // returned by the IMAP query, or we will create duplicate copies of the same messages.
    544         // So we will increase our local query range by this much.
    545         // Note that this complicates deletion: It's not okay to delete anything that is in the
    546         // localMessageMap but not in the remote result, because we know that we may be getting
    547         // Ids of local messages that are outside the IMAP query window.
    548         Cursor localUidCursor = null;
    549         HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
    550         try {
    551             // FLAG: There is a problem that causes us to store the wrong date on some messages,
    552             // so messages get a date of zero. If we filter these messages out and don't put them
    553             // in our localMessageMap, then we'll end up loading the same message again.
    554             // See b/10508861
    555 //            final long queryEndDate = endDate - DateUtils.DAY_IN_MILLIS - DateUtils.HOUR_IN_MILLIS;
    556             final long queryEndDate = 0;
    557             localUidCursor = resolver.query(
    558                     EmailContent.Message.CONTENT_URI,
    559                     LocalMessageInfo.PROJECTION,
    560                     EmailContent.MessageColumns.ACCOUNT_KEY + "=?"
    561                             + " AND " + MessageColumns.MAILBOX_KEY + "=?"
    562                             + " AND " + MessageColumns.TIMESTAMP + ">=?",
    563                     new String[] {
    564                             String.valueOf(account.mId),
    565                             String.valueOf(mailbox.mId),
    566                             String.valueOf(queryEndDate) },
    567                     null);
    568             while (localUidCursor.moveToNext()) {
    569                 LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
    570                 // If the message has no server id, it's local only. This should only happen for
    571                 // mail created on the client that has failed to upsync. We want to ignore such
    572                 // mail during synchronization (i.e. leave it as-is and let the next sync try again
    573                 // to upsync).
    574                 if (!TextUtils.isEmpty(info.mServerId)) {
    575                     localMessageMap.put(info.mServerId, info);
    576                 }
    577             }
    578         } finally {
    579             if (localUidCursor != null) {
    580                 localUidCursor.close();
    581             }
    582         }
    583 
    584         // 9. Get a list of the messages that are in the remote list but not on the
    585         // local store, or messages that are in the local store but failed to download
    586         // on the last sync. These are the new messages that we will download.
    587         // Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
    588         // because they are locally deleted and we don't need or want the old message from
    589         // the server.
    590         final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
    591         final HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
    592         // Process the messages in the reverse order we received them in. This means that
    593         // we load the most recent one first, which gives a better user experience.
    594         for (int i = remoteMessages.length - 1; i >= 0; i--) {
    595             Message message = remoteMessages[i];
    596             LogUtils.d(Logging.LOG_TAG, "remote message " + message.getUid());
    597             remoteUidMap.put(message.getUid(), message);
    598 
    599             LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
    600 
    601             // localMessage == null -> message has never been created (not even headers)
    602             // mFlagLoaded = UNLOADED -> message created, but none of body loaded
    603             // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
    604             // mFlagLoaded = COMPLETE -> message body has been completely loaded
    605             // mFlagLoaded = DELETED -> message has been deleted
    606             // Only the first two of these are "unsynced", so let's retrieve them
    607             if (localMessage == null ||
    608                     (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) ||
    609                     (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
    610                 unsyncedMessages.add(message);
    611             }
    612         }
    613 
    614         // 10. Download basic info about the new/unloaded messages (if any)
    615         /*
    616          * Fetch the flags and envelope only of the new messages. This is intended to get us
    617          * critical data as fast as possible, and then we'll fill in the details.
    618          */
    619         if (unsyncedMessages.size() > 0) {
    620             downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
    621                     localMessageMap, unseenMessages);
    622         }
    623 
    624         // 11. Refresh the flags for any messages in the local store that we didn't just download.
    625         // TODO This is a bit wasteful because we're also updating any messages we already did get
    626         // the flags and envelope for previously.
    627         // TODO: the fetch() function, and others, should take List<>s of messages, not
    628         // arrays of messages.
    629         FetchProfile fp = new FetchProfile();
    630         fp.add(FetchProfile.Item.FLAGS);
    631         if (remoteMessages.length > MAX_MESSAGES_TO_FETCH) {
    632             List<Message> remoteMessageList = Arrays.asList(remoteMessages);
    633             for (int start = 0; start < remoteMessageList.size(); start += MAX_MESSAGES_TO_FETCH) {
    634                 int end = start + MAX_MESSAGES_TO_FETCH;
    635                 if (end >= remoteMessageList.size()) {
    636                     end = remoteMessageList.size() - 1;
    637                 }
    638                 List<Message> chunk = remoteMessageList.subList(start, end);
    639                 final Message[] partialArray = chunk.toArray(new Message[chunk.size()]);
    640                 // Fetch this one chunk of messages
    641                 remoteFolder.fetch(partialArray, fp, null);
    642             }
    643         } else {
    644             remoteFolder.fetch(remoteMessages, fp, null);
    645         }
    646         boolean remoteSupportsSeen = false;
    647         boolean remoteSupportsFlagged = false;
    648         boolean remoteSupportsAnswered = false;
    649         for (Flag flag : remoteFolder.getPermanentFlags()) {
    650             if (flag == Flag.SEEN) {
    651                 remoteSupportsSeen = true;
    652             }
    653             if (flag == Flag.FLAGGED) {
    654                 remoteSupportsFlagged = true;
    655             }
    656             if (flag == Flag.ANSWERED) {
    657                 remoteSupportsAnswered = true;
    658             }
    659         }
    660 
    661         // 12. Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
    662         if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
    663             for (Message remoteMessage : remoteMessages) {
    664                 LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
    665                 if (localMessageInfo == null) {
    666                     continue;
    667                 }
    668                 boolean localSeen = localMessageInfo.mFlagRead;
    669                 boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
    670                 boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
    671                 boolean localFlagged = localMessageInfo.mFlagFavorite;
    672                 boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
    673                 boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
    674                 int localFlags = localMessageInfo.mFlags;
    675                 boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
    676                 boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
    677                 boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
    678                 if (newSeen || newFlagged || newAnswered) {
    679                     Uri uri = ContentUris.withAppendedId(
    680                             EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
    681                     ContentValues updateValues = new ContentValues();
    682                     updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
    683                     updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
    684                     if (remoteAnswered) {
    685                         localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
    686                     } else {
    687                         localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
    688                     }
    689                     updateValues.put(MessageColumns.FLAGS, localFlags);
    690                     resolver.update(uri, updateValues, null, null);
    691                 }
    692             }
    693         }
    694 
    695         // 12.5 Remove messages that are marked as deleted so that we drop them from the DB in the
    696         // next step
    697         for (final Message remoteMessage : remoteMessages) {
    698             if (remoteMessage.isSet(Flag.DELETED)) {
    699                 remoteUidMap.remove(remoteMessage.getUid());
    700                 unsyncedMessages.remove(remoteMessage);
    701             }
    702         }
    703 
    704         // 13. Remove messages that are in the local store and in the current sync window,
    705         // but no longer on the remote store. Note that localMessageMap can contain messages
    706         // that are not actually in our sync window. We need to check the timestamp to ensure
    707         // that it is before deleting.
    708         for (final LocalMessageInfo info : localMessageMap.values()) {
    709             // If this message is inside our sync window, and we cannot find it in our list
    710             // of remote messages, then we know it's been deleted from the server.
    711             if (info.mTimestamp >= endDate && !remoteUidMap.containsKey(info.mServerId)) {
    712                 // Delete associated data (attachment files)
    713                 // Attachment & Body records are auto-deleted when we delete the Message record
    714                 AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, info.mId);
    715 
    716                 // Delete the message itself
    717                 final Uri uriToDelete = ContentUris.withAppendedId(
    718                         EmailContent.Message.CONTENT_URI, info.mId);
    719                 resolver.delete(uriToDelete, null, null);
    720 
    721                 // Delete extra rows (e.g. updated or deleted)
    722                 final Uri updateRowToDelete = ContentUris.withAppendedId(
    723                         EmailContent.Message.UPDATED_CONTENT_URI, info.mId);
    724                 resolver.delete(updateRowToDelete, null, null);
    725                 final Uri deleteRowToDelete = ContentUris.withAppendedId(
    726                         EmailContent.Message.DELETED_CONTENT_URI, info.mId);
    727                 resolver.delete(deleteRowToDelete, null, null);
    728             }
    729         }
    730 
    731         loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
    732 
    733         if (fullSync) {
    734             mailbox.updateLastFullSyncTime(context, SystemClock.elapsedRealtime());
    735         }
    736 
    737         // 14. Clean up and report results
    738         remoteFolder.close(false);
    739     }
    740 
    741     /**
    742      * Find messages in the updated table that need to be written back to server.
    743      *
    744      * Handles:
    745      *   Read/Unread
    746      *   Flagged
    747      *   Append (upload)
    748      *   Move To Trash
    749      *   Empty trash
    750      * TODO:
    751      *   Move
    752      *
    753      * @param account the account to scan for pending actions
    754      * @throws MessagingException
    755      */
    756     private static void processPendingActionsSynchronous(Context context, Account account,
    757             Store remoteStore, boolean manualSync)
    758             throws MessagingException {
    759         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
    760         String[] accountIdArgs = new String[] { Long.toString(account.mId) };
    761 
    762         // Handle deletes first, it's always better to get rid of things first
    763         processPendingDeletesSynchronous(context, account, remoteStore, accountIdArgs);
    764 
    765         // Handle uploads (currently, only to sent messages)
    766         processPendingUploadsSynchronous(context, account, remoteStore, accountIdArgs, manualSync);
    767 
    768         // Now handle updates / upsyncs
    769         processPendingUpdatesSynchronous(context, account, remoteStore, accountIdArgs);
    770     }
    771 
    772     /**
    773      * Get the mailbox corresponding to the remote location of a message; this will normally be
    774      * the mailbox whose _id is mailboxKey, except for search results, where we must look it up
    775      * by serverId.
    776      *
    777      * @param message the message in question
    778      * @return the mailbox in which the message resides on the server
    779      */
    780     private static Mailbox getRemoteMailboxForMessage(
    781             Context context, EmailContent.Message message) {
    782         // If this is a search result, use the protocolSearchInfo field to get the server info
    783         if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
    784             long accountKey = message.mAccountKey;
    785             String protocolSearchInfo = message.mProtocolSearchInfo;
    786             if (accountKey == mLastSearchAccountKey &&
    787                     protocolSearchInfo.equals(mLastSearchServerId)) {
    788                 return mLastSearchRemoteMailbox;
    789             }
    790             Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
    791                     Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
    792                     new String[] {protocolSearchInfo, Long.toString(accountKey) },
    793                     null);
    794             try {
    795                 if (c.moveToNext()) {
    796                     Mailbox mailbox = new Mailbox();
    797                     mailbox.restore(c);
    798                     mLastSearchAccountKey = accountKey;
    799                     mLastSearchServerId = protocolSearchInfo;
    800                     mLastSearchRemoteMailbox = mailbox;
    801                     return mailbox;
    802                 } else {
    803                     return null;
    804                 }
    805             } finally {
    806                 c.close();
    807             }
    808         } else {
    809             return Mailbox.restoreMailboxWithId(context, message.mMailboxKey);
    810         }
    811     }
    812 
    813     /**
    814      * Scan for messages that are in the Message_Deletes table, look for differences that
    815      * we can deal with, and do the work.
    816      */
    817     private static void processPendingDeletesSynchronous(Context context, Account account,
    818             Store remoteStore, String[] accountIdArgs) {
    819         Cursor deletes = context.getContentResolver().query(
    820                 EmailContent.Message.DELETED_CONTENT_URI,
    821                 EmailContent.Message.CONTENT_PROJECTION,
    822                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
    823                 EmailContent.MessageColumns.MAILBOX_KEY);
    824         long lastMessageId = -1;
    825         try {
    826             // loop through messages marked as deleted
    827             while (deletes.moveToNext()) {
    828                 EmailContent.Message oldMessage =
    829                         EmailContent.getContent(context, deletes, EmailContent.Message.class);
    830 
    831                 if (oldMessage != null) {
    832                     lastMessageId = oldMessage.mId;
    833 
    834                     Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage);
    835                     if (mailbox == null) {
    836                         continue; // Mailbox removed. Move to the next message.
    837                     }
    838                     final boolean deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
    839 
    840                     // Dispatch here for specific change types
    841                     if (deleteFromTrash) {
    842                         // Move message to trash
    843                         processPendingDeleteFromTrash(remoteStore, mailbox, oldMessage);
    844                     }
    845 
    846                     // Finally, delete the update
    847                     Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
    848                             oldMessage.mId);
    849                     context.getContentResolver().delete(uri, null, null);
    850                 }
    851             }
    852         } catch (MessagingException me) {
    853             // Presumably an error here is an account connection failure, so there is
    854             // no point in continuing through the rest of the pending updates.
    855             if (DebugUtils.DEBUG) {
    856                 LogUtils.d(Logging.LOG_TAG, "Unable to process pending delete for id="
    857                         + lastMessageId + ": " + me);
    858             }
    859         } finally {
    860             deletes.close();
    861         }
    862     }
    863 
    864     /**
    865      * Scan for messages that are in Sent, and are in need of upload,
    866      * and send them to the server. "In need of upload" is defined as:
    867      *  serverId == null (no UID has been assigned)
    868      * or
    869      *  message is in the updated list
    870      *
    871      * Note we also look for messages that are moving from drafts->outbox->sent. They never
    872      * go through "drafts" or "outbox" on the server, so we hang onto these until they can be
    873      * uploaded directly to the Sent folder.
    874      */
    875     private static void processPendingUploadsSynchronous(Context context, Account account,
    876             Store remoteStore, String[] accountIdArgs, boolean manualSync) {
    877         ContentResolver resolver = context.getContentResolver();
    878         // Find the Sent folder (since that's all we're uploading for now
    879         // TODO: Upsync for all folders? (In case a user moves mail from Sent before it is
    880         // handled. Also, this would generically solve allowing drafts to upload.)
    881         Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
    882                 MailboxColumns.ACCOUNT_KEY + "=?"
    883                 + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT,
    884                 accountIdArgs, null);
    885         long lastMessageId = -1;
    886         try {
    887             while (mailboxes.moveToNext()) {
    888                 long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN);
    889                 String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) };
    890                 // Demand load mailbox
    891                 Mailbox mailbox = null;
    892 
    893                 // First handle the "new" messages (serverId == null)
    894                 Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI,
    895                         EmailContent.Message.ID_PROJECTION,
    896                         MessageColumns.MAILBOX_KEY + "=?"
    897                         + " and (" + MessageColumns.SERVER_ID + " is null"
    898                         + " or " + MessageColumns.SERVER_ID + "=''" + ")",
    899                         mailboxKeyArgs,
    900                         null);
    901                 try {
    902                     while (upsyncs1.moveToNext()) {
    903                         // Load the remote store if it will be needed
    904                         if (remoteStore == null) {
    905                             remoteStore = Store.getInstance(account, context);
    906                         }
    907                         // Load the mailbox if it will be needed
    908                         if (mailbox == null) {
    909                             mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
    910                             if (mailbox == null) {
    911                                 continue; // Mailbox removed. Move to the next message.
    912                             }
    913                         }
    914                         // upsync the message
    915                         long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
    916                         lastMessageId = id;
    917                         processUploadMessage(context, remoteStore, mailbox, id, manualSync);
    918                     }
    919                 } finally {
    920                     if (upsyncs1 != null) {
    921                         upsyncs1.close();
    922                     }
    923                     if (remoteStore != null) {
    924                         remoteStore.closeConnections();
    925                     }
    926                 }
    927             }
    928         } catch (MessagingException me) {
    929             // Presumably an error here is an account connection failure, so there is
    930             // no point in continuing through the rest of the pending updates.
    931             if (DebugUtils.DEBUG) {
    932                 LogUtils.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
    933                         + lastMessageId + ": " + me);
    934             }
    935         } finally {
    936             if (mailboxes != null) {
    937                 mailboxes.close();
    938             }
    939         }
    940     }
    941 
    942     /**
    943      * Scan for messages that are in the Message_Updates table, look for differences that
    944      * we can deal with, and do the work.
    945      */
    946     private static void processPendingUpdatesSynchronous(Context context, Account account,
    947             Store remoteStore, String[] accountIdArgs) {
    948         ContentResolver resolver = context.getContentResolver();
    949         Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
    950                 EmailContent.Message.CONTENT_PROJECTION,
    951                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
    952                 EmailContent.MessageColumns.MAILBOX_KEY);
    953         long lastMessageId = -1;
    954         try {
    955             // Demand load mailbox (note order-by to reduce thrashing here)
    956             Mailbox mailbox = null;
    957             // loop through messages marked as needing updates
    958             while (updates.moveToNext()) {
    959                 boolean changeMoveToTrash = false;
    960                 boolean changeRead = false;
    961                 boolean changeFlagged = false;
    962                 boolean changeMailbox = false;
    963                 boolean changeAnswered = false;
    964 
    965                 EmailContent.Message oldMessage =
    966                         EmailContent.getContent(context, updates, EmailContent.Message.class);
    967                 lastMessageId = oldMessage.mId;
    968                 EmailContent.Message newMessage =
    969                         EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);
    970                 if (newMessage != null) {
    971                     mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey);
    972                     if (mailbox == null) {
    973                         continue; // Mailbox removed. Move to the next message.
    974                     }
    975                     if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
    976                         if (mailbox.mType == Mailbox.TYPE_TRASH) {
    977                             changeMoveToTrash = true;
    978                         } else {
    979                             changeMailbox = true;
    980                         }
    981                     }
    982                     changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
    983                     changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
    984                     changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) !=
    985                             (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO);
    986                 }
    987 
    988                 // Load the remote store if it will be needed
    989                 if (remoteStore == null &&
    990                         (changeMoveToTrash || changeRead || changeFlagged || changeMailbox ||
    991                                 changeAnswered)) {
    992                     remoteStore = Store.getInstance(account, context);
    993                 }
    994 
    995                 // Dispatch here for specific change types
    996                 if (changeMoveToTrash) {
    997                     // Move message to trash
    998                     processPendingMoveToTrash(context, remoteStore, mailbox, oldMessage,
    999                             newMessage);
   1000                 } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) {
   1001                     processPendingDataChange(context, remoteStore, mailbox, changeRead,
   1002                             changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage);
   1003                 }
   1004 
   1005                 // Finally, delete the update
   1006                 Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI,
   1007                         oldMessage.mId);
   1008                 resolver.delete(uri, null, null);
   1009             }
   1010 
   1011         } catch (MessagingException me) {
   1012             // Presumably an error here is an account connection failure, so there is
   1013             // no point in continuing through the rest of the pending updates.
   1014             if (DebugUtils.DEBUG) {
   1015                 LogUtils.d(Logging.LOG_TAG, "Unable to process pending update for id="
   1016                         + lastMessageId + ": " + me);
   1017             }
   1018         } finally {
   1019             updates.close();
   1020         }
   1021     }
   1022 
   1023     /**
   1024      * Upsync an entire message. This must also unwind whatever triggered it (either by
   1025      * updating the serverId, or by deleting the update record, or it's going to keep happening
   1026      * over and over again.
   1027      *
   1028      * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload.
   1029      * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select
   1030      * only the Drafts and Sent folders, this can happen when the update record and the current
   1031      * record mismatch. In this case, we let the update record remain, because the filters
   1032      * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it)
   1033      * appropriately.
   1034      *
   1035      * @param mailbox the actual mailbox
   1036      */
   1037     private static void processUploadMessage(Context context, Store remoteStore, Mailbox mailbox,
   1038             long messageId, boolean manualSync)
   1039             throws MessagingException {
   1040         EmailContent.Message newMessage =
   1041                 EmailContent.Message.restoreMessageWithId(context, messageId);
   1042         final boolean deleteUpdate;
   1043         if (newMessage == null) {
   1044             deleteUpdate = true;
   1045             LogUtils.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId);
   1046         } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
   1047             deleteUpdate = false;
   1048             LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId);
   1049         } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
   1050             deleteUpdate = false;
   1051             LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId);
   1052         } else if (mailbox.mType == Mailbox.TYPE_TRASH) {
   1053             deleteUpdate = false;
   1054             LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId);
   1055         } else if (newMessage.mMailboxKey != mailbox.mId) {
   1056             deleteUpdate = false;
   1057             LogUtils.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId);
   1058         } else {
   1059             LogUtils.d(Logging.LOG_TAG, "Upsync triggered for message id=" + messageId);
   1060             deleteUpdate =
   1061                     processPendingAppend(context, remoteStore, mailbox, newMessage, manualSync);
   1062         }
   1063         if (deleteUpdate) {
   1064             // Finally, delete the update (if any)
   1065             Uri uri = ContentUris.withAppendedId(
   1066                     EmailContent.Message.UPDATED_CONTENT_URI, messageId);
   1067             context.getContentResolver().delete(uri, null, null);
   1068         }
   1069     }
   1070 
   1071     /**
   1072      * Upsync changes to read, flagged, or mailbox
   1073      *
   1074      * @param remoteStore the remote store for this mailbox
   1075      * @param mailbox the mailbox the message is stored in
   1076      * @param changeRead whether the message's read state has changed
   1077      * @param changeFlagged whether the message's flagged state has changed
   1078      * @param changeMailbox whether the message's mailbox has changed
   1079      * @param oldMessage the message in it's pre-change state
   1080      * @param newMessage the current version of the message
   1081      */
   1082     private static void processPendingDataChange(final Context context, Store remoteStore,
   1083             Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox,
   1084             boolean changeAnswered, EmailContent.Message oldMessage,
   1085             final EmailContent.Message newMessage) throws MessagingException {
   1086         // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
   1087         // being moved
   1088         Mailbox newMailbox = mailbox;
   1089         // Mailbox is the original remote mailbox (the one we're acting on)
   1090         mailbox = getRemoteMailboxForMessage(context, oldMessage);
   1091 
   1092         // 0. No remote update if the message is local-only
   1093         if (newMessage.mServerId == null || newMessage.mServerId.equals("")
   1094                 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) {
   1095             return;
   1096         }
   1097 
   1098         // 1. No remote update for DRAFTS or OUTBOX
   1099         if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
   1100             return;
   1101         }
   1102 
   1103         // 2. Open the remote store & folder
   1104         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
   1105         if (!remoteFolder.exists()) {
   1106             return;
   1107         }
   1108         remoteFolder.open(OpenMode.READ_WRITE);
   1109         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
   1110             return;
   1111         }
   1112 
   1113         // 3. Finally, apply the changes to the message
   1114         Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId);
   1115         if (remoteMessage == null) {
   1116             return;
   1117         }
   1118         if (DebugUtils.DEBUG) {
   1119             LogUtils.d(Logging.LOG_TAG,
   1120                     "Update for msg id=" + newMessage.mId
   1121                     + " read=" + newMessage.mFlagRead
   1122                     + " flagged=" + newMessage.mFlagFavorite
   1123                     + " answered="
   1124                     + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0)
   1125                     + " new mailbox=" + newMessage.mMailboxKey);
   1126         }
   1127         Message[] messages = new Message[] { remoteMessage };
   1128         if (changeRead) {
   1129             remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead);
   1130         }
   1131         if (changeFlagged) {
   1132             remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite);
   1133         }
   1134         if (changeAnswered) {
   1135             remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED,
   1136                     (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0);
   1137         }
   1138         if (changeMailbox) {
   1139             Folder toFolder = remoteStore.getFolder(newMailbox.mServerId);
   1140             if (!remoteFolder.exists()) {
   1141                 return;
   1142             }
   1143             // We may need the message id to search for the message in the destination folder
   1144             remoteMessage.setMessageId(newMessage.mMessageId);
   1145             // Copy the message to its new folder
   1146             remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
   1147                 @Override
   1148                 public void onMessageUidChange(Message message, String newUid) {
   1149                     ContentValues cv = new ContentValues();
   1150                     cv.put(MessageColumns.SERVER_ID, newUid);
   1151                     // We only have one message, so, any updates _must_ be for it. Otherwise,
   1152                     // we'd have to cycle through to find the one with the same server ID.
   1153                     context.getContentResolver().update(ContentUris.withAppendedId(
   1154                             EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
   1155                 }
   1156 
   1157                 @Override
   1158                 public void onMessageNotFound(Message message) {
   1159                 }
   1160             });
   1161             // Delete the message from the remote source folder
   1162             remoteMessage.setFlag(Flag.DELETED, true);
   1163             remoteFolder.expunge();
   1164         }
   1165         remoteFolder.close(false);
   1166     }
   1167 
   1168     /**
   1169      * Process a pending trash message command.
   1170      *
   1171      * @param remoteStore the remote store we're working in
   1172      * @param newMailbox The local trash mailbox
   1173      * @param oldMessage The message copy that was saved in the updates shadow table
   1174      * @param newMessage The message that was moved to the mailbox
   1175      */
   1176     private static void processPendingMoveToTrash(final Context context, Store remoteStore,
   1177             Mailbox newMailbox, EmailContent.Message oldMessage,
   1178             final EmailContent.Message newMessage) throws MessagingException {
   1179 
   1180         // 0. No remote move if the message is local-only
   1181         if (newMessage.mServerId == null || newMessage.mServerId.equals("")
   1182                 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) {
   1183             return;
   1184         }
   1185 
   1186         // 1. Escape early if we can't find the local mailbox
   1187         // TODO smaller projection here
   1188         Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage);
   1189         if (oldMailbox == null) {
   1190             // can't find old mailbox, it may have been deleted.  just return.
   1191             return;
   1192         }
   1193         // 2. We don't support delete-from-trash here
   1194         if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
   1195             return;
   1196         }
   1197 
   1198         // The rest of this method handles server-side deletion
   1199 
   1200         // 4.  Find the remote mailbox (that we deleted from), and open it
   1201         Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId);
   1202         if (!remoteFolder.exists()) {
   1203             return;
   1204         }
   1205 
   1206         remoteFolder.open(OpenMode.READ_WRITE);
   1207         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
   1208             remoteFolder.close(false);
   1209             return;
   1210         }
   1211 
   1212         // 5. Find the remote original message
   1213         Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
   1214         if (remoteMessage == null) {
   1215             remoteFolder.close(false);
   1216             return;
   1217         }
   1218 
   1219         // 6. Find the remote trash folder, and create it if not found
   1220         Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId);
   1221         if (!remoteTrashFolder.exists()) {
   1222             /*
   1223              * If the remote trash folder doesn't exist we try to create it.
   1224              */
   1225             remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
   1226         }
   1227 
   1228         // 7. Try to copy the message into the remote trash folder
   1229         // Note, this entire section will be skipped for POP3 because there's no remote trash
   1230         if (remoteTrashFolder.exists()) {
   1231             /*
   1232              * Because remoteTrashFolder may be new, we need to explicitly open it
   1233              */
   1234             remoteTrashFolder.open(OpenMode.READ_WRITE);
   1235             if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
   1236                 remoteFolder.close(false);
   1237                 remoteTrashFolder.close(false);
   1238                 return;
   1239             }
   1240 
   1241             remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
   1242                     new Folder.MessageUpdateCallbacks() {
   1243                 @Override
   1244                 public void onMessageUidChange(Message message, String newUid) {
   1245                     // update the UID in the local trash folder, because some stores will
   1246                     // have to change it when copying to remoteTrashFolder
   1247                     ContentValues cv = new ContentValues();
   1248                     cv.put(MessageColumns.SERVER_ID, newUid);
   1249                     context.getContentResolver().update(newMessage.getUri(), cv, null, null);
   1250                 }
   1251 
   1252                 /**
   1253                  * This will be called if the deleted message doesn't exist and can't be
   1254                  * deleted (e.g. it was already deleted from the server.)  In this case,
   1255                  * attempt to delete the local copy as well.
   1256                  */
   1257                 @Override
   1258                 public void onMessageNotFound(Message message) {
   1259                     context.getContentResolver().delete(newMessage.getUri(), null, null);
   1260                 }
   1261             });
   1262             remoteTrashFolder.close(false);
   1263         }
   1264 
   1265         // 8. Delete the message from the remote source folder
   1266         remoteMessage.setFlag(Flag.DELETED, true);
   1267         remoteFolder.expunge();
   1268         remoteFolder.close(false);
   1269     }
   1270 
   1271     /**
   1272      * Process a pending trash message command.
   1273      *
   1274      * @param remoteStore the remote store we're working in
   1275      * @param oldMailbox The local trash mailbox
   1276      * @param oldMessage The message that was deleted from the trash
   1277      */
   1278     private static void processPendingDeleteFromTrash(Store remoteStore,
   1279             Mailbox oldMailbox, EmailContent.Message oldMessage)
   1280             throws MessagingException {
   1281 
   1282         // 1. We only support delete-from-trash here
   1283         if (oldMailbox.mType != Mailbox.TYPE_TRASH) {
   1284             return;
   1285         }
   1286 
   1287         // 2.  Find the remote trash folder (that we are deleting from), and open it
   1288         Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId);
   1289         if (!remoteTrashFolder.exists()) {
   1290             return;
   1291         }
   1292 
   1293         remoteTrashFolder.open(OpenMode.READ_WRITE);
   1294         if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
   1295             remoteTrashFolder.close(false);
   1296             return;
   1297         }
   1298 
   1299         // 3. Find the remote original message
   1300         Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId);
   1301         if (remoteMessage == null) {
   1302             remoteTrashFolder.close(false);
   1303             return;
   1304         }
   1305 
   1306         // 4. Delete the message from the remote trash folder
   1307         remoteMessage.setFlag(Flag.DELETED, true);
   1308         remoteTrashFolder.expunge();
   1309         remoteTrashFolder.close(false);
   1310     }
   1311 
   1312     /**
   1313      * Process a pending append message command. This command uploads a local message to the
   1314      * server, first checking to be sure that the server message is not newer than
   1315      * the local message.
   1316      *
   1317      * @param remoteStore the remote store we're working in
   1318      * @param mailbox The mailbox we're appending to
   1319      * @param message The message we're appending
   1320      * @param manualSync True if this is a manual sync (changes upsync behavior)
   1321      * @return true if successfully uploaded
   1322      */
   1323     private static boolean processPendingAppend(Context context, Store remoteStore, Mailbox mailbox,
   1324             EmailContent.Message message, boolean manualSync)
   1325             throws MessagingException {
   1326         boolean updateInternalDate = false;
   1327         boolean updateMessage = false;
   1328         boolean deleteMessage = false;
   1329 
   1330         // 1. Find the remote folder that we're appending to and create and/or open it
   1331         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
   1332         if (!remoteFolder.exists()) {
   1333             if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
   1334                 // This is a (hopefully) transient error and we return false to try again later
   1335                 return false;
   1336             }
   1337         }
   1338         remoteFolder.open(OpenMode.READ_WRITE);
   1339         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
   1340             return false;
   1341         }
   1342 
   1343         // 2. If possible, load a remote message with the matching UID
   1344         Message remoteMessage = null;
   1345         if (message.mServerId != null && message.mServerId.length() > 0) {
   1346             remoteMessage = remoteFolder.getMessage(message.mServerId);
   1347         }
   1348 
   1349         // 3. If a remote message could not be found, upload our local message
   1350         if (remoteMessage == null) {
   1351             // TODO:
   1352             // if we have a serverId and remoteMessage is still null, then probably the message
   1353             // has been deleted and we should delete locally.
   1354             // 3a. Create a legacy message to upload
   1355             Message localMessage = LegacyConversions.makeMessage(context, message);
   1356             // 3b. Upload it
   1357             //FetchProfile fp = new FetchProfile();
   1358             //fp.add(FetchProfile.Item.BODY);
   1359             // Note that this operation will assign the Uid to localMessage
   1360             remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */);
   1361 
   1362             // 3b. And record the UID from the server
   1363             message.mServerId = localMessage.getUid();
   1364             updateInternalDate = true;
   1365             updateMessage = true;
   1366         } else {
   1367             // 4. If the remote message exists we need to determine which copy to keep.
   1368             // TODO:
   1369             // I don't see a good reason we should be here. If the message already has a serverId,
   1370             // then we should be handling it in processPendingUpdates(),
   1371             // not processPendingUploads()
   1372             FetchProfile fp = new FetchProfile();
   1373             fp.add(FetchProfile.Item.ENVELOPE);
   1374             remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
   1375             Date localDate = new Date(message.mServerTimeStamp);
   1376             Date remoteDate = remoteMessage.getInternalDate();
   1377             if (remoteDate != null && remoteDate.compareTo(localDate) > 0) {
   1378                 // 4a. If the remote message is newer than ours we'll just
   1379                 // delete ours and move on. A sync will get the server message
   1380                 // if we need to be able to see it.
   1381                 deleteMessage = true;
   1382             } else {
   1383                 // 4b. Otherwise we'll upload our message and then delete the remote message.
   1384 
   1385                 // Create a legacy message to upload
   1386                 // TODO: This strategy has a problem: This will create a second message,
   1387                 // so that at least temporarily, we will have two messages for what the
   1388                 // user would think of as one.
   1389                 Message localMessage = LegacyConversions.makeMessage(context, message);
   1390 
   1391                 // 4c. Upload it
   1392                 fp.clear();
   1393                 fp = new FetchProfile();
   1394                 fp.add(FetchProfile.Item.BODY);
   1395                 remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */);
   1396 
   1397                 // 4d. Record the UID and new internalDate from the server
   1398                 message.mServerId = localMessage.getUid();
   1399                 updateInternalDate = true;
   1400                 updateMessage = true;
   1401 
   1402                 // 4e. And delete the old copy of the message from the server.
   1403                 remoteMessage.setFlag(Flag.DELETED, true);
   1404             }
   1405         }
   1406 
   1407         // 5. If requested, Best-effort to capture new "internaldate" from the server
   1408         if (updateInternalDate && message.mServerId != null) {
   1409             try {
   1410                 Message remoteMessage2 = remoteFolder.getMessage(message.mServerId);
   1411                 if (remoteMessage2 != null) {
   1412                     FetchProfile fp2 = new FetchProfile();
   1413                     fp2.add(FetchProfile.Item.ENVELOPE);
   1414                     remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null);
   1415                     final Date remoteDate = remoteMessage2.getInternalDate();
   1416                     if (remoteDate != null) {
   1417                         message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime();
   1418                         updateMessage = true;
   1419                     }
   1420                 }
   1421             } catch (MessagingException me) {
   1422                 // skip it - we can live without this
   1423             }
   1424         }
   1425 
   1426         // 6. Perform required edits to local copy of message
   1427         if (deleteMessage || updateMessage) {
   1428             Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
   1429             ContentResolver resolver = context.getContentResolver();
   1430             if (deleteMessage) {
   1431                 resolver.delete(uri, null, null);
   1432             } else if (updateMessage) {
   1433                 ContentValues cv = new ContentValues();
   1434                 cv.put(MessageColumns.SERVER_ID, message.mServerId);
   1435                 cv.put(MessageColumns.SERVER_TIMESTAMP, message.mServerTimeStamp);
   1436                 resolver.update(uri, cv, null, null);
   1437             }
   1438         }
   1439 
   1440         return true;
   1441     }
   1442 
   1443     /**
   1444      * A message and numeric uid that's easily sortable
   1445      */
   1446     private static class SortableMessage {
   1447         private final Message mMessage;
   1448         private final long mUid;
   1449 
   1450         SortableMessage(Message message, long uid) {
   1451             mMessage = message;
   1452             mUid = uid;
   1453         }
   1454     }
   1455 
   1456     private static int searchMailboxImpl(final Context context, final long accountId,
   1457             final SearchParams searchParams, final long destMailboxId) throws MessagingException {
   1458         final Account account = Account.restoreAccountWithId(context, accountId);
   1459         final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId);
   1460         final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
   1461         if (account == null || mailbox == null || destMailbox == null) {
   1462             LogUtils.d(Logging.LOG_TAG, "Attempted search for %s "
   1463                     + "but account or mailbox information was missing", searchParams);
   1464             return 0;
   1465         }
   1466 
   1467         // Tell UI that we're loading messages
   1468         final ContentValues statusValues = new ContentValues(2);
   1469         statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY);
   1470         destMailbox.update(context, statusValues);
   1471 
   1472         Store remoteStore = null;
   1473         int numSearchResults = 0;
   1474         try {
   1475             remoteStore = Store.getInstance(account, context);
   1476             final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
   1477             remoteFolder.open(OpenMode.READ_WRITE);
   1478 
   1479             SortableMessage[] sortableMessages = new SortableMessage[0];
   1480             if (searchParams.mOffset == 0) {
   1481                 // Get the "bare" messages (basically uid)
   1482                 final Message[] remoteMessages = remoteFolder.getMessages(searchParams, null);
   1483                 final int remoteCount = remoteMessages.length;
   1484                 if (remoteCount > 0) {
   1485                     sortableMessages = new SortableMessage[remoteCount];
   1486                     int i = 0;
   1487                     for (Message msg : remoteMessages) {
   1488                         sortableMessages[i++] = new SortableMessage(msg,
   1489                                 Long.parseLong(msg.getUid()));
   1490                     }
   1491                     // Sort the uid's, most recent first
   1492                     // Note: Not all servers will be nice and return results in the order of
   1493                     // request; those that do will see messages arrive from newest to oldest
   1494                     Arrays.sort(sortableMessages, new Comparator<SortableMessage>() {
   1495                         @Override
   1496                         public int compare(SortableMessage lhs, SortableMessage rhs) {
   1497                             return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
   1498                         }
   1499                     });
   1500                     sSearchResults.put(accountId, sortableMessages);
   1501                 }
   1502             } else {
   1503                 // It seems odd for this to happen, but if the previous query returned zero results,
   1504                 // but the UI somehow still attempted to load more, then sSearchResults will have
   1505                 // a null value for this account. We need to handle this below.
   1506                 sortableMessages = sSearchResults.get(accountId);
   1507             }
   1508 
   1509             numSearchResults = (sortableMessages != null ? sortableMessages.length : 0);
   1510             final int numToLoad =
   1511                     Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit);
   1512             destMailbox.updateMessageCount(context, numSearchResults);
   1513             if (numToLoad <= 0) {
   1514                 return 0;
   1515             }
   1516 
   1517             final ArrayList<Message> messageList = new ArrayList<>();
   1518             for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) {
   1519                 messageList.add(sortableMessages[i].mMessage);
   1520             }
   1521             // First fetch FLAGS and ENVELOPE. In a second pass, we'll fetch STRUCTURE and
   1522             // the first body part.
   1523             final FetchProfile fp = new FetchProfile();
   1524             fp.add(FetchProfile.Item.FLAGS);
   1525             fp.add(FetchProfile.Item.ENVELOPE);
   1526 
   1527             Message[] messageArray = messageList.toArray(new Message[messageList.size()]);
   1528 
   1529             // TODO: We are purposely processing messages with a MessageRetrievalListener here,
   1530             // rather than just walking the messageArray after the operation completes. This is so
   1531             // that we can immediately update the database so the user can see something useful
   1532             // happening, even if the message body has not yet been fetched.
   1533             // There are some issues with this approach:
   1534             // 1. It means that we have a single thread doing both network and database operations,
   1535             // and either can block the other. The database updates could slow down the network
   1536             // reads, keeping our network connection open longer than is really necessary.
   1537             // 2. We still load all of this data into messageArray, even though it's not used.
   1538             // It would be nicer if we had one thread doing the network operation, and a separate
   1539             // thread consuming that data and performing the appropriate database work, then
   1540             // discarding the data as soon as it is no longer needed. This would reduce our memory
   1541             // footprint and potentially allow our network operation to complete faster.
   1542             remoteFolder.fetch(messageArray, fp, new MessageRetrievalListener() {
   1543                 @Override
   1544                 public void messageRetrieved(Message message) {
   1545                     try {
   1546                         EmailContent.Message localMessage = new EmailContent.Message();
   1547 
   1548                         // Copy the fields that are available into the message
   1549                         LegacyConversions.updateMessageFields(localMessage,
   1550                                 message, account.mId, mailbox.mId);
   1551                         // Save off the mailbox that this message *really* belongs in.
   1552                         // We need this information if we need to do more lookups
   1553                         // (like loading attachments) for this message. See b/11294681
   1554                         localMessage.mMainMailboxKey = localMessage.mMailboxKey;
   1555                         localMessage.mMailboxKey = destMailboxId;
   1556                         // We load 50k or so; maybe it's complete, maybe not...
   1557                         int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
   1558                         // We store the serverId of the source mailbox into protocolSearchInfo
   1559                         // This will be used by loadMessageForView, etc. to use the proper remote
   1560                         // folder
   1561                         localMessage.mProtocolSearchInfo = mailbox.mServerId;
   1562                         // Commit the message to the local store
   1563                         Utilities.saveOrUpdate(localMessage, context);
   1564                     } catch (MessagingException me) {
   1565                         LogUtils.e(Logging.LOG_TAG, me,
   1566                                 "Error while copying downloaded message.");
   1567                     } catch (Exception e) {
   1568                         LogUtils.e(Logging.LOG_TAG, e,
   1569                                 "Error while storing downloaded message.");
   1570                     }
   1571                 }
   1572 
   1573                 @Override
   1574                 public void loadAttachmentProgress(int progress) {
   1575                 }
   1576             });
   1577 
   1578             // Now load the structure for all of the messages:
   1579             fp.clear();
   1580             fp.add(FetchProfile.Item.STRUCTURE);
   1581             remoteFolder.fetch(messageArray, fp, null);
   1582 
   1583             // Finally, load the first body part (i.e. message text).
   1584             // This means attachment contents are not yet loaded, but that's okay,
   1585             // we'll load them as needed, same as in synced messages.
   1586             Message[] oneMessageArray = new Message[1];
   1587             for (Message message : messageArray) {
   1588                 // Build a list of parts we are interested in. Text parts will be downloaded
   1589                 // right now, attachments will be left for later.
   1590                 ArrayList<Part> viewables = new ArrayList<>();
   1591                 ArrayList<Part> attachments = new ArrayList<>();
   1592                 MimeUtility.collectParts(message, viewables, attachments);
   1593                 // Download the viewables immediately
   1594                 oneMessageArray[0] = message;
   1595                 for (Part part : viewables) {
   1596                     fp.clear();
   1597                     fp.add(part);
   1598                     remoteFolder.fetch(oneMessageArray, fp, null);
   1599                 }
   1600                 // Store the updated message locally and mark it fully loaded
   1601                 Utilities.copyOneMessageToProvider(context, message, account, destMailbox,
   1602                         EmailContent.Message.FLAG_LOADED_COMPLETE);
   1603             }
   1604 
   1605         } finally {
   1606             if (remoteStore != null) {
   1607                 remoteStore.closeConnections();
   1608             }
   1609             // Tell UI that we're done loading messages
   1610             statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
   1611             statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
   1612             destMailbox.update(context, statusValues);
   1613         }
   1614 
   1615         return numSearchResults;
   1616     }
   1617 }
   1618