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