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