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