MessageListFragment.java revision 78684ccc795c0d5211dfc04a834cb452dccb1058
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 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 String mMessageText;
504        private PointF mMessagePoint;
505
506        private String mCountText;
507        private 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    public void onMultiToggleRead() {
693        onMultiToggleRead(mListAdapter.getSelectedSet());
694    }
695
696    public void onMultiToggleFavorite() {
697        onMultiToggleFavorite(mListAdapter.getSelectedSet());
698    }
699
700    public void onMultiDelete() {
701        onMultiDelete(mListAdapter.getSelectedSet());
702    }
703
704    public void onMultiMove() {
705        long[] messageIds = Utility.toPrimitiveLongArray(mListAdapter.getSelectedSet());
706        MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(messageIds, this);
707        dialog.show(getFragmentManager(), "dialog");
708    }
709
710    @Override
711    public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
712        ActivityHelper.moveMessages(getActivity(), newMailboxId, messageIds);
713
714        // Move is async, so we can't refresh now.  Instead, just clear the selection.
715        onDeselectAll();
716    }
717
718    /**
719     * Refresh the list.  NOOP for special mailboxes (e.g. combined inbox).
720     *
721     * Note: Manual refresh is enabled even for push accounts.
722     */
723    public void onRefresh(boolean userRequest) {
724        if (!mIsRefreshable) {
725            return;
726        }
727        long accountId = getAccountId();
728        if (accountId != -1) {
729            mRefreshManager.refreshMessageList(accountId, mMailboxId, userRequest);
730        }
731    }
732
733    public void onDeselectAll() {
734        if ((mListAdapter == null) || (mListAdapter.getSelectedSet().size() == 0)) {
735            return;
736        }
737        mListAdapter.getSelectedSet().clear();
738        getListView().invalidateViews();
739        if (isInSelectionMode()) {
740            finishSelectionMode();
741        }
742    }
743
744    /**
745     * Load more messages.  NOOP for special mailboxes (e.g. combined inbox).
746     */
747    private void onLoadMoreMessages() {
748        long accountId = getAccountId();
749        if (accountId != -1) {
750            mRefreshManager.loadMoreMessages(accountId, mMailboxId);
751        }
752    }
753
754    /**
755     * @return if it's an outbox or "all outboxes".
756     *
757     * TODO make it private.  It's only used by MessageList, but the callsite is obsolete.
758     */
759    public boolean isOutbox() {
760        return (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX)
761            || ((mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_OUTBOX));
762    }
763
764    public void onSendPendingMessages() {
765        RefreshManager rm = RefreshManager.getInstance(mActivity);
766        if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) {
767            rm.sendPendingMessagesForAllAccounts();
768        } else if (mMailbox != null) { // Magic boxes don't have a specific account id.
769            rm.sendPendingMessages(mMailbox.mAccountKey);
770        }
771    }
772
773    private void onSetMessageRead(long messageId, boolean newRead) {
774        mController.setMessageRead(messageId, newRead);
775    }
776
777    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
778        mController.setMessageFavorite(messageId, newFavorite);
779    }
780
781    /**
782     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
783     * sense of the helper methods is "true=unread".
784     *
785     * @param selectedSet The current list of selected items
786     */
787    private void onMultiToggleRead(Set<Long> selectedSet) {
788        toggleMultiple(selectedSet, new MultiToggleHelper() {
789
790            public boolean getField(long messageId, Cursor c) {
791                return c.getInt(MessagesAdapter.COLUMN_READ) == 0;
792            }
793
794            public boolean setField(long messageId, Cursor c, boolean newValue) {
795                boolean oldValue = getField(messageId, c);
796                if (oldValue != newValue) {
797                    onSetMessageRead(messageId, !newValue);
798                    return true;
799                }
800                return false;
801            }
802        });
803    }
804
805    /**
806     * Toggles a set of favorites (stars)
807     *
808     * @param selectedSet The current list of selected items
809     */
810    private void onMultiToggleFavorite(Set<Long> selectedSet) {
811        toggleMultiple(selectedSet, new MultiToggleHelper() {
812
813            public boolean getField(long messageId, Cursor c) {
814                return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0;
815            }
816
817            public boolean setField(long messageId, Cursor c, boolean newValue) {
818                boolean oldValue = getField(messageId, c);
819                if (oldValue != newValue) {
820                    onSetMessageFavorite(messageId, newValue);
821                    return true;
822                }
823                return false;
824            }
825        });
826    }
827
828    private void onMultiDelete(Set<Long> selectedSet) {
829        // Clone the set, because deleting is going to thrash things
830        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
831        for (Long id : cloneSet) {
832            mController.deleteMessage(id, -1);
833        }
834        Toast.makeText(mActivity, mActivity.getResources().getQuantityString(
835                R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show();
836        selectedSet.clear();
837        // Message deletion is async... Can't refresh the list immediately.
838    }
839
840    private interface MultiToggleHelper {
841        /**
842         * Return true if the field of interest is "set".  If one or more are false, then our
843         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
844         * @param messageId the message id of the current message
845         * @param c the cursor, positioned to the item of interest
846         * @return true if the field at this row is "set"
847         */
848        public boolean getField(long messageId, Cursor c);
849
850        /**
851         * Set or clear the field of interest.  Return true if a change was made.
852         * @param messageId the message id of the current message
853         * @param c the cursor, positioned to the item of interest
854         * @param newValue the new value to be set at this row
855         * @return true if a change was actually made
856         */
857        public boolean setField(long messageId, Cursor c, boolean newValue);
858    }
859
860    /**
861     * Toggle multiple fields in a message, using the following logic:  If one or more fields
862     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
863     *
864     * @param selectedSet the set of messages that are selected
865     * @param helper functions to implement the specific getter & setter
866     * @return the number of messages that were updated
867     */
868    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
869        Cursor c = mListAdapter.getCursor();
870        boolean anyWereFound = false;
871        boolean allWereSet = true;
872
873        c.moveToPosition(-1);
874        while (c.moveToNext()) {
875            long id = c.getInt(MessagesAdapter.COLUMN_ID);
876            if (selectedSet.contains(Long.valueOf(id))) {
877                anyWereFound = true;
878                if (!helper.getField(id, c)) {
879                    allWereSet = false;
880                    break;
881                }
882            }
883        }
884
885        int numChanged = 0;
886
887        if (anyWereFound) {
888            boolean newValue = !allWereSet;
889            c.moveToPosition(-1);
890            while (c.moveToNext()) {
891                long id = c.getInt(MessagesAdapter.COLUMN_ID);
892                if (selectedSet.contains(Long.valueOf(id))) {
893                    if (helper.setField(id, c, newValue)) {
894                        ++numChanged;
895                    }
896                }
897            }
898        }
899
900        refreshList();
901
902        return numChanged;
903    }
904
905    /**
906     * Test selected messages for showing appropriate labels
907     * @param selectedSet
908     * @param column_id
909     * @param defaultflag
910     * @return true when the specified flagged message is selected
911     */
912    private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) {
913        Cursor c = mListAdapter.getCursor();
914        if (c == null || c.isClosed()) {
915            return false;
916        }
917        c.moveToPosition(-1);
918        while (c.moveToNext()) {
919            long id = c.getInt(MessagesAdapter.COLUMN_ID);
920            if (selectedSet.contains(Long.valueOf(id))) {
921                if (c.getInt(column_id) == (defaultflag ? 1 : 0)) {
922                    return true;
923                }
924            }
925        }
926        return false;
927    }
928
929    /**
930     * @return true if one or more non-starred messages are selected.
931     */
932    public boolean doesSelectionContainNonStarredMessage() {
933        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE,
934                false);
935    }
936
937    /**
938     * @return true if one or more read messages are selected.
939     */
940    public boolean doesSelectionContainReadMessage() {
941        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true);
942    }
943
944    /**
945     * Implements a timed refresh of "stale" mailboxes.  This should only happen when
946     * multiple conditions are true, including:
947     *   Only refreshable mailboxes.
948     *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
949     * Note we do this even if it's a push account; even on Exchange only inbox can be pushed.
950     */
951    private void autoRefreshStaleMailbox() {
952        if (!mIsRefreshable) {
953            // Not refreshable (special box such as drafts, or magic boxes)
954            return;
955        }
956        if (!mRefreshManager.isMailboxStale(mMailboxId)) {
957            return;
958        }
959        onRefresh(false);
960    }
961
962    /** Implements {@link MessagesAdapter.Callback} */
963    @Override
964    public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) {
965        onSetMessageFavorite(itemView.mMessageId, newFavorite);
966    }
967
968    /** Implements {@link MessagesAdapter.Callback} */
969    @Override
970    public void onAdapterSelectedChanged(
971            MessageListItem itemView, boolean newSelected, int mSelectedCount) {
972        updateSelectionMode();
973    }
974
975    private void determineFooterMode() {
976        mListFooterMode = LIST_FOOTER_MODE_NONE;
977        if ((mMailbox == null) || (mMailbox.mType == Mailbox.TYPE_OUTBOX)
978                || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) {
979            return; // No footer
980        }
981        if (!mIsEasAccount) {
982            // IMAP, POP has "load more"
983            mListFooterMode = LIST_FOOTER_MODE_MORE;
984        }
985    }
986
987    private void addFooterView() {
988        ListView lv = getListView();
989        if (mListFooterView != null) {
990            lv.removeFooterView(mListFooterView);
991        }
992        determineFooterMode();
993        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
994
995            lv.addFooterView(mListFooterView);
996            lv.setAdapter(mListAdapter);
997
998            mListFooterProgress = mListFooterView.findViewById(R.id.progress);
999            mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
1000
1001            updateListFooter();
1002        }
1003    }
1004
1005    /**
1006     * Set the list footer text based on mode and the current "network active" status
1007     */
1008    private void updateListFooter() {
1009        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
1010            int footerTextId = 0;
1011            switch (mListFooterMode) {
1012                case LIST_FOOTER_MODE_MORE:
1013                    boolean active = mRefreshManager.isMessageListRefreshing(mMailboxId);
1014                    footerTextId = active ? R.string.status_loading_messages
1015                            : R.string.message_list_load_more_messages_action;
1016                    mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE);
1017                    break;
1018            }
1019            mListFooterText.setText(footerTextId);
1020        }
1021    }
1022
1023    /**
1024     * Handle a click in the list footer, which changes meaning depending on what we're looking at.
1025     */
1026    private void doFooterClick() {
1027        switch (mListFooterMode) {
1028            case LIST_FOOTER_MODE_NONE: // should never happen
1029                break;
1030            case LIST_FOOTER_MODE_MORE:
1031                onLoadMoreMessages();
1032                break;
1033        }
1034    }
1035
1036    private void showSendCommand(boolean show) {
1037        mShowSendCommand = show;
1038        mActivity.invalidateOptionsMenu();
1039    }
1040
1041    private void showSendCommandIfNecessary() {
1042        showSendCommand(isOutbox() && (mListAdapter != null) && (mListAdapter.getCount() > 0));
1043    }
1044
1045    private void showNoMessageText(boolean visible) {
1046        mNoMessagesPanel.setVisibility(visible ? View.VISIBLE : View.GONE);
1047        mListPanel.setVisibility(visible ? View.GONE : View.VISIBLE);
1048    }
1049
1050    private void showNoMessageTextIfNecessary() {
1051        boolean noItem = (mListFooterMode == LIST_FOOTER_MODE_NONE)
1052                && (mListView.getCount() == 0);
1053        showNoMessageText(noItem);
1054    }
1055
1056    private void startLoading() {
1057        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
1058            Log.d(Logging.LOG_TAG, "MessageListFragment startLoading");
1059        }
1060        mOpenRequested = false;
1061
1062        // Clear the list. (ListFragment will show the "Loading" animation)
1063        showNoMessageText(false);
1064        setListShown(false);
1065        showSendCommand(false);
1066
1067        // Start loading...
1068        final LoaderManager lm = getLoaderManager();
1069
1070        // If we're loading a different mailbox, discard the previous result.
1071        // It also causes not to preserve the list position.
1072        boolean mailboxChanging = false;
1073        if ((mLastLoadedMailboxId != -1) && (mLastLoadedMailboxId != mMailboxId)) {
1074            mailboxChanging = true;
1075            stopLoaders();
1076        }
1077        lm.initLoader(LOADER_ID_MAILBOX_LOADER, null,
1078                new MailboxAccountLoaderCallback(mailboxChanging));
1079    }
1080
1081    private void stopLoaders() {
1082        final LoaderManager lm = getLoaderManager();
1083        lm.destroyLoader(LOADER_ID_MAILBOX_LOADER);
1084        lm.destroyLoader(LOADER_ID_MESSAGES_LOADER);
1085    }
1086
1087    /**
1088     * Loader callbacks for {@link MailboxAccountLoader}.
1089     */
1090    private class MailboxAccountLoaderCallback implements LoaderManager.LoaderCallbacks<
1091            MailboxAccountLoader.Result> {
1092        private boolean mMailboxChanging;
1093
1094        public MailboxAccountLoaderCallback(boolean mailboxChanging) {
1095            mMailboxChanging = mailboxChanging;
1096        }
1097
1098        @Override
1099        public Loader<MailboxAccountLoader.Result> onCreateLoader(int id, Bundle args) {
1100            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
1101                Log.d(Logging.LOG_TAG,
1102                        "MessageListFragment onCreateLoader(mailbox) mailboxId=" + mMailboxId);
1103            }
1104            return new MailboxAccountLoader(getActivity().getApplicationContext(), mMailboxId);
1105        }
1106
1107        @Override
1108        public void onLoadFinished(Loader<MailboxAccountLoader.Result> loader,
1109                MailboxAccountLoader.Result result) {
1110            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
1111                Log.d(Logging.LOG_TAG, "MessageListFragment onLoadFinished(mailbox) mailboxId="
1112                        + mMailboxId);
1113            }
1114            if (!result.mIsFound) {
1115                mCallback.onMailboxNotFound();
1116                return;
1117            }
1118
1119            mLastLoadedMailboxId = mMailboxId;
1120            mAccount = result.mAccount;
1121            mMailbox = result.mMailbox;
1122            mIsEasAccount = result.mIsEasAccount;
1123            mIsRefreshable = result.mIsRefreshable;
1124            mCountTotalAccounts = result.mCountTotalAccounts;
1125            getLoaderManager().initLoader(LOADER_ID_MESSAGES_LOADER, null,
1126                    new MessagesLoaderCallback(mMailboxChanging));
1127
1128            // Clear this for next reload triggered by content changed events.
1129            mMailboxChanging = false;
1130        }
1131
1132        @Override
1133        public void onLoaderReset(Loader<MailboxAccountLoader.Result> loader) {
1134        }
1135    }
1136
1137    /**
1138     * Reload the data and refresh the list view.
1139     */
1140    private void refreshList() {
1141        getLoaderManager().restartLoader(LOADER_ID_MESSAGES_LOADER, null,
1142                new MessagesLoaderCallback(false));
1143    }
1144
1145    /**
1146     * Loader callbacks for message list.
1147     */
1148    private class MessagesLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
1149        private boolean mMailboxChanging;
1150
1151        public MessagesLoaderCallback(boolean mailboxChanging) {
1152            mMailboxChanging = mailboxChanging;
1153        }
1154
1155        @Override
1156        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1157            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
1158                Log.d(Logging.LOG_TAG,
1159                        "MessageListFragment onCreateLoader(messages) mailboxId=" + mMailboxId);
1160            }
1161            return MessagesAdapter.createLoader(getActivity(), mMailboxId);
1162        }
1163
1164        @Override
1165        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
1166            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
1167                Log.d(Logging.LOG_TAG,
1168                        "MessageListFragment onLoadFinished(messages) mailboxId=" + mMailboxId);
1169            }
1170
1171            // Save list view state (primarily scroll position)
1172            final ListView lv = getListView();
1173            final Utility.ListStateSaver lss;
1174            if (mMailboxChanging) {
1175                lss = null; // Don't preserve list state
1176            } else if (mSavedListState != null) {
1177                lss = mSavedListState;
1178                mSavedListState = null;
1179            } else {
1180                lss = new Utility.ListStateSaver(lv);
1181            }
1182
1183            // If this is a search mailbox, set the query
1184            if (mMailbox != null && mMailbox.mType == Mailbox.TYPE_SEARCH) {
1185                mListAdapter.setQuery(mMailbox.mDisplayName);
1186            }
1187
1188            // Update the list
1189            mListAdapter.swapCursor(cursor);
1190            // Show chips if combined view.
1191            mListAdapter.setShowColorChips(mMailboxId < 0 && mCountTotalAccounts > 1);
1192            setListAdapter(mListAdapter);
1193            setListShown(true);
1194
1195            // Various post processing...
1196            autoRefreshStaleMailbox();
1197            addFooterView();
1198            updateSelectionMode();
1199            showSendCommandIfNecessary();
1200            showNoMessageTextIfNecessary();
1201
1202            // We want to make selection visible only when the loader was explicitly started.
1203            // i.e. Refresh caused by content changed events shouldn't scroll the list.
1204            highlightSelectedMessage(mMailboxChanging);
1205
1206            // Restore the state -- this step has to be the last, because Some of the
1207            // "post processing" seems to reset the scroll position.
1208            if (lss != null) {
1209                lss.restore(lv);
1210            }
1211
1212            resetNewMessageCount(mActivity, mMailboxId, getAccountId());
1213
1214            // Clear this for next reload triggered by content changed events.
1215            mMailboxChanging = false;
1216
1217            mCallback.onListLoaded();
1218        }
1219
1220        @Override
1221        public void onLoaderReset(Loader<Cursor> loader) {
1222            mListAdapter.swapCursor(null);
1223        }
1224    }
1225
1226    /**
1227     * Reset the "new message" count.
1228     * <ul>
1229     * <li>If {@code mailboxId} is {@link Mailbox#QUERY_ALL_INBOXES}, reset the
1230     * counts of all accounts.
1231     * <li>If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code
1232     * accountId} is valid, reset the count of the specified account.
1233     * </ul>
1234     */
1235    /* protected */static void resetNewMessageCount(
1236            Context context, long mailboxId, long accountId) {
1237        if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
1238            MailService.resetNewMessageCount(context, -1);
1239        } else if (mailboxId >= 0 && accountId != -1) {
1240            MailService.resetNewMessageCount(context, accountId);
1241        }
1242    }
1243
1244    /**
1245     * Show/hide the "selection" action mode, according to the number of selected messages and
1246     * the visibility of the fragment.
1247     * Also update the content (title and menus) if necessary.
1248     */
1249    public void updateSelectionMode() {
1250        final int numSelected = getSelectedCount();
1251        if ((numSelected == 0) || !mIsVisible) {
1252            finishSelectionMode();
1253            return;
1254        }
1255        if (isInSelectionMode()) {
1256            updateSelectionModeView();
1257        } else {
1258            mLastSelectionModeCallback = new SelectionModeCallback();
1259            getActivity().startActionMode(mLastSelectionModeCallback);
1260        }
1261    }
1262
1263
1264    /**
1265     * Finish the "selection" action mode.
1266     *
1267     * Note this method finishes the contextual mode, but does *not* clear the selection.
1268     * If you want to do so use {@link #onDeselectAll()} instead.
1269     */
1270    private void finishSelectionMode() {
1271        if (isInSelectionMode()) {
1272            mLastSelectionModeCallback.mClosedByUser = false;
1273            mSelectionMode.finish();
1274        }
1275    }
1276
1277    /** Update the "selection" action mode bar */
1278    private void updateSelectionModeView() {
1279        mSelectionMode.invalidate();
1280    }
1281
1282    private class SelectionModeCallback implements ActionMode.Callback {
1283        private MenuItem mMarkRead;
1284        private MenuItem mMarkUnread;
1285        private MenuItem mAddStar;
1286        private MenuItem mRemoveStar;
1287
1288        /* package */ boolean mClosedByUser = true;
1289
1290        @Override
1291        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1292            mSelectionMode = mode;
1293
1294            MenuInflater inflater = getActivity().getMenuInflater();
1295            inflater.inflate(R.menu.message_list_selection_mode, menu);
1296            mMarkRead = menu.findItem(R.id.mark_read);
1297            mMarkUnread = menu.findItem(R.id.mark_unread);
1298            mAddStar = menu.findItem(R.id.add_star);
1299            mRemoveStar = menu.findItem(R.id.remove_star);
1300
1301            mCallback.onEnterSelectionMode(true);
1302            return true;
1303        }
1304
1305        @Override
1306        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1307            int num = getSelectedCount();
1308            // Set title -- "# selected"
1309            mSelectionMode.setTitle(getActivity().getResources().getQuantityString(
1310                    R.plurals.message_view_selected_message_count, num, num));
1311
1312            // Show appropriate menu items.
1313            boolean nonStarExists = doesSelectionContainNonStarredMessage();
1314            boolean readExists = doesSelectionContainReadMessage();
1315            mMarkRead.setVisible(!readExists);
1316            mMarkUnread.setVisible(readExists);
1317            mAddStar.setVisible(nonStarExists);
1318            mRemoveStar.setVisible(!nonStarExists);
1319            return true;
1320        }
1321
1322        @Override
1323        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1324            switch (item.getItemId()) {
1325                case R.id.mark_read:
1326                case R.id.mark_unread:
1327                    onMultiToggleRead();
1328                    break;
1329                case R.id.add_star:
1330                case R.id.remove_star:
1331                    onMultiToggleFavorite();
1332                    break;
1333                case R.id.delete:
1334                    onMultiDelete();
1335                    break;
1336                case R.id.move:
1337                    onMultiMove();
1338                    break;
1339            }
1340            return true;
1341        }
1342
1343        @Override
1344        public void onDestroyActionMode(ActionMode mode) {
1345            mCallback.onEnterSelectionMode(false);
1346
1347            // Clear this before onDeselectAll() to prevent onDeselectAll() from trying to close the
1348            // contextual mode again.
1349            mSelectionMode = null;
1350            if (mClosedByUser) {
1351                // Clear selection, only when the contextual mode is explicitly closed by the user.
1352                //
1353                // We close the contextual mode when the fragment becomes temporary invisible
1354                // (i.e. mIsVisible == false) too, in which case we want to keep the selection.
1355                onDeselectAll();
1356            }
1357        }
1358    }
1359
1360    private class RefreshListener implements RefreshManager.Listener {
1361        @Override
1362        public void onMessagingError(long accountId, long mailboxId, String message) {
1363        }
1364
1365        @Override
1366        public void onRefreshStatusChanged(long accountId, long mailboxId) {
1367            updateListFooter();
1368        }
1369    }
1370
1371    /**
1372     * Object that holds the current state (right now it's only the ListView state) of the fragment.
1373     *
1374     * Used by {@link MessageListXLFragmentManager} to preserve scroll position through fragment
1375     * transitions.
1376     */
1377    public static class State implements Parcelable {
1378        private final ListStateSaver mListState;
1379
1380        private State(Parcel p) {
1381            mListState = p.readParcelable(getClass().getClassLoader());
1382        }
1383
1384        private State(MessageListFragment messageListFragment) {
1385            mListState = new Utility.ListStateSaver(messageListFragment.getListView());
1386        }
1387
1388        public void restore(MessageListFragment messageListFragment) {
1389            messageListFragment.mSavedListState = mListState;
1390        }
1391
1392        @Override
1393        public int describeContents() {
1394            return 0;
1395        }
1396
1397        @Override
1398        public void writeToParcel(Parcel dest, int flags) {
1399            dest.writeParcelable(mListState, flags);
1400        }
1401
1402        public static final Parcelable.Creator<State> CREATOR
1403                = new Parcelable.Creator<State>() {
1404                    public State createFromParcel(Parcel in) {
1405                        return new State(in);
1406                    }
1407
1408                    public State[] newArray(int size) {
1409                        return new State[size];
1410                    }
1411                };
1412    }
1413
1414    public State getState() {
1415        return new State(this);
1416    }
1417
1418    /**
1419     * Highlight the selected message.
1420     */
1421    private void highlightSelectedMessage(boolean ensureSelectionVisible) {
1422        if (mSelectedMessageId == -1) {
1423            // No mailbox selected
1424            mListView.clearChoices();
1425            return;
1426        }
1427
1428        final int count = mListView.getCount();
1429        for (int i = 0; i < count; i++) {
1430            if (mListView.getItemIdAtPosition(i) != mSelectedMessageId) {
1431                continue;
1432            }
1433            mListView.setItemChecked(i, true);
1434            if (ensureSelectionVisible) {
1435                Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i);
1436            }
1437            break;
1438        }
1439    }
1440}
1441