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