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