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
17package com.android.email.activity;
18
19import android.app.Activity;
20import android.app.ListFragment;
21import android.app.LoaderManager;
22import android.content.ClipData;
23import android.content.ContentUris;
24import android.content.Context;
25import android.content.Loader;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.graphics.Canvas;
30import android.graphics.Point;
31import android.graphics.PointF;
32import android.graphics.Rect;
33import android.graphics.Typeface;
34import android.graphics.drawable.Drawable;
35import android.os.Bundle;
36import android.os.Parcelable;
37import android.text.TextPaint;
38import android.util.Log;
39import android.view.ActionMode;
40import android.view.DragEvent;
41import android.view.LayoutInflater;
42import android.view.Menu;
43import android.view.MenuInflater;
44import android.view.MenuItem;
45import android.view.MotionEvent;
46import android.view.View;
47import android.view.View.DragShadowBuilder;
48import android.view.View.OnDragListener;
49import android.view.View.OnTouchListener;
50import android.view.ViewGroup;
51import android.widget.AdapterView;
52import android.widget.AdapterView.OnItemLongClickListener;
53import android.widget.ListView;
54import android.widget.TextView;
55import android.widget.Toast;
56
57import com.android.email.Controller;
58import com.android.email.Email;
59import com.android.email.MessageListContext;
60import com.android.email.NotificationController;
61import com.android.email.R;
62import com.android.email.RefreshManager;
63import com.android.email.activity.MessagesAdapter.SearchResultsCursor;
64import com.android.email.provider.EmailProvider;
65import com.android.emailcommon.Logging;
66import com.android.emailcommon.provider.Account;
67import com.android.emailcommon.provider.EmailContent.Message;
68import com.android.emailcommon.provider.Mailbox;
69import com.android.emailcommon.utility.EmailAsyncTask;
70import com.android.emailcommon.utility.Utility;
71import com.google.common.annotations.VisibleForTesting;
72
73import java.util.HashMap;
74import 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 */
82public 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