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