Home | History | Annotate | Download | only in activity
      1 /*
      2  * Copyright (C) 2010 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.activity;
     18 
     19 import android.app.Activity;
     20 import android.app.ListFragment;
     21 import android.app.LoaderManager;
     22 import android.content.ClipData;
     23 import android.content.ContentUris;
     24 import android.content.Context;
     25 import android.content.Loader;
     26 import android.content.res.Configuration;
     27 import android.content.res.Resources;
     28 import android.database.Cursor;
     29 import android.graphics.Canvas;
     30 import android.graphics.Point;
     31 import android.graphics.PointF;
     32 import android.graphics.Rect;
     33 import android.graphics.Typeface;
     34 import android.graphics.drawable.Drawable;
     35 import android.os.Bundle;
     36 import android.os.Parcelable;
     37 import android.text.TextPaint;
     38 import android.util.Log;
     39 import android.view.ActionMode;
     40 import android.view.DragEvent;
     41 import android.view.LayoutInflater;
     42 import android.view.Menu;
     43 import android.view.MenuInflater;
     44 import android.view.MenuItem;
     45 import android.view.MotionEvent;
     46 import android.view.View;
     47 import android.view.View.DragShadowBuilder;
     48 import android.view.View.OnDragListener;
     49 import android.view.View.OnTouchListener;
     50 import android.view.ViewGroup;
     51 import android.widget.AdapterView;
     52 import android.widget.AdapterView.OnItemLongClickListener;
     53 import android.widget.ListView;
     54 import android.widget.TextView;
     55 import android.widget.Toast;
     56 
     57 import com.android.email.Controller;
     58 import com.android.email.Email;
     59 import com.android.email.MessageListContext;
     60 import com.android.email.NotificationController;
     61 import com.android.email.R;
     62 import com.android.email.RefreshManager;
     63 import com.android.email.activity.MessagesAdapter.SearchResultsCursor;
     64 import com.android.email.provider.EmailProvider;
     65 import com.android.emailcommon.Logging;
     66 import com.android.emailcommon.provider.Account;
     67 import com.android.emailcommon.provider.EmailContent.Message;
     68 import com.android.emailcommon.provider.Mailbox;
     69 import com.android.emailcommon.utility.EmailAsyncTask;
     70 import com.android.emailcommon.utility.Utility;
     71 import com.google.common.annotations.VisibleForTesting;
     72 import com.google.common.collect.Maps;
     73 
     74 import java.util.HashMap;
     75 import java.util.Set;
     76 
     77 /**
     78  * Message list.
     79  *
     80  * See the class javadoc for {@link MailboxListFragment} for notes on {@link #getListView()} and
     81  * {@link #isViewCreated()}.
     82  */
     83 public class MessageListFragment extends ListFragment
     84         implements OnItemLongClickListener, MessagesAdapter.Callback,
     85         MoveMessageToDialog.Callback, OnDragListener, OnTouchListener {
     86     private static final String BUNDLE_LIST_STATE = "MessageListFragment.state.listState";
     87     private static final String BUNDLE_KEY_SELECTED_MESSAGE_ID
     88             = "messageListFragment.state.listState.selected_message_id";
     89 
     90     private static final int LOADER_ID_MESSAGES_LOADER = 1;
     91 
     92     /** Argument name(s) */
     93     private static final String ARG_LIST_CONTEXT = "listContext";
     94 
     95     // Controller access
     96     private Controller mController;
     97     private RefreshManager mRefreshManager;
     98     private final RefreshListener mRefreshListener = new RefreshListener();
     99 
    100     // UI Support
    101     private Activity mActivity;
    102     private Callback mCallback = EmptyCallback.INSTANCE;
    103     private boolean mIsViewCreated;
    104 
    105     private View mListPanel;
    106     private View mListFooterView;
    107     private TextView mListFooterText;
    108     private View mListFooterProgress;
    109     private ViewGroup mSearchHeader;
    110     private ViewGroup mWarningContainer;
    111     private TextView mSearchHeaderText;
    112     private TextView mSearchHeaderCount;
    113 
    114     private static final int LIST_FOOTER_MODE_NONE = 0;
    115     private static final int LIST_FOOTER_MODE_MORE = 1;
    116     private int mListFooterMode;
    117 
    118     private MessagesAdapter mListAdapter;
    119     private boolean mIsFirstLoad;
    120 
    121     /** ID of the message to hightlight. */
    122     private long mSelectedMessageId = -1;
    123 
    124     private Account mAccount;
    125     private Mailbox mMailbox;
    126     /** The original mailbox being searched, if this list is showing search results. */
    127     private Mailbox mSearchedMailbox;
    128     private boolean mIsEasAccount;
    129     private boolean mIsRefreshable;
    130     private int mCountTotalAccounts;
    131 
    132     // Misc members
    133 
    134     private boolean mShowSendCommand;
    135     private boolean mShowMoveCommand;
    136 
    137     /**
    138      * If true, we disable the CAB even if there are selected messages.
    139      * It's used in portrait on the tablet when the message view becomes visible and the message
    140      * list gets pushed out of the screen, in which case we want to keep the selection but the CAB
    141      * should be gone.
    142      */
    143     private boolean mDisableCab;
    144 
    145     /** true between {@link #onResume} and {@link #onPause}. */
    146     private boolean mResumed;
    147 
    148     /**
    149      * {@link ActionMode} shown when 1 or more message is selected.
    150      */
    151     private ActionMode mSelectionMode;
    152     private SelectionModeCallback mLastSelectionModeCallback;
    153 
    154     private Parcelable mSavedListState;
    155 
    156     private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
    157 
    158     /**
    159      * Callback interface that owning activities must implement
    160      */
    161     public interface Callback {
    162         public static final int TYPE_REGULAR = 0;
    163         public static final int TYPE_DRAFT = 1;
    164         public static final int TYPE_TRASH = 2;
    165 
    166         /**
    167          * Called when the specified mailbox does not exist.
    168          */
    169         public void onMailboxNotFound(boolean firstLoad);
    170 
    171         /**
    172          * Called when the user wants to open a message.
    173          * Note {@code mailboxId} is of the actual mailbox of the message, which is different from
    174          * {@link MessageListFragment#getMailboxId} if it's magic mailboxes.
    175          *
    176          * @param messageId the message ID of the message
    177          * @param messageMailboxId the mailbox ID of the message.
    178          *     This will never take values like {@link Mailbox#QUERY_ALL_INBOXES}.
    179          * @param listMailboxId the mailbox ID of the listbox shown on this fragment.
    180          *     This can be that of a magic mailbox, e.g.  {@link Mailbox#QUERY_ALL_INBOXES}.
    181          * @param type {@link #TYPE_REGULAR}, {@link #TYPE_DRAFT} or {@link #TYPE_TRASH}.
    182          */
    183         public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId,
    184                 int type);
    185 
    186         /**
    187          * Called when an operation is initiated that can potentially advance the current
    188          * message selection (e.g. a delete operation may advance the selection).
    189          * @param affectedMessages the messages the operation will apply to
    190          */
    191         public void onAdvancingOpAccepted(Set<Long> affectedMessages);
    192 
    193         /**
    194          * Called when a drag & drop is initiated.
    195          *
    196          * @return true if drag & drop is allowed
    197          */
    198         public boolean onDragStarted();
    199 
    200         /**
    201          * Called when a drag & drop is ended.
    202          */
    203         public void onDragEnded();
    204     }
    205 
    206     private static final class EmptyCallback implements Callback {
    207         public static final Callback INSTANCE = new EmptyCallback();
    208 
    209         @Override
    210         public void onMailboxNotFound(boolean isFirstLoad) {
    211         }
    212 
    213         @Override
    214         public void onMessageOpen(
    215                 long messageId, long messageMailboxId, long listMailboxId, int type) {
    216         }
    217 
    218         @Override
    219         public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
    220         }
    221 
    222         @Override
    223         public boolean onDragStarted() {
    224             return false; // We don't know -- err on the safe side.
    225         }
    226 
    227         @Override
    228         public void onDragEnded() {
    229         }
    230     }
    231 
    232     /**
    233      * Create a new instance with initialization parameters.
    234      *
    235      * This fragment should be created only with this method.  (Arguments should always be set.)
    236      *
    237      * @param listContext The list context to show messages for
    238      */
    239     public static MessageListFragment newInstance(MessageListContext listContext) {
    240         final MessageListFragment instance = new MessageListFragment();
    241         final Bundle args = new Bundle();
    242         args.putParcelable(ARG_LIST_CONTEXT, listContext);
    243         instance.setArguments(args);
    244         return instance;
    245     }
    246 
    247     /**
    248      * The context describing the contents to be shown in the list.
    249      * Do not use directly; instead, use the getters such as {@link #getAccountId()}.
    250      * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
    251      * constructs, this <em>must</em> be considered immutable.
    252      */
    253     private MessageListContext mListContext;
    254 
    255     private void initializeArgCache() {
    256         if (mListContext != null) return;
    257         mListContext = getArguments().getParcelable(ARG_LIST_CONTEXT);
    258     }
    259 
    260     /**
    261      * @return the account ID passed to {@link #newInstance}.  Safe to call even before onCreate.
    262      *
    263      * NOTE it may return {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
    264      */
    265     public long getAccountId() {
    266         initializeArgCache();
    267         return mListContext.mAccountId;
    268     }
    269 
    270     /**
    271      * @return the mailbox ID passed to {@link #newInstance}.  Safe to call even before onCreate.
    272      */
    273     public long getMailboxId() {
    274         initializeArgCache();
    275         return mListContext.getMailboxId();
    276     }
    277 
    278     /**
    279      * @return true if the mailbox is a combined mailbox.  Safe to call even before onCreate.
    280      */
    281     public boolean isCombinedMailbox() {
    282         return getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW;
    283     }
    284 
    285     public MessageListContext getListContext() {
    286         initializeArgCache();
    287         return mListContext;
    288     }
    289 
    290     /**
    291      * @return Whether or not initial data is loaded in this list.
    292      */
    293     public boolean hasDataLoaded() {
    294         return mCountTotalAccounts > 0;
    295     }
    296 
    297     /**
    298      * @return The account object, when known. Null if not yet known.
    299      */
    300     public Account getAccount() {
    301         return mAccount;
    302     }
    303 
    304     /**
    305      * @return The mailbox where the messages belong in, when known. Null if not yet known.
    306      */
    307     public Mailbox getMailbox() {
    308         return mMailbox;
    309     }
    310 
    311     /**
    312      * @return Whether or not this message list is showing a user's inbox.
    313      *     Note that combined inbox view is treated as an inbox view.
    314      */
    315     public boolean isInboxList() {
    316         MessageListContext listContext = getListContext();
    317         long accountId = listContext.mAccountId;
    318         if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
    319             return listContext.getMailboxId() == Mailbox.QUERY_ALL_INBOXES;
    320         }
    321 
    322         if (!hasDataLoaded()) {
    323             // If the data hasn't finished loading, we don't have the full mailbox - infer from ID.
    324             long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
    325             return listContext.getMailboxId() == inboxId;
    326         }
    327         return (mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_INBOX);
    328     }
    329 
    330     /**
    331      * @return The mailbox being searched, when known. Null if not yet known or if not a search
    332      *    result.
    333      */
    334     public Mailbox getSearchedMailbox() {
    335         return mSearchedMailbox;
    336     }
    337 
    338     @Override
    339     public void onAttach(Activity activity) {
    340         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    341             Log.d(Logging.LOG_TAG, this + " onAttach");
    342         }
    343         super.onAttach(activity);
    344     }
    345 
    346     @Override
    347     public void onCreate(Bundle savedInstanceState) {
    348         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    349             Log.d(Logging.LOG_TAG, this + " onCreate");
    350         }
    351         super.onCreate(savedInstanceState);
    352 
    353         mActivity = getActivity();
    354         setHasOptionsMenu(true);
    355         mController = Controller.getInstance(mActivity);
    356         mRefreshManager = RefreshManager.getInstance(mActivity);
    357 
    358         mListAdapter = new MessagesAdapter(mActivity, this);
    359         mIsFirstLoad = true;
    360     }
    361 
    362     @Override
    363     public View onCreateView(
    364             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    365         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    366             Log.d(Logging.LOG_TAG, this + " onCreateView");
    367         }
    368         // Use a custom layout, which includes the original layout with "send messages" panel.
    369         View root = inflater.inflate(R.layout.message_list_fragment,null);
    370         mIsViewCreated = true;
    371         mListPanel = UiUtilities.getView(root, R.id.list_panel);
    372         return root;
    373     }
    374 
    375     public void setLayout(ThreePaneLayout layout) {
    376         if (UiUtilities.useTwoPane(mActivity)) {
    377             mListAdapter.setLayout(layout);
    378         }
    379     }
    380 
    381     private void initSearchHeader() {
    382         if (mSearchHeader == null) {
    383             ViewGroup root = (ViewGroup) getView();
    384             mSearchHeader = (ViewGroup) LayoutInflater.from(mActivity).inflate(
    385                     R.layout.message_list_search_header, root, false);
    386             mSearchHeaderText = UiUtilities.getView(mSearchHeader, R.id.search_header_text);
    387             mSearchHeaderCount = UiUtilities.getView(mSearchHeader, R.id.search_count);
    388 
    389             // Add above the actual list.
    390             root.addView(mSearchHeader, 0);
    391         }
    392     }
    393 
    394     /**
    395      * @return true if the content view is created and not destroyed yet. (i.e. between
    396      * {@link #onCreateView} and {@link #onDestroyView}.
    397      */
    398     private boolean isViewCreated() {
    399         // Note that we don't use "getView() != null".  This method is used in updateSelectionMode()
    400         // to determine if CAB shold be shown.  But because it's called from onDestroyView(), at
    401         // this point the fragment still has views but we want to hide CAB, we can't use
    402         // getView() here.
    403         return mIsViewCreated;
    404     }
    405 
    406     @Override
    407     public void onActivityCreated(Bundle savedInstanceState) {
    408         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    409             Log.d(Logging.LOG_TAG, this + " onActivityCreated");
    410         }
    411         super.onActivityCreated(savedInstanceState);
    412 
    413         final ListView lv = getListView();
    414         lv.setOnItemLongClickListener(this);
    415         lv.setOnTouchListener(this);
    416         lv.setItemsCanFocus(false);
    417         lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    418 
    419         mListFooterView = getActivity().getLayoutInflater().inflate(
    420                 R.layout.message_list_item_footer, lv, false);
    421         setEmptyText(getString(R.string.message_list_no_messages));
    422 
    423         if (savedInstanceState != null) {
    424             // Fragment doesn't have this method.  Call it manually.
    425             restoreInstanceState(savedInstanceState);
    426         }
    427 
    428         startLoading();
    429 
    430         UiUtilities.installFragment(this);
    431     }
    432 
    433     @Override
    434     public void onStart() {
    435         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    436             Log.d(Logging.LOG_TAG, this + " onStart");
    437         }
    438         super.onStart();
    439     }
    440 
    441     @Override
    442     public void onResume() {
    443         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    444             Log.d(Logging.LOG_TAG, this + " onResume");
    445         }
    446         super.onResume();
    447         adjustMessageNotification(false);
    448         mRefreshManager.registerListener(mRefreshListener);
    449         mResumed = true;
    450     }
    451 
    452     @Override
    453     public void onPause() {
    454         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    455             Log.d(Logging.LOG_TAG, this + " onPause");
    456         }
    457         mResumed = false;
    458         mSavedListState = getListView().onSaveInstanceState();
    459         adjustMessageNotification(true);
    460         super.onPause();
    461     }
    462 
    463     @Override
    464     public void onStop() {
    465         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    466             Log.d(Logging.LOG_TAG, this + " onStop");
    467         }
    468         mTaskTracker.cancellAllInterrupt();
    469         mRefreshManager.unregisterListener(mRefreshListener);
    470 
    471         super.onStop();
    472     }
    473 
    474     @Override
    475     public void onDestroyView() {
    476         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    477             Log.d(Logging.LOG_TAG, this + " onDestroyView");
    478         }
    479         mIsViewCreated = false; // Clear this first for updateSelectionMode(). See isViewCreated().
    480         UiUtilities.uninstallFragment(this);
    481         updateSelectionMode();
    482 
    483         // Reset the footer mode since we just blew away the footer view we were holding on to.
    484         // This will get re-updated when/if this fragment is restored.
    485         mListFooterMode = LIST_FOOTER_MODE_NONE;
    486         super.onDestroyView();
    487     }
    488 
    489     @Override
    490     public void onDestroy() {
    491         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    492             Log.d(Logging.LOG_TAG, this + " onDestroy");
    493         }
    494 
    495         finishSelectionMode();
    496         super.onDestroy();
    497     }
    498 
    499     @Override
    500     public void onDetach() {
    501         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    502             Log.d(Logging.LOG_TAG, this + " onDetach");
    503         }
    504         super.onDetach();
    505     }
    506 
    507     @Override
    508     public void onSaveInstanceState(Bundle outState) {
    509         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    510             Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
    511         }
    512         super.onSaveInstanceState(outState);
    513         mListAdapter.onSaveInstanceState(outState);
    514         if (isViewCreated()) {
    515             outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState());
    516         }
    517         outState.putLong(BUNDLE_KEY_SELECTED_MESSAGE_ID, mSelectedMessageId);
    518     }
    519 
    520     @VisibleForTesting
    521     void restoreInstanceState(Bundle savedInstanceState) {
    522         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    523             Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
    524         }
    525         mListAdapter.loadState(savedInstanceState);
    526         mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
    527         mSelectedMessageId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MESSAGE_ID);
    528     }
    529 
    530     @Override
    531     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    532         inflater.inflate(R.menu.message_list_fragment_option, menu);
    533     }
    534 
    535     @Override
    536     public void onPrepareOptionsMenu(Menu menu) {
    537         menu.findItem(R.id.send).setVisible(mShowSendCommand);
    538     }
    539 
    540     @Override
    541     public boolean onOptionsItemSelected(MenuItem item) {
    542         switch (item.getItemId()) {
    543             case R.id.send:
    544                 onSendPendingMessages();
    545                 return true;
    546 
    547         }
    548         return false;
    549     }
    550 
    551     public void setCallback(Callback callback) {
    552         mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE;
    553     }
    554 
    555     /**
    556      * This method must be called when the fragment is hidden/shown.
    557      */
    558     public void onHidden(boolean hidden) {
    559         // When hidden, we need to disable CAB.
    560         if (hidden == mDisableCab) {
    561             return;
    562         }
    563         mDisableCab = hidden;
    564         updateSelectionMode();
    565     }
    566 
    567     public void setSelectedMessage(long messageId) {
    568         if (mSelectedMessageId == messageId) {
    569             return;
    570         }
    571         mSelectedMessageId = messageId;
    572         if (mResumed) {
    573             highlightSelectedMessage(true);
    574         }
    575     }
    576 
    577     /**
    578      * @return true if the mailbox is refreshable.  false otherwise, or unknown yet.
    579      */
    580     public boolean isRefreshable() {
    581         return mIsRefreshable;
    582     }
    583 
    584     /**
    585      * @return the number of messages that are currently selected.
    586      */
    587     private int getSelectedCount() {
    588         return mListAdapter.getSelectedSet().size();
    589     }
    590 
    591     /**
    592      * @return true if the list is in the "selection" mode.
    593      */
    594     public boolean isInSelectionMode() {
    595         return mSelectionMode != null;
    596     }
    597 
    598     /**
    599      * Called when a message is clicked.
    600      */
    601     @Override
    602     public void onListItemClick(ListView parent, View view, int position, long id) {
    603         if (view != mListFooterView) {
    604             MessageListItem itemView = (MessageListItem) view;
    605             onMessageOpen(itemView.mMailboxId, id);
    606         } else {
    607             doFooterClick();
    608         }
    609     }
    610 
    611     // This is tentative drag & drop UI
    612     private static class ShadowBuilder extends DragShadowBuilder {
    613         private static Drawable sBackground;
    614         /** Paint information for the move message text */
    615         private static TextPaint sMessagePaint;
    616         /** Paint information for the message count */
    617         private static TextPaint sCountPaint;
    618         /** The x location of any touch event; used to ensure the drag overlay is drawn correctly */
    619         private static int sTouchX;
    620 
    621         /** Width of the draggable view */
    622         private final int mDragWidth;
    623         /** Height of the draggable view */
    624         private final int mDragHeight;
    625 
    626         private final String mMessageText;
    627         private final PointF mMessagePoint;
    628 
    629         private final String mCountText;
    630         private final PointF mCountPoint;
    631         private int mOldOrientation = Configuration.ORIENTATION_UNDEFINED;
    632 
    633         /** Margin applied to the right of count text */
    634         private static float sCountMargin;
    635         /** Margin applied to left of the message text */
    636         private static float sMessageMargin;
    637         /** Vertical offset of the drag view */
    638         private static int sDragOffset;
    639 
    640         public ShadowBuilder(View view, int count) {
    641             super(view);
    642             Resources res = view.getResources();
    643             int newOrientation = res.getConfiguration().orientation;
    644 
    645             mDragHeight = view.getHeight();
    646             mDragWidth = view.getWidth();
    647 
    648             // TODO: Can we define a layout for the contents of the drag area?
    649             if (sBackground == null || mOldOrientation != newOrientation) {
    650                 mOldOrientation = newOrientation;
    651 
    652                 sBackground = res.getDrawable(R.drawable.list_pressed_holo);
    653                 sBackground.setBounds(0, 0, mDragWidth, mDragHeight);
    654 
    655                 sDragOffset = (int)res.getDimension(R.dimen.message_list_drag_offset);
    656 
    657                 sMessagePaint = new TextPaint();
    658                 float messageTextSize;
    659                 messageTextSize = res.getDimension(R.dimen.message_list_drag_message_font_size);
    660                 sMessagePaint.setTextSize(messageTextSize);
    661                 sMessagePaint.setTypeface(Typeface.DEFAULT_BOLD);
    662                 sMessagePaint.setAntiAlias(true);
    663                 sMessageMargin = res.getDimension(R.dimen.message_list_drag_message_right_margin);
    664 
    665                 sCountPaint = new TextPaint();
    666                 float countTextSize;
    667                 countTextSize = res.getDimension(R.dimen.message_list_drag_count_font_size);
    668                 sCountPaint.setTextSize(countTextSize);
    669                 sCountPaint.setTypeface(Typeface.DEFAULT_BOLD);
    670                 sCountPaint.setAntiAlias(true);
    671                 sCountMargin = res.getDimension(R.dimen.message_list_drag_count_left_margin);
    672             }
    673 
    674             // Calculate layout positions
    675             Rect b = new Rect();
    676 
    677             mMessageText = res.getQuantityString(R.plurals.move_messages, count, count);
    678             sMessagePaint.getTextBounds(mMessageText, 0, mMessageText.length(), b);
    679             mMessagePoint = new PointF(mDragWidth - b.right - sMessageMargin,
    680                     (mDragHeight - b.top)/ 2);
    681 
    682             mCountText = Integer.toString(count);
    683             sCountPaint.getTextBounds(mCountText, 0, mCountText.length(), b);
    684             mCountPoint = new PointF(sCountMargin,
    685                     (mDragHeight - b.top) / 2);
    686         }
    687 
    688         @Override
    689         public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
    690             shadowSize.set(mDragWidth, mDragHeight);
    691             shadowTouchPoint.set(sTouchX, (mDragHeight / 2) + sDragOffset);
    692         }
    693 
    694         @Override
    695         public void onDrawShadow(Canvas canvas) {
    696             super.onDrawShadow(canvas);
    697             sBackground.draw(canvas);
    698             canvas.drawText(mMessageText, mMessagePoint.x, mMessagePoint.y, sMessagePaint);
    699             canvas.drawText(mCountText, mCountPoint.x, mCountPoint.y, sCountPaint);
    700         }
    701     }
    702 
    703     @Override
    704     public boolean onDrag(View view, DragEvent event) {
    705         switch(event.getAction()) {
    706             case DragEvent.ACTION_DRAG_ENDED:
    707                 if (event.getResult()) {
    708                     onDeselectAll(); // Clear the selection
    709                 }
    710                 mCallback.onDragEnded();
    711                 break;
    712         }
    713         return false;
    714     }
    715 
    716     @Override
    717     public boolean onTouch(View v, MotionEvent event) {
    718         if (event.getAction() == MotionEvent.ACTION_DOWN) {
    719             // Save the touch location to draw the drag overlay at the correct location
    720             ShadowBuilder.sTouchX = (int)event.getX();
    721         }
    722         // don't do anything, let the system process the event
    723         return false;
    724     }
    725 
    726     @Override
    727     public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
    728         if (view != mListFooterView) {
    729             // Always toggle the item.
    730             MessageListItem listItem = (MessageListItem) view;
    731             boolean toggled = false;
    732             if (!mListAdapter.isSelected(listItem)) {
    733                 toggleSelection(listItem);
    734                 toggled = true;
    735             }
    736 
    737             // Additionally, check to see if we can drag the item.
    738             if (!mCallback.onDragStarted()) {
    739                 return toggled; // D&D not allowed.
    740             }
    741             // We can't move from combined accounts view
    742             // We also need to check the actual mailbox to see if we can move items from it
    743             final long mailboxId = getMailboxId();
    744             if (mAccount == null || mMailbox == null) {
    745                 return false;
    746             } else if (mailboxId > 0 && !mMailbox.canHaveMessagesMoved()) {
    747                 return false;
    748             }
    749             // Start drag&drop.
    750 
    751             // Create ClipData with the Uri of the message we're long clicking
    752             ClipData data = ClipData.newUri(mActivity.getContentResolver(),
    753                     MessageListItem.MESSAGE_LIST_ITEMS_CLIP_LABEL, Message.CONTENT_URI.buildUpon()
    754                     .appendPath(Long.toString(listItem.mMessageId))
    755                     .appendQueryParameter(
    756                             EmailProvider.MESSAGE_URI_PARAMETER_MAILBOX_ID,
    757                             Long.toString(mailboxId))
    758                             .build());
    759             Set<Long> selectedMessageIds = mListAdapter.getSelectedSet();
    760             int size = selectedMessageIds.size();
    761             // Add additional Uri's for any other selected messages
    762             for (Long messageId: selectedMessageIds) {
    763                 if (messageId.longValue() != listItem.mMessageId) {
    764                     data.addItem(new ClipData.Item(
    765                             ContentUris.withAppendedId(Message.CONTENT_URI, messageId)));
    766                 }
    767             }
    768             // Start dragging now
    769             listItem.setOnDragListener(this);
    770             listItem.startDrag(data, new ShadowBuilder(listItem, size), null, 0);
    771             return true;
    772         }
    773         return false;
    774     }
    775 
    776     private void toggleSelection(MessageListItem itemView) {
    777         itemView.invalidate();
    778         mListAdapter.toggleSelected(itemView);
    779     }
    780 
    781     /**
    782      * Called when a message on the list is selected
    783      *
    784      * @param messageMailboxId the actual mailbox ID of the message.  Note it's different than
    785      *        what is returned by {@link #getMailboxId()} for combined mailboxes.
    786      *        ({@link #getMailboxId()} may return special mailbox values such as
    787      *        {@link Mailbox#QUERY_ALL_INBOXES})
    788      * @param messageId ID of the message to open.
    789      */
    790     private void onMessageOpen(final long messageMailboxId, final long messageId) {
    791         if ((mMailbox != null) && (mMailbox.mId == messageMailboxId)) {
    792             // Normal case - the message belongs in the mailbox list we're viewing.
    793             mCallback.onMessageOpen(messageId, messageMailboxId,
    794                     getMailboxId(), callbackTypeForMailboxType(mMailbox.mType));
    795             return;
    796         }
    797 
    798         // Weird case - a virtual mailbox where the messages could come from different mailbox
    799         // types - here we have to query the DB for the type.
    800         new MessageOpenTask(messageMailboxId, messageId).cancelPreviousAndExecuteParallel();
    801     }
    802 
    803     private int callbackTypeForMailboxType(int mailboxType) {
    804         switch (mailboxType) {
    805             case Mailbox.TYPE_DRAFTS:
    806                 return Callback.TYPE_DRAFT;
    807             case Mailbox.TYPE_TRASH:
    808                 return Callback.TYPE_TRASH;
    809             default:
    810                 return Callback.TYPE_REGULAR;
    811         }
    812     }
    813 
    814     /**
    815      * Task to look up the mailbox type for a message, and kicks the callback.
    816      */
    817     private class MessageOpenTask extends EmailAsyncTask<Void, Void, Integer> {
    818         private final long mMessageMailboxId;
    819         private final long mMessageId;
    820 
    821         public MessageOpenTask(long messageMailboxId, long messageId) {
    822             super(mTaskTracker);
    823             mMessageMailboxId = messageMailboxId;
    824             mMessageId = messageId;
    825         }
    826 
    827         @Override
    828         protected Integer doInBackground(Void... params) {
    829             // Restore the mailbox type.  Note we can't use mMailbox.mType here, because
    830             // we don't have mMailbox for combined mailbox.
    831             // ("All Starred" can contain any kind of messages.)
    832             return callbackTypeForMailboxType(
    833                     Mailbox.getMailboxType(mActivity, mMessageMailboxId));
    834         }
    835 
    836         @Override
    837         protected void onSuccess(Integer type) {
    838             if (type == null) {
    839                 return;
    840             }
    841             mCallback.onMessageOpen(mMessageId, mMessageMailboxId, getMailboxId(), type);
    842         }
    843     }
    844 
    845     private void showMoveMessagesDialog(Set<Long> selectedSet) {
    846         long[] messageIds = Utility.toPrimitiveLongArray(selectedSet);
    847         MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(messageIds, this);
    848         dialog.show(getFragmentManager(), "dialog");
    849     }
    850 
    851     @Override
    852     public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
    853         final Context context = getActivity();
    854         if (context == null) {
    855             // Detached from activity. This callback was really delayed or a monkey was involved.
    856             return;
    857         }
    858 
    859         mCallback.onAdvancingOpAccepted(Utility.toLongSet(messageIds));
    860         ActivityHelper.moveMessages(context, newMailboxId, messageIds);
    861 
    862         // Move is async, so we can't refresh now.  Instead, just clear the selection.
    863         onDeselectAll();
    864     }
    865 
    866     /**
    867      * Refresh the list.  NOOP for special mailboxes (e.g. combined inbox).
    868      *
    869      * Note: Manual refresh is enabled even for push accounts.
    870      */
    871     public void onRefresh(boolean userRequest) {
    872         if (mIsRefreshable) {
    873             mRefreshManager.refreshMessageList(getAccountId(), getMailboxId(), userRequest);
    874         }
    875     }
    876 
    877     private void onDeselectAll() {
    878         mListAdapter.clearSelection();
    879         if (isInSelectionMode()) {
    880             finishSelectionMode();
    881         }
    882     }
    883 
    884     /**
    885      * Load more messages.  NOOP for special mailboxes (e.g. combined inbox).
    886      */
    887     private void onLoadMoreMessages() {
    888         if (mIsRefreshable) {
    889             mRefreshManager.loadMoreMessages(getAccountId(), getMailboxId());
    890         }
    891     }
    892 
    893     public void onSendPendingMessages() {
    894         RefreshManager rm = RefreshManager.getInstance(mActivity);
    895         if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) {
    896             rm.sendPendingMessagesForAllAccounts();
    897         } else if (mMailbox != null) { // Magic boxes don't have a specific account id.
    898             rm.sendPendingMessages(mMailbox.mAccountKey);
    899         }
    900     }
    901 
    902     /**
    903      * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
    904      * sense of the helper methods is "true=unread"; this may be called from the UI thread
    905      *
    906      * @param selectedSet The current list of selected items
    907      */
    908     private void toggleRead(Set<Long> selectedSet) {
    909         toggleMultiple(selectedSet, new MultiToggleHelper() {
    910 
    911             @Override
    912             public boolean getField(Cursor c) {
    913                 return c.getInt(MessagesAdapter.COLUMN_READ) == 0;
    914             }
    915 
    916             @Override
    917             public void setField(long messageId, boolean newValue) {
    918                 mController.setMessageReadSync(messageId, !newValue);
    919             }
    920         });
    921     }
    922 
    923     /**
    924      * Toggles a set of favorites (stars); this may be called from the UI thread
    925      *
    926      * @param selectedSet The current list of selected items
    927      */
    928     private void toggleFavorite(Set<Long> selectedSet) {
    929         toggleMultiple(selectedSet, new MultiToggleHelper() {
    930 
    931             @Override
    932             public boolean getField(Cursor c) {
    933                 return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0;
    934             }
    935 
    936             @Override
    937             public void setField(long messageId, boolean newValue) {
    938                 mController.setMessageFavoriteSync(messageId, newValue);
    939              }
    940         });
    941     }
    942 
    943     private void deleteMessages(Set<Long> selectedSet) {
    944         final long[] messageIds = Utility.toPrimitiveLongArray(selectedSet);
    945         mController.deleteMessages(messageIds);
    946         Toast.makeText(mActivity, mActivity.getResources().getQuantityString(
    947                 R.plurals.message_deleted_toast, messageIds.length), Toast.LENGTH_SHORT).show();
    948         selectedSet.clear();
    949         // Message deletion is async... Can't refresh the list immediately.
    950     }
    951 
    952     private interface MultiToggleHelper {
    953         /**
    954          * Return true if the field of interest is "set".  If one or more are false, then our
    955          * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
    956          * @param c the cursor, positioned to the item of interest
    957          * @return true if the field at this row is "set"
    958          */
    959         public boolean getField(Cursor c);
    960 
    961         /**
    962          * Set or clear the field of interest; setField is called asynchronously via EmailAsyncTask
    963          * @param messageId the message id of the current message
    964          * @param newValue the new value to be set at this row
    965          */
    966         public void setField(long messageId, boolean newValue);
    967     }
    968 
    969     /**
    970      * Toggle multiple fields in a message, using the following logic:  If one or more fields
    971      * are "clear", then "set" them.  If all fields are "set", then "clear" them all.  Provider
    972      * calls are applied asynchronously in setField
    973      *
    974      * @param selectedSet the set of messages that are selected
    975      * @param helper functions to implement the specific getter & setter
    976      */
    977     private void toggleMultiple(final Set<Long> selectedSet, final MultiToggleHelper helper) {
    978         final Cursor c = mListAdapter.getCursor();
    979         if (c == null || c.isClosed()) {
    980             return;
    981         }
    982 
    983         final HashMap<Long, Boolean> setValues = Maps.newHashMap();
    984         boolean allWereSet = true;
    985 
    986         c.moveToPosition(-1);
    987         while (c.moveToNext()) {
    988             long id = c.getInt(MessagesAdapter.COLUMN_ID);
    989             if (selectedSet.contains(id)) {
    990                 boolean value = helper.getField(c);
    991                 setValues.put(id, value);
    992                 allWereSet = allWereSet && value;
    993             }
    994         }
    995 
    996         if (!setValues.isEmpty()) {
    997             final boolean newValue = !allWereSet;
    998             c.moveToPosition(-1);
    999             // TODO: we should probably put up a dialog or some other progress indicator for this.
   1000             EmailAsyncTask.runAsyncParallel(new Runnable() {
   1001                @Override
   1002                 public void run() {
   1003                    for (long id : setValues.keySet()) {
   1004                        if (setValues.get(id) != newValue) {
   1005                            helper.setField(id, newValue);
   1006                        }
   1007                    }
   1008                 }});
   1009         }
   1010     }
   1011 
   1012     /**
   1013      * Test selected messages for showing appropriate labels
   1014      * @param selectedSet
   1015      * @param columnId
   1016      * @param defaultflag
   1017      * @return true when the specified flagged message is selected
   1018      */
   1019     private boolean testMultiple(Set<Long> selectedSet, int columnId, boolean defaultflag) {
   1020         final Cursor c = mListAdapter.getCursor();
   1021         if (c == null || c.isClosed()) {
   1022             return false;
   1023         }
   1024         c.moveToPosition(-1);
   1025         while (c.moveToNext()) {
   1026             long id = c.getInt(MessagesAdapter.COLUMN_ID);
   1027             if (selectedSet.contains(Long.valueOf(id))) {
   1028                 if (c.getInt(columnId) == (defaultflag ? 1 : 0)) {
   1029                     return true;
   1030                 }
   1031             }
   1032         }
   1033         return false;
   1034     }
   1035 
   1036     /**
   1037      * @return true if one or more non-starred messages are selected.
   1038      */
   1039     public boolean doesSelectionContainNonStarredMessage() {
   1040         return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE,
   1041                 false);
   1042     }
   1043 
   1044     /**
   1045      * @return true if one or more read messages are selected.
   1046      */
   1047     public boolean doesSelectionContainReadMessage() {
   1048         return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true);
   1049     }
   1050 
   1051     /**
   1052      * Implements a timed refresh of "stale" mailboxes.  This should only happen when
   1053      * multiple conditions are true, including:
   1054      *   Only refreshable mailboxes.
   1055      *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
   1056      * Note we do this even if it's a push account; even on Exchange only inbox can be pushed.
   1057      */
   1058     private void autoRefreshStaleMailbox() {
   1059         if (!mIsRefreshable) {
   1060             // Not refreshable (special box such as drafts, or magic boxes)
   1061             return;
   1062         }
   1063         if (!mRefreshManager.isMailboxStale(getMailboxId())) {
   1064             return;
   1065         }
   1066         onRefresh(false);
   1067     }
   1068 
   1069     /** Implements {@link MessagesAdapter.Callback} */
   1070     @Override
   1071     public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) {
   1072         mController.setMessageFavorite(itemView.mMessageId, newFavorite);
   1073     }
   1074 
   1075     /** Implements {@link MessagesAdapter.Callback} */
   1076     @Override
   1077     public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected,
   1078             int mSelectedCount) {
   1079         updateSelectionMode();
   1080     }
   1081 
   1082     private void updateSearchHeader(Cursor cursor) {
   1083         MessageListContext listContext = getListContext();
   1084         if (!listContext.isSearch() || cursor == null) {
   1085             UiUtilities.setVisibilitySafe(mSearchHeader, View.GONE);
   1086             return;
   1087         }
   1088 
   1089         SearchResultsCursor searchCursor = (SearchResultsCursor) cursor;
   1090         initSearchHeader();
   1091         mSearchHeader.setVisibility(View.VISIBLE);
   1092         String header = String.format(
   1093                 mActivity.getString(R.string.search_header_text_fmt),
   1094                 listContext.getSearchParams().mFilter);
   1095         mSearchHeaderText.setText(header);
   1096         int resultCount = searchCursor.getResultsCount();
   1097         // Don't show a negative value here; this means that the server request failed
   1098         // TODO Use some other text for this case (e.g. "search failed")?
   1099         if (resultCount < 0) {
   1100             resultCount = 0;
   1101         }
   1102         mSearchHeaderCount.setText(UiUtilities.getMessageCountForUi(
   1103                 mActivity, resultCount, false /* replaceZeroWithBlank */));
   1104     }
   1105 
   1106     private int determineFooterMode() {
   1107         int result = LIST_FOOTER_MODE_NONE;
   1108         if ((mMailbox == null)
   1109                 || (mMailbox.mType == Mailbox.TYPE_OUTBOX)
   1110                 || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) {
   1111             return result; // No footer
   1112         }
   1113         if (mMailbox.mType == Mailbox.TYPE_SEARCH) {
   1114             // Determine how many results have been loaded.
   1115             Cursor c = mListAdapter.getCursor();
   1116             if (c == null || c.isClosed()) {
   1117                 // Unknown yet - don't do anything.
   1118                 return result;
   1119             }
   1120             int total = ((SearchResultsCursor) c).getResultsCount();
   1121             int loaded = c.getCount();
   1122 
   1123             if (loaded < total) {
   1124                 result = LIST_FOOTER_MODE_MORE;
   1125             }
   1126         } else if (!mIsEasAccount) {
   1127             // IMAP, POP has "load more" for regular mailboxes.
   1128             result = LIST_FOOTER_MODE_MORE;
   1129         }
   1130         return result;
   1131     }
   1132 
   1133     private void updateFooterView() {
   1134         // Only called from onLoadFinished -- always has views.
   1135         int mode = determineFooterMode();
   1136         if (mListFooterMode == mode) {
   1137             return;
   1138         }
   1139         mListFooterMode = mode;
   1140 
   1141         ListView lv = getListView();
   1142         if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
   1143             lv.addFooterView(mListFooterView);
   1144             if (getListAdapter() != null) {
   1145                 // Already have an adapter - reset it to force the mode. But save the scroll
   1146                 // position so that we don't get kicked to the top.
   1147                 Parcelable listState = lv.onSaveInstanceState();
   1148                 setListAdapter(mListAdapter);
   1149                 lv.onRestoreInstanceState(listState);
   1150             }
   1151 
   1152             mListFooterProgress = mListFooterView.findViewById(R.id.progress);
   1153             mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
   1154         } else {
   1155             lv.removeFooterView(mListFooterView);
   1156         }
   1157         updateListFooter();
   1158     }
   1159 
   1160     /**
   1161      * Set the list footer text based on mode and the current "network active" status
   1162      */
   1163     private void updateListFooter() {
   1164         if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
   1165             int footerTextId = 0;
   1166             switch (mListFooterMode) {
   1167                 case LIST_FOOTER_MODE_MORE:
   1168                     boolean active = mRefreshManager.isMessageListRefreshing(getMailboxId());
   1169                     footerTextId = active ? R.string.status_loading_messages
   1170                             : R.string.message_list_load_more_messages_action;
   1171                     mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE);
   1172                     break;
   1173             }
   1174             mListFooterText.setText(footerTextId);
   1175         }
   1176     }
   1177 
   1178     /**
   1179      * Handle a click in the list footer, which changes meaning depending on what we're looking at.
   1180      */
   1181     private void doFooterClick() {
   1182         switch (mListFooterMode) {
   1183             case LIST_FOOTER_MODE_NONE: // should never happen
   1184                 break;
   1185             case LIST_FOOTER_MODE_MORE:
   1186                 onLoadMoreMessages();
   1187                 break;
   1188         }
   1189     }
   1190 
   1191     private void showSendCommand(boolean show) {
   1192         if (show != mShowSendCommand) {
   1193             mShowSendCommand = show;
   1194             mActivity.invalidateOptionsMenu();
   1195         }
   1196     }
   1197 
   1198     private void updateMailboxSpecificActions() {
   1199         final boolean isOutbox = (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX)
   1200                 || ((mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_OUTBOX));
   1201         showSendCommand(isOutbox && (mListAdapter != null) && (mListAdapter.getCount() > 0));
   1202 
   1203         // A null account/mailbox means we're in a combined view. We show the move icon there,
   1204         // even though it may be the case that we can't move messages from one of the mailboxes.
   1205         // There's no good way to tell that right now, though.
   1206         mShowMoveCommand = (mAccount == null || mAccount.supportsMoveMessages(getActivity()))
   1207                 && (mMailbox == null || mMailbox.canHaveMessagesMoved());
   1208 
   1209         // Enable mailbox specific actions on the UIController level if needed.
   1210         mActivity.invalidateOptionsMenu();
   1211     }
   1212 
   1213     /**
   1214      * Adjusts message notification depending upon the state of the fragment and the currently
   1215      * viewed mailbox. If the fragment is resumed, notifications for the current mailbox may
   1216      * be suspended. Otherwise, notifications may be re-activated. Not all mailbox types are
   1217      * supported for notifications. These include (but are not limited to) special mailboxes
   1218      * such as {@link Mailbox#QUERY_ALL_DRAFTS}, {@link Mailbox#QUERY_ALL_FAVORITES}, etc...
   1219      *
   1220      * @param updateLastSeenKey If {@code true}, the last seen message key for the currently
   1221      *                          viewed mailbox will be updated.
   1222      */
   1223     private void adjustMessageNotification(boolean updateLastSeenKey) {
   1224         final long accountId = getAccountId();
   1225         final long mailboxId = getMailboxId();
   1226         if (mailboxId == Mailbox.QUERY_ALL_INBOXES || mailboxId > 0) {
   1227             if (updateLastSeenKey) {
   1228                 Utility.updateLastSeenMessageKey(mActivity, accountId);
   1229             }
   1230             NotificationController notifier = NotificationController.getInstance(mActivity);
   1231             notifier.suspendMessageNotification(mResumed, accountId);
   1232         }
   1233     }
   1234 
   1235     private void startLoading() {
   1236         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
   1237             Log.d(Logging.LOG_TAG, this + " startLoading");
   1238         }
   1239         // Clear the list. (ListFragment will show the "Loading" animation)
   1240         showSendCommand(false);
   1241         updateSearchHeader(null);
   1242 
   1243         // Start loading...
   1244         final LoaderManager lm = getLoaderManager();
   1245         lm.initLoader(LOADER_ID_MESSAGES_LOADER, null, LOADER_CALLBACKS);
   1246     }
   1247 
   1248     /** Timeout to show a warning, since some IMAP searches could take a long time. */
   1249     private final int SEARCH_WARNING_DELAY_MS = 10000;
   1250 
   1251     private void onSearchLoadTimeout() {
   1252         // Search is taking too long. Show an error message.
   1253         ViewGroup root = (ViewGroup) getView();
   1254         Activity host = getActivity();
   1255         if (root != null && host != null) {
   1256             mListPanel.setVisibility(View.GONE);
   1257             mWarningContainer = (ViewGroup) LayoutInflater.from(host).inflate(
   1258                     R.layout.message_list_warning, root, false);
   1259             TextView title = UiUtilities.getView(mWarningContainer, R.id.message_title);
   1260             TextView message = UiUtilities.getView(mWarningContainer, R.id.message_warning);
   1261             title.setText(R.string.search_slow_warning_title);
   1262             message.setText(R.string.search_slow_warning_message);
   1263             root.addView(mWarningContainer);
   1264         }
   1265     }
   1266 
   1267     /**
   1268      * Loader callbacks for message list.
   1269      */
   1270     private final LoaderManager.LoaderCallbacks<Cursor> LOADER_CALLBACKS =
   1271             new LoaderManager.LoaderCallbacks<Cursor>() {
   1272         @Override
   1273         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
   1274             final MessageListContext listContext = getListContext();
   1275             if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
   1276                 Log.d(Logging.LOG_TAG, MessageListFragment.this
   1277                         + " onCreateLoader(messages) listContext=" + listContext);
   1278             }
   1279 
   1280             if (mListContext.isSearch()) {
   1281                 final MessageListContext searchInfo = mListContext;
   1282 
   1283                 // Search results are not primed with local data, and so will usually be slow.
   1284                 // In some cases, they could take a long time to return, so we need to be robust.
   1285                 setListShownNoAnimation(false);
   1286                 Utility.getMainThreadHandler().postDelayed(new Runnable() {
   1287                     @Override
   1288                     public void run() {
   1289                         if (mListContext != searchInfo) {
   1290                             // Different list is being shown now.
   1291                             return;
   1292                         }
   1293                         if (!mIsFirstLoad) {
   1294                             // Something already returned. No need to do anything.
   1295                             return;
   1296                         }
   1297                         onSearchLoadTimeout();
   1298                     }
   1299                 }, SEARCH_WARNING_DELAY_MS);
   1300             }
   1301 
   1302             mIsFirstLoad = true;
   1303             return MessagesAdapter.createLoader(getActivity(), listContext);
   1304         }
   1305 
   1306         @Override
   1307         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
   1308             if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
   1309                 Log.d(Logging.LOG_TAG, MessageListFragment.this
   1310                         + " onLoadFinished(messages) mailboxId=" + getMailboxId());
   1311             }
   1312             MessagesAdapter.MessagesCursor cursor = (MessagesAdapter.MessagesCursor) c;
   1313 
   1314             // Update the list
   1315             mListAdapter.swapCursor(cursor);
   1316 
   1317             if (!cursor.mIsFound) {
   1318                 mCallback.onMailboxNotFound(mIsFirstLoad);
   1319                 return;
   1320             }
   1321 
   1322             // Get the "extras" part.
   1323             mAccount = cursor.mAccount;
   1324             mMailbox = cursor.mMailbox;
   1325             mIsEasAccount = cursor.mIsEasAccount;
   1326             mIsRefreshable = cursor.mIsRefreshable;
   1327             mCountTotalAccounts = cursor.mCountTotalAccounts;
   1328 
   1329             // If this is a search result, open the first message.
   1330             if (UiUtilities.useTwoPane(getActivity()) && mIsFirstLoad && mListContext.isSearch()
   1331                     && cursor.getCount() > 0) {
   1332                 cursor.moveToFirst();
   1333                 onMessageOpen(getMailboxId(), cursor.getLong(MessagesAdapter.COLUMN_ID));
   1334             }
   1335 
   1336             // Suspend message notifications as long as we're resumed
   1337             adjustMessageNotification(false);
   1338 
   1339             // If this is a search mailbox, set the query; otherwise, clear it
   1340             if (mIsFirstLoad) {
   1341                 if (mMailbox != null && mMailbox.mType == Mailbox.TYPE_SEARCH) {
   1342                     mListAdapter.setQuery(getListContext().getSearchParams().mFilter);
   1343                     mSearchedMailbox = ((SearchResultsCursor) c).getSearchedMailbox();
   1344                 } else {
   1345                     mListAdapter.setQuery(null);
   1346                     mSearchedMailbox = null;
   1347                 }
   1348                 updateMailboxSpecificActions();
   1349 
   1350                 // Show chips if combined view.
   1351                 mListAdapter.setShowColorChips(isCombinedMailbox() && mCountTotalAccounts > 1);
   1352             }
   1353 
   1354             // Various post processing...
   1355             updateSearchHeader(cursor);
   1356             autoRefreshStaleMailbox();
   1357             updateFooterView();
   1358             updateSelectionMode();
   1359 
   1360             // We want to make visible the selection only for the first load.
   1361             // Re-load caused by content changed events shouldn't scroll the list.
   1362             highlightSelectedMessage(mIsFirstLoad);
   1363 
   1364             if (mIsFirstLoad) {
   1365                 UiUtilities.setVisibilitySafe(mWarningContainer, View.GONE);
   1366                 mListPanel.setVisibility(View.VISIBLE);
   1367 
   1368                 // Setting the adapter will automatically transition from "Loading" to showing
   1369                 // the list, which could show "No messages". Avoid showing that on the first sync,
   1370                 // if we know we're still potentially loading more.
   1371                 if (!isEmptyAndLoading(cursor)) {
   1372                     setListAdapter(mListAdapter);
   1373                 }
   1374             } else if ((getListAdapter() == null) && !isEmptyAndLoading(cursor)) {
   1375                 setListAdapter(mListAdapter);
   1376             }
   1377 
   1378             // Restore the state -- this step has to be the last, because Some of the
   1379             // "post processing" seems to reset the scroll position.
   1380             if (mSavedListState != null) {
   1381                 getListView().onRestoreInstanceState(mSavedListState);
   1382                 mSavedListState = null;
   1383             }
   1384 
   1385             mIsFirstLoad = false;
   1386         }
   1387 
   1388         /**
   1389          * Determines whether or not the list is empty, but we're still potentially loading data.
   1390          * This represents an ambiguous state where we may not want to show "No messages", since
   1391          * it may still just be loading.
   1392          */
   1393         private boolean isEmptyAndLoading(Cursor cursor) {
   1394             return (cursor.getCount() == 0)
   1395                         && mRefreshManager.isMessageListRefreshing(mMailbox.mId);
   1396         }
   1397 
   1398         @Override
   1399         public void onLoaderReset(Loader<Cursor> loader) {
   1400             if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
   1401                 Log.d(Logging.LOG_TAG, MessageListFragment.this
   1402                         + " onLoaderReset(messages)");
   1403             }
   1404             mListAdapter.swapCursor(null);
   1405             mAccount = null;
   1406             mMailbox = null;
   1407             mSearchedMailbox = null;
   1408             mCountTotalAccounts = 0;
   1409         }
   1410     };
   1411 
   1412     /**
   1413      * Show/hide the "selection" action mode, according to the number of selected messages and
   1414      * the visibility of the fragment.
   1415      * Also update the content (title and menus) if necessary.
   1416      */
   1417     public void updateSelectionMode() {
   1418         final int numSelected = getSelectedCount();
   1419         if ((numSelected == 0) || mDisableCab || !isViewCreated()) {
   1420             finishSelectionMode();
   1421             return;
   1422         }
   1423         if (isInSelectionMode()) {
   1424             updateSelectionModeView();
   1425         } else {
   1426             mLastSelectionModeCallback = new SelectionModeCallback();
   1427             getActivity().startActionMode(mLastSelectionModeCallback);
   1428         }
   1429     }
   1430 
   1431 
   1432     /**
   1433      * Finish the "selection" action mode.
   1434      *
   1435      * Note this method finishes the contextual mode, but does *not* clear the selection.
   1436      * If you want to do so use {@link #onDeselectAll()} instead.
   1437      */
   1438     private void finishSelectionMode() {
   1439         if (isInSelectionMode()) {
   1440             mLastSelectionModeCallback.mClosedByUser = false;
   1441             mSelectionMode.finish();
   1442         }
   1443     }
   1444 
   1445     /** Update the "selection" action mode bar */
   1446     private void updateSelectionModeView() {
   1447         mSelectionMode.invalidate();
   1448     }
   1449 
   1450     private class SelectionModeCallback implements ActionMode.Callback {
   1451         private MenuItem mMarkRead;
   1452         private MenuItem mMarkUnread;
   1453         private MenuItem mAddStar;
   1454         private MenuItem mRemoveStar;
   1455         private MenuItem mMove;
   1456 
   1457         /* package */ boolean mClosedByUser = true;
   1458 
   1459         @Override
   1460         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
   1461             mSelectionMode = mode;
   1462 
   1463             MenuInflater inflater = getActivity().getMenuInflater();
   1464             inflater.inflate(R.menu.message_list_fragment_cab_options, menu);
   1465             mMarkRead = menu.findItem(R.id.mark_read);
   1466             mMarkUnread = menu.findItem(R.id.mark_unread);
   1467             mAddStar = menu.findItem(R.id.add_star);
   1468             mRemoveStar = menu.findItem(R.id.remove_star);
   1469             mMove = menu.findItem(R.id.move);
   1470             return true;
   1471         }
   1472 
   1473         @Override
   1474         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
   1475             int num = getSelectedCount();
   1476             // Set title -- "# selected"
   1477             mSelectionMode.setTitle(getActivity().getResources().getQuantityString(
   1478                     R.plurals.message_view_selected_message_count, num, num));
   1479 
   1480             // Show appropriate menu items.
   1481             boolean nonStarExists = doesSelectionContainNonStarredMessage();
   1482             boolean readExists = doesSelectionContainReadMessage();
   1483             mMarkRead.setVisible(!readExists);
   1484             mMarkUnread.setVisible(readExists);
   1485             mAddStar.setVisible(nonStarExists);
   1486             mRemoveStar.setVisible(!nonStarExists);
   1487             mMove.setVisible(mShowMoveCommand);
   1488             return true;
   1489         }
   1490 
   1491         @Override
   1492         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
   1493             Set<Long> selectedConversations = mListAdapter.getSelectedSet();
   1494             if (selectedConversations.isEmpty()) return true;
   1495             switch (item.getItemId()) {
   1496                 case R.id.mark_read:
   1497                     // Note - marking as read does not trigger auto-advance.
   1498                     toggleRead(selectedConversations);
   1499                     break;
   1500                 case R.id.mark_unread:
   1501                     mCallback.onAdvancingOpAccepted(selectedConversations);
   1502                     toggleRead(selectedConversations);
   1503                     break;
   1504                 case R.id.add_star:
   1505                 case R.id.remove_star:
   1506                     // TODO: removing a star can be a destructive command and cause auto-advance
   1507                     // if the current mailbox shown is favorites.
   1508                     toggleFavorite(selectedConversations);
   1509                     break;
   1510                 case R.id.delete:
   1511                     mCallback.onAdvancingOpAccepted(selectedConversations);
   1512                     deleteMessages(selectedConversations);
   1513                     break;
   1514                 case R.id.move:
   1515                     showMoveMessagesDialog(selectedConversations);
   1516                     break;
   1517             }
   1518             return true;
   1519         }
   1520 
   1521         @Override
   1522         public void onDestroyActionMode(ActionMode mode) {
   1523             // Clear this before onDeselectAll() to prevent onDeselectAll() from trying to close the
   1524             // contextual mode again.
   1525             mSelectionMode = null;
   1526             if (mClosedByUser) {
   1527                 // Clear selection, only when the contextual mode is explicitly closed by the user.
   1528                 //
   1529                 // We close the contextual mode when the fragment becomes temporary invisible
   1530                 // (i.e. mIsVisible == false) too, in which case we want to keep the selection.
   1531                 onDeselectAll();
   1532             }
   1533         }
   1534     }
   1535 
   1536     private class RefreshListener implements RefreshManager.Listener {
   1537         @Override
   1538         public void onMessagingError(long accountId, long mailboxId, String message) {
   1539         }
   1540 
   1541         @Override
   1542         public void onRefreshStatusChanged(long accountId, long mailboxId) {
   1543             updateListFooter();
   1544         }
   1545     }
   1546 
   1547     /**
   1548      * Highlight the selected message.
   1549      */
   1550     private void highlightSelectedMessage(boolean ensureSelectionVisible) {
   1551         if (!isViewCreated()) {
   1552             return;
   1553         }
   1554 
   1555         final ListView lv = getListView();
   1556         if (mSelectedMessageId == -1) {
   1557             // No message selected
   1558             lv.clearChoices();
   1559             return;
   1560         }
   1561 
   1562         final int count = lv.getCount();
   1563         for (int i = 0; i < count; i++) {
   1564             if (lv.getItemIdAtPosition(i) != mSelectedMessageId) {
   1565                 continue;
   1566             }
   1567             lv.setItemChecked(i, true);
   1568             if (ensureSelectionVisible) {
   1569                 Utility.listViewSmoothScrollToPosition(getActivity(), lv, i);
   1570             }
   1571             break;
   1572         }
   1573     }
   1574 }
   1575