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