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