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