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