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