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