MessageListFragment.java revision 3d784232812d0103f2ac353b0fb17df37be9dcfe
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.service.MailService;
30
31import android.app.Activity;
32import android.app.ListFragment;
33import android.app.LoaderManager;
34import android.content.Context;
35import android.content.Loader;
36import android.database.Cursor;
37import android.os.Bundle;
38import android.os.Parcel;
39import android.os.Parcelable;
40import android.util.Log;
41import android.view.ActionMode;
42import android.view.LayoutInflater;
43import android.view.Menu;
44import android.view.MenuInflater;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.View.OnClickListener;
48import android.view.ViewGroup;
49import android.widget.AdapterView;
50import android.widget.AdapterView.OnItemClickListener;
51import android.widget.AdapterView.OnItemLongClickListener;
52import android.widget.Button;
53import android.widget.ListView;
54import android.widget.TextView;
55import android.widget.Toast;
56
57import java.security.InvalidParameterException;
58import java.util.HashSet;
59import java.util.Set;
60
61// TODO Better handling of restoring list position/adapter check status
62/**
63 * Message list.
64 *
65 * <p>This fragment uses two different loaders to load data.
66 * <ul>
67 *   <li>One to load {@link Account} and {@link Mailbox}, with {@link MailboxAccountLoader}.
68 *   <li>The other to actually load messages.
69 * </ul>
70 * We run them sequentially.  i.e. First starts {@link MailboxAccountLoader}, and when it finishes
71 * starts the other.
72 *
73 * TODO Add "send all messages" button to outboxes
74 */
75public class MessageListFragment extends ListFragment
76        implements OnItemClickListener, OnItemLongClickListener, MessagesAdapter.Callback,
77        OnClickListener {
78    private static final String BUNDLE_LIST_STATE = "MessageListFragment.state.listState";
79
80    private static final int LOADER_ID_MAILBOX_LOADER = 1;
81    private static final int LOADER_ID_MESSAGES_LOADER = 2;
82
83    // UI Support
84    private Activity mActivity;
85    private Callback mCallback = EmptyCallback.INSTANCE;
86
87    private View mListFooterView;
88    private TextView mListFooterText;
89    private View mListFooterProgress;
90    private View mSendPanel;
91
92    private static final int LIST_FOOTER_MODE_NONE = 0;
93    private static final int LIST_FOOTER_MODE_MORE = 1;
94    private int mListFooterMode;
95
96    private MessagesAdapter mListAdapter;
97
98    private long mMailboxId = -1;
99    private long mLastLoadedMailboxId = -1;
100    private Account mAccount;
101    private Mailbox mMailbox;
102    private boolean mIsEasAccount;
103    private boolean mIsRefreshable;
104
105    // Controller access
106    private Controller mController;
107    private RefreshManager mRefreshManager;
108    private RefreshListener mRefreshListener = new RefreshListener();
109
110    // Misc members
111    private boolean mDoAutoRefresh;
112
113    private boolean mOpenRequested;
114
115    /** true between {@link #onResume} and {@link #onPause}. */
116    private boolean mResumed;
117
118    /**
119     * {@link ActionMode} shown when 1 or more message is selected.
120     */
121    private ActionMode mSelectionMode;
122
123    private Utility.ListStateSaver mSavedListState;
124
125    /**
126     * Callback interface that owning activities must implement
127     */
128    public interface Callback {
129        public static final int TYPE_REGULAR = 0;
130        public static final int TYPE_DRAFT = 1;
131        public static final int TYPE_TRASH = 2;
132
133        /**
134         * Called when the specified mailbox does not exist.
135         */
136        public void onMailboxNotFound();
137
138        /**
139         * Called when the user wants to open a message.
140         * Note {@code mailboxId} is of the actual mailbox of the message, which is different from
141         * {@link MessageListFragment#getMailboxId} if it's magic mailboxes.
142         *
143         * @param messageId the message ID of the message
144         * @param messageMailboxId the mailbox ID of the message.
145         *     This will never take values like {@link Mailbox#QUERY_ALL_INBOXES}.
146         * @param listMailboxId the mailbox ID of the listbox shown on this fragment.
147         *     This can be that of a magic mailbox, e.g.  {@link Mailbox#QUERY_ALL_INBOXES}.
148         * @param type {@link #TYPE_REGULAR}, {@link #TYPE_DRAFT} or {@link #TYPE_TRASH}.
149         */
150        public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId,
151                int type);
152    }
153
154    private static final class EmptyCallback implements Callback {
155        public static final Callback INSTANCE = new EmptyCallback();
156
157        @Override
158        public void onMailboxNotFound() {
159        }
160        @Override
161        public void onMessageOpen(
162                long messageId, long messageMailboxId, long listMailboxId, int type) {
163        }
164    }
165
166    @Override
167    public void onCreate(Bundle savedInstanceState) {
168        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
169            Log.d(Email.LOG_TAG, "MessageListFragment onCreate");
170        }
171        super.onCreate(savedInstanceState);
172        mActivity = getActivity();
173        mController = Controller.getInstance(mActivity);
174        mRefreshManager = RefreshManager.getInstance(mActivity);
175        mRefreshManager.registerListener(mRefreshListener);
176    }
177
178    @Override
179    public View onCreateView(
180            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
181        // Use a custom layout, which includes the original layout with "send messages" panel.
182        View root = inflater.inflate(R.layout.message_list_fragment,null);
183        mSendPanel = root.findViewById(R.id.send_panel);
184        ((Button) mSendPanel.findViewById(R.id.send_messages)).setOnClickListener(this);
185        return root;
186    }
187
188    @Override
189    public void onActivityCreated(Bundle savedInstanceState) {
190        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
191            Log.d(Email.LOG_TAG, "MessageListFragment onActivityCreated");
192        }
193        super.onActivityCreated(savedInstanceState);
194
195        ListView listView = getListView();
196        listView.setOnItemClickListener(this);
197        listView.setOnItemLongClickListener(this);
198        listView.setItemsCanFocus(false);
199
200        mListAdapter = new MessagesAdapter(mActivity, this);
201
202        mListFooterView = getActivity().getLayoutInflater().inflate(
203                R.layout.message_list_item_footer, listView, false);
204
205        if (savedInstanceState != null) {
206            // Fragment doesn't have this method.  Call it manually.
207            loadState(savedInstanceState);
208        }
209    }
210
211    @Override
212    public void onStart() {
213        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
214            Log.d(Email.LOG_TAG, "MessageListFragment onStart");
215        }
216        super.onStart();
217    }
218
219    @Override
220    public void onResume() {
221        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
222            Log.d(Email.LOG_TAG, "MessageListFragment onResume");
223        }
224        super.onResume();
225        mResumed = true;
226
227        // If we're recovering from the stopped state, we don't have to reload.
228        // (when mOpenRequested = false)
229        if (mMailboxId != -1 && mOpenRequested) {
230            startLoading();
231        }
232    }
233
234    @Override
235    public void onPause() {
236        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
237            Log.d(Email.LOG_TAG, "MessageListFragment onPause");
238        }
239        mResumed = false;
240        super.onStop();
241        mSavedListState = new Utility.ListStateSaver(getListView());
242    }
243
244    @Override
245    public void onStop() {
246        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
247            Log.d(Email.LOG_TAG, "MessageListFragment onStop");
248        }
249        super.onStop();
250    }
251
252    @Override
253    public void onDestroy() {
254        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
255            Log.d(Email.LOG_TAG, "MessageListFragment onDestroy");
256        }
257        mRefreshManager.unregisterListener(mRefreshListener);
258        super.onDestroy();
259    }
260
261    @Override
262    public void onSaveInstanceState(Bundle outState) {
263        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
264            Log.d(Email.LOG_TAG, "MessageListFragment onSaveInstanceState");
265        }
266        super.onSaveInstanceState(outState);
267        mListAdapter.onSaveInstanceState(outState);
268        outState.putParcelable(BUNDLE_LIST_STATE, new Utility.ListStateSaver(getListView()));
269    }
270
271    // Unit tests use it
272    /* package */void loadState(Bundle savedInstanceState) {
273        mListAdapter.loadState(savedInstanceState);
274        mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
275    }
276
277    public void setCallback(Callback callback) {
278        mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE;
279    }
280
281    /**
282     * Called by an Activity to open an mailbox.
283     *
284     * @param mailboxId the ID of a mailbox, or one of "special" mailbox IDs like
285     *     {@link Mailbox#QUERY_ALL_INBOXES}.  -1 is not allowed.
286     */
287    public void openMailbox(long mailboxId) {
288        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
289            Log.d(Email.LOG_TAG, "MessageListFragment openMailbox");
290        }
291        if (mailboxId == -1) {
292            throw new InvalidParameterException();
293        }
294        if (mMailboxId == mailboxId) {
295            return;
296        }
297
298        mOpenRequested = true;
299        mMailboxId = mailboxId;
300
301        onDeselectAll();
302        if (mResumed) {
303            startLoading();
304        }
305    }
306
307    /* package */MessagesAdapter getAdapterForTest() {
308        return mListAdapter;
309    }
310
311    /**
312     * @return the account id or -1 if it's unknown yet.  It's also -1 if it's a magic mailbox.
313     */
314    public long getAccountId() {
315        return (mMailbox == null) ? -1 : mMailbox.mAccountKey;
316    }
317
318    /**
319     * @return the mailbox id, which is the value set to {@link #openMailbox}.
320     * (Meaning it will never return -1, but may return special values,
321     * eg {@link Mailbox#QUERY_ALL_INBOXES}).
322     */
323    public long getMailboxId() {
324        return mMailboxId;
325    }
326
327    /**
328     * @return true if the mailbox is a "special" box.  (e.g. combined inbox, all starred, etc.)
329     */
330    public boolean isMagicMailbox() {
331        return mMailboxId < 0;
332    }
333
334    /**
335     * @return the number of messages that are currently selecteed.
336     */
337    public int getSelectedCount() {
338        return mListAdapter.getSelectedSet().size();
339    }
340
341    /**
342     * @return true if the list is in the "selection" mode.
343     */
344    private boolean isInSelectionMode() {
345        return mSelectionMode != null;
346    }
347
348    @Override
349    public void onClick(View v) {
350        switch (v.getId()) {
351            case R.id.send_messages:
352                onSendPendingMessages();
353                break;
354        }
355    }
356
357    /**
358     * Called when a message is clicked.
359     */
360    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
361        if (view != mListFooterView) {
362            MessageListItem itemView = (MessageListItem) view;
363            if (isInSelectionMode()) {
364                toggleSelection(itemView);
365            } else {
366                onMessageOpen(itemView.mMailboxId, id);
367            }
368        } else {
369            doFooterClick();
370        }
371    }
372
373    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
374        if (view != mListFooterView) {
375            if (isInSelectionMode()) {
376                // Already in selection mode.  Ignore.
377            } else {
378                toggleSelection((MessageListItem) view);
379                return true;
380            }
381        }
382        return false;
383    }
384
385    private void toggleSelection(MessageListItem itemView) {
386        mListAdapter.toggleSelected(itemView);
387    }
388
389    private void onMessageOpen(final long mailboxId, final long messageId) {
390        final int type;
391        if (mMailbox == null) { // Magic mailbox
392            if (mMailboxId == Mailbox.QUERY_ALL_DRAFTS) {
393                type = Callback.TYPE_DRAFT;
394            } else {
395                type = Callback.TYPE_REGULAR;
396            }
397        } else {
398            switch (mMailbox.mType) {
399                case EmailContent.Mailbox.TYPE_DRAFTS:
400                    type = Callback.TYPE_DRAFT;
401                    break;
402                case EmailContent.Mailbox.TYPE_TRASH:
403                    type = Callback.TYPE_TRASH;
404                    break;
405                default:
406                    type = Callback.TYPE_REGULAR;
407                    break;
408            }
409        }
410        mCallback.onMessageOpen(messageId, mailboxId, getMailboxId(), type);
411    }
412
413    public void onMultiToggleRead() {
414        onMultiToggleRead(mListAdapter.getSelectedSet());
415    }
416
417    public void onMultiToggleFavorite() {
418        onMultiToggleFavorite(mListAdapter.getSelectedSet());
419    }
420
421    public void onMultiDelete() {
422        onMultiDelete(mListAdapter.getSelectedSet());
423    }
424
425    /**
426     * Refresh the list.  NOOP for special mailboxes (e.g. combined inbox).
427     *
428     * Note: Manual refresh is enabled even for push accounts.
429     */
430    public void onRefresh() {
431        if (!mIsRefreshable) {
432            return;
433        }
434        long accountId = getAccountId();
435        if (accountId != -1) {
436            mRefreshManager.refreshMessageList(accountId, mMailboxId);
437        }
438    }
439
440    public void onDeselectAll() {
441        if ((mListAdapter == null) || (mListAdapter.getSelectedSet().size() == 0)) {
442            return;
443        }
444        mListAdapter.getSelectedSet().clear();
445        getListView().invalidateViews();
446        finishSelectionMode();
447    }
448
449    /**
450     * Load more messages.  NOOP for special mailboxes (e.g. combined inbox).
451     */
452    private void onLoadMoreMessages() {
453        long accountId = getAccountId();
454        if (accountId != -1) {
455            mRefreshManager.loadMoreMessages(accountId, mMailboxId);
456        }
457    }
458
459    /**
460     * @return if it's an outbox or "all outboxes".
461     *
462     * TODO make it private.  It's only used by MessageList, but the callsite is obsolete.
463     */
464    public boolean isOutbox() {
465        return (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX)
466            || ((mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_OUTBOX));
467    }
468
469    public void onSendPendingMessages() {
470        RefreshManager rm = RefreshManager.getInstance(mActivity);
471        if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) {
472            rm.sendPendingMessagesForAllAccounts();
473        } else if (mMailbox != null) { // Magic boxes don't have a specific account id.
474            rm.sendPendingMessages(mMailbox.mId);
475        }
476    }
477
478    private void onSetMessageRead(long messageId, boolean newRead) {
479        mController.setMessageRead(messageId, newRead);
480    }
481
482    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
483        mController.setMessageFavorite(messageId, newFavorite);
484    }
485
486    /**
487     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
488     * sense of the helper methods is "true=unread".
489     *
490     * @param selectedSet The current list of selected items
491     */
492    private void onMultiToggleRead(Set<Long> selectedSet) {
493        toggleMultiple(selectedSet, new MultiToggleHelper() {
494
495            public boolean getField(long messageId, Cursor c) {
496                return c.getInt(MessagesAdapter.COLUMN_READ) == 0;
497            }
498
499            public boolean setField(long messageId, Cursor c, boolean newValue) {
500                boolean oldValue = getField(messageId, c);
501                if (oldValue != newValue) {
502                    onSetMessageRead(messageId, !newValue);
503                    return true;
504                }
505                return false;
506            }
507        });
508    }
509
510    /**
511     * Toggles a set of favorites (stars)
512     *
513     * @param selectedSet The current list of selected items
514     */
515    private void onMultiToggleFavorite(Set<Long> selectedSet) {
516        toggleMultiple(selectedSet, new MultiToggleHelper() {
517
518            public boolean getField(long messageId, Cursor c) {
519                return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0;
520            }
521
522            public boolean setField(long messageId, Cursor c, boolean newValue) {
523                boolean oldValue = getField(messageId, c);
524                if (oldValue != newValue) {
525                    onSetMessageFavorite(messageId, newValue);
526                    return true;
527                }
528                return false;
529            }
530        });
531    }
532
533    private void onMultiDelete(Set<Long> selectedSet) {
534        // Clone the set, because deleting is going to thrash things
535        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
536        for (Long id : cloneSet) {
537            mController.deleteMessage(id, -1);
538        }
539        Toast.makeText(mActivity, mActivity.getResources().getQuantityString(
540                R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show();
541        selectedSet.clear();
542        // Message deletion is async... Can't refresh the list immediately.
543    }
544
545    private interface MultiToggleHelper {
546        /**
547         * Return true if the field of interest is "set".  If one or more are false, then our
548         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
549         * @param messageId the message id of the current message
550         * @param c the cursor, positioned to the item of interest
551         * @return true if the field at this row is "set"
552         */
553        public boolean getField(long messageId, Cursor c);
554
555        /**
556         * Set or clear the field of interest.  Return true if a change was made.
557         * @param messageId the message id of the current message
558         * @param c the cursor, positioned to the item of interest
559         * @param newValue the new value to be set at this row
560         * @return true if a change was actually made
561         */
562        public boolean setField(long messageId, Cursor c, boolean newValue);
563    }
564
565    /**
566     * Toggle multiple fields in a message, using the following logic:  If one or more fields
567     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
568     *
569     * @param selectedSet the set of messages that are selected
570     * @param helper functions to implement the specific getter & setter
571     * @return the number of messages that were updated
572     */
573    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
574        Cursor c = mListAdapter.getCursor();
575        boolean anyWereFound = false;
576        boolean allWereSet = true;
577
578        c.moveToPosition(-1);
579        while (c.moveToNext()) {
580            long id = c.getInt(MessagesAdapter.COLUMN_ID);
581            if (selectedSet.contains(Long.valueOf(id))) {
582                anyWereFound = true;
583                if (!helper.getField(id, c)) {
584                    allWereSet = false;
585                    break;
586                }
587            }
588        }
589
590        int numChanged = 0;
591
592        if (anyWereFound) {
593            boolean newValue = !allWereSet;
594            c.moveToPosition(-1);
595            while (c.moveToNext()) {
596                long id = c.getInt(MessagesAdapter.COLUMN_ID);
597                if (selectedSet.contains(Long.valueOf(id))) {
598                    if (helper.setField(id, c, newValue)) {
599                        ++numChanged;
600                    }
601                }
602            }
603        }
604
605        refreshList();
606
607        return numChanged;
608    }
609
610    /**
611     * Test selected messages for showing appropriate labels
612     * @param selectedSet
613     * @param column_id
614     * @param defaultflag
615     * @return true when the specified flagged message is selected
616     */
617    private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) {
618        Cursor c = mListAdapter.getCursor();
619        if (c == null || c.isClosed()) {
620            return false;
621        }
622        c.moveToPosition(-1);
623        while (c.moveToNext()) {
624            long id = c.getInt(MessagesAdapter.COLUMN_ID);
625            if (selectedSet.contains(Long.valueOf(id))) {
626                if (c.getInt(column_id) == (defaultflag ? 1 : 0)) {
627                    return true;
628                }
629            }
630        }
631        return false;
632    }
633
634    /**
635     * @return true if one or more non-starred messages are selected.
636     */
637    public boolean doesSelectionContainNonStarredMessage() {
638        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE,
639                false);
640    }
641
642    /**
643     * @return true if one or more read messages are selected.
644     */
645    public boolean doesSelectionContainReadMessage() {
646        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true);
647    }
648
649    /**
650     * Called by activity to indicate that the user explicitly opened the
651     * mailbox and it needs auto-refresh when it's first shown. TODO:
652     * {@link MessageList} needs to call this as well.
653     *
654     * TODO It's a bit ugly. We can remove this if this fragment "remembers" the current mailbox ID
655     * through configuration changes.
656     */
657    public void doAutoRefresh() {
658        mDoAutoRefresh = true;
659    }
660
661    /**
662     * Implements a timed refresh of "stale" mailboxes.  This should only happen when
663     * multiple conditions are true, including:
664     *   Only refreshable mailboxes.
665     *   Only when the user explicitly opens the mailbox (not onResume, for example)
666     *   Only for real, non-push mailboxes (c.f. manual refresh is still enabled for push accounts)
667     *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
668     */
669    private void autoRefreshStaleMailbox() {
670        if (!mDoAutoRefresh // Not explicitly open
671                || !mIsRefreshable // Not refreshable (special box such as drafts, or magic boxes)
672                || (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) // Push account
673                ) {
674            return;
675        }
676        mDoAutoRefresh = false;
677        if (!mRefreshManager.isMailboxStale(mMailboxId)) {
678            return;
679        }
680        onRefresh();
681    }
682
683    /** Implements {@link MessagesAdapter.Callback} */
684    @Override
685    public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) {
686        onSetMessageFavorite(itemView.mMessageId, newFavorite);
687    }
688
689    /** Implements {@link MessagesAdapter.Callback} */
690    @Override
691    public void onAdapterSelectedChanged(
692            MessageListItem itemView, boolean newSelected, int mSelectedCount) {
693        updateSelectionMode();
694    }
695
696    private void determineFooterMode() {
697        mListFooterMode = LIST_FOOTER_MODE_NONE;
698        if ((mMailbox == null) || (mMailbox.mType == Mailbox.TYPE_OUTBOX)
699                || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) {
700            return; // No footer
701        }
702        if (!mIsEasAccount) {
703            // IMAP, POP has "load more"
704            mListFooterMode = LIST_FOOTER_MODE_MORE;
705        }
706    }
707
708    private void addFooterView() {
709        ListView lv = getListView();
710        if (mListFooterView != null) {
711            lv.removeFooterView(mListFooterView);
712        }
713        determineFooterMode();
714        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
715
716            lv.addFooterView(mListFooterView);
717            lv.setAdapter(mListAdapter);
718
719            mListFooterProgress = mListFooterView.findViewById(R.id.progress);
720            mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
721
722            updateListFooter();
723        }
724    }
725
726    /**
727     * Set the list footer text based on mode and the current "network active" status
728     */
729    private void updateListFooter() {
730        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
731            int footerTextId = 0;
732            switch (mListFooterMode) {
733                case LIST_FOOTER_MODE_MORE:
734                    boolean active = mRefreshManager.isMessageListRefreshing(mMailboxId);
735                    footerTextId = active ? R.string.status_loading_messages
736                            : R.string.message_list_load_more_messages_action;
737                    mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE);
738                    break;
739            }
740            mListFooterText.setText(footerTextId);
741        }
742    }
743
744    /**
745     * Handle a click in the list footer, which changes meaning depending on what we're looking at.
746     */
747    private void doFooterClick() {
748        switch (mListFooterMode) {
749            case LIST_FOOTER_MODE_NONE: // should never happen
750                break;
751            case LIST_FOOTER_MODE_MORE:
752                onLoadMoreMessages();
753                break;
754        }
755    }
756
757    private void hideSendPanel() {
758        mSendPanel.setVisibility(View.GONE);
759    }
760
761    private void showSendPanelIfNecessary() {
762        final boolean show =
763                isOutbox()
764                && (mListAdapter != null)
765                && (mListAdapter.getCount() > 0);
766        mSendPanel.setVisibility(show ? View.VISIBLE : View.GONE);
767    }
768
769    private void startLoading() {
770        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
771            Log.d(Email.LOG_TAG, "MessageListFragment startLoading");
772        }
773        mOpenRequested = false;
774
775        // Clear the list. (ListFragment will show the "Loading" animation)
776        setListShown(false);
777        hideSendPanel();
778
779        // Start loading...
780        final LoaderManager lm = getLoaderManager();
781
782        // If we're loading a different mailbox, discard the previous result.
783        if ((mLastLoadedMailboxId != -1) && (mLastLoadedMailboxId != mMailboxId)) {
784            lm.stopLoader(LOADER_ID_MAILBOX_LOADER);
785            lm.stopLoader(LOADER_ID_MESSAGES_LOADER);
786        }
787        lm.initLoader(LOADER_ID_MAILBOX_LOADER, null, new MailboxAccountLoaderCallback());
788    }
789
790    /**
791     * Loader callbacks for {@link MailboxAccountLoader}.
792     */
793    private class MailboxAccountLoaderCallback implements LoaderManager.LoaderCallbacks<
794            MailboxAccountLoader.Result> {
795        @Override
796        public Loader<MailboxAccountLoader.Result> onCreateLoader(int id, Bundle args) {
797            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
798                Log.d(Email.LOG_TAG,
799                        "MessageListFragment onCreateLoader(mailbox) mailboxId=" + mMailboxId);
800            }
801            return new MailboxAccountLoader(getActivity().getApplicationContext(), mMailboxId);
802        }
803
804        @Override
805        public void onLoadFinished(Loader<MailboxAccountLoader.Result> loader,
806                MailboxAccountLoader.Result result) {
807            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
808                Log.d(Email.LOG_TAG, "MessageListFragment onLoadFinished(mailbox) mailboxId="
809                        + mMailboxId);
810            }
811            if (!result.mIsFound) {
812                mCallback.onMailboxNotFound();
813                return;
814            }
815
816            mLastLoadedMailboxId = mMailboxId;
817            mAccount = result.mAccount;
818            mMailbox = result.mMailbox;
819            mIsEasAccount = result.mIsEasAccount;
820            mIsRefreshable = result.mIsRefreshable;
821            getLoaderManager().initLoader(LOADER_ID_MESSAGES_LOADER, null,
822                    new MessagesLoaderCallback());
823        }
824    }
825
826    /**
827     * Reload the data and refresh the list view.
828     */
829    private void refreshList() {
830        getLoaderManager().restartLoader(LOADER_ID_MESSAGES_LOADER, null,
831                new MessagesLoaderCallback());
832    }
833
834    /**
835     * Loader callbacks for message list.
836     */
837    private class MessagesLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
838        @Override
839        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
840            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
841                Log.d(Email.LOG_TAG,
842                        "MessageListFragment onCreateLoader(messages) mailboxId=" + mMailboxId);
843            }
844
845            // Reset new message count.
846            // TODO Do it in onLoadFinished(). Unfortunately
847            // resetNewMessageCount() ends up a
848            // db operation, which causes a onContentChanged notification, which
849            // makes cursor
850            // loaders to requery. Until we fix ContentProvider (don't notify
851            // unrelated cursors)
852            // we need to do it here.
853            resetNewMessageCount(mActivity, mMailboxId, getAccountId());
854            return MessagesAdapter.createLoader(getActivity(), mMailboxId);
855        }
856
857        @Override
858        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
859            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
860                Log.d(Email.LOG_TAG,
861                        "MessageListFragment onLoadFinished(messages) mailboxId=" + mMailboxId);
862            }
863
864            // Save list view state (primarily scroll position)
865            final ListView lv = getListView();
866            final Utility.ListStateSaver lss;
867            if (mSavedListState != null) {
868                lss = mSavedListState;
869                mSavedListState = null;
870            } else {
871                lss = new Utility.ListStateSaver(lv);
872            }
873
874            // Update the list
875            mListAdapter.changeCursor(cursor);
876            setListAdapter(mListAdapter);
877            setListShown(true);
878
879            // Various post processing...
880            // (resetNewMessageCount should be here. See above.)
881            autoRefreshStaleMailbox();
882            addFooterView();
883            updateSelectionMode();
884            showSendPanelIfNecessary();
885
886            // Restore the state -- it has to be the last.
887            // (Some of the "post processing" resets the state.)
888            lss.restore(lv);
889        }
890    }
891
892    /**
893     * Reset the "new message" count.
894     * <ul>
895     * <li>If {@code mailboxId} is {@link Mailbox#QUERY_ALL_INBOXES}, reset the
896     * counts of all accounts.
897     * <li>If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code
898     * accountId} is valid, reset the count of the specified account.
899     * </ul>
900     */
901    /* protected */static void resetNewMessageCount(
902            Context context, long mailboxId, long accountId) {
903        if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
904            MailService.resetNewMessageCount(context, -1);
905        } else if (mailboxId >= 0 && accountId != -1) {
906            MailService.resetNewMessageCount(context, accountId);
907        }
908    }
909
910    /**
911     * Show/hide the "selection" action mode, according to the number of selected messages,
912     * and update the content (title and menus) if necessary.
913     */
914    public void updateSelectionMode() {
915        final int numSelected = getSelectedCount();
916        if (numSelected == 0) {
917            finishSelectionMode();
918            return;
919        }
920        if (isInSelectionMode()) {
921            updateSelectionModeView();
922        } else {
923            getActivity().startActionMode(new SelectionModeCallback());
924        }
925    }
926
927    /** Finish the "selection" action mode */
928    private void finishSelectionMode() {
929        if (isInSelectionMode()) {
930            mSelectionMode.finish();
931            mSelectionMode = null;
932        }
933    }
934
935    /** Update the "selection" action mode bar */
936    private void updateSelectionModeView() {
937        mSelectionMode.invalidate();
938    }
939
940    private class SelectionModeCallback implements ActionMode.Callback {
941        private MenuItem mMarkRead;
942        private MenuItem mMarkUnread;
943        private MenuItem mAddStar;
944        private MenuItem mRemoveStar;
945
946        @Override
947        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
948            mSelectionMode = mode;
949
950            MenuInflater inflater = getActivity().getMenuInflater();
951            inflater.inflate(R.menu.message_list_selection_mode, menu);
952            mMarkRead = menu.findItem(R.id.mark_read);
953            mMarkUnread = menu.findItem(R.id.mark_unread);
954            mAddStar = menu.findItem(R.id.add_star);
955            mRemoveStar = menu.findItem(R.id.remove_star);
956            return true;
957        }
958
959        @Override
960        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
961            int num = getSelectedCount();
962            // Set title -- "# selected"
963            mSelectionMode.setTitle(getActivity().getResources().getQuantityString(
964                    R.plurals.message_view_selected_message_count, num, num));
965
966            // Show appropriate menu items.
967            boolean nonStarExists = doesSelectionContainNonStarredMessage();
968            boolean readExists = doesSelectionContainReadMessage();
969            mMarkRead.setVisible(!readExists);
970            mMarkUnread.setVisible(readExists);
971            mAddStar.setVisible(nonStarExists);
972            mRemoveStar.setVisible(!nonStarExists);
973            return true;
974        }
975
976        @Override
977        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
978            switch (item.getItemId()) {
979                case R.id.mark_read:
980                case R.id.mark_unread:
981                    onMultiToggleRead();
982                    break;
983                case R.id.add_star:
984                case R.id.remove_star:
985                    onMultiToggleFavorite();
986                    break;
987                case R.id.delete:
988                    onMultiDelete();
989                    break;
990            }
991            return true;
992        }
993
994        @Override
995        public void onDestroyActionMode(ActionMode mode) {
996            onDeselectAll();
997            mSelectionMode = null;
998        }
999    }
1000
1001    private class RefreshListener implements RefreshManager.Listener {
1002        @Override
1003        public void onMessagingError(long accountId, long mailboxId, String message) {
1004        }
1005
1006        @Override
1007        public void onRefreshStatusChanged(long accountId, long mailboxId) {
1008            updateListFooter();
1009        }
1010    }
1011
1012    /**
1013     * Object that holds the current state (right now it's only the ListView state) of the fragment.
1014     *
1015     * Used by {@link MessageListXLFragmentManager} to preserve scroll position through fragment
1016     * transitions.
1017     */
1018    public static class State implements Parcelable {
1019        private final ListStateSaver mListState;
1020
1021        private State(Parcel p) {
1022            mListState = p.readParcelable(null);
1023        }
1024
1025        private State(MessageListFragment messageListFragment) {
1026            mListState = new Utility.ListStateSaver(messageListFragment.getListView());
1027        }
1028
1029        public void restore(MessageListFragment messageListFragment) {
1030            messageListFragment.mSavedListState = mListState;
1031        }
1032
1033        @Override
1034        public int describeContents() {
1035            return 0;
1036        }
1037
1038        @Override
1039        public void writeToParcel(Parcel dest, int flags) {
1040            dest.writeParcelable(mListState, flags);
1041        }
1042
1043        public static final Parcelable.Creator<State> CREATOR
1044                = new Parcelable.Creator<State>() {
1045                    public State createFromParcel(Parcel in) {
1046                        return new State(in);
1047                    }
1048
1049                    public State[] newArray(int size) {
1050                        return new State[size];
1051                    }
1052                };
1053    }
1054
1055    public State getState() {
1056        return new State(this);
1057    }
1058}