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