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