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