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