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