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