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