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