MessageListFragment.java revision fe7dd2df56a1439fd8e2676ae1479f9a4f30eb26
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            // Reset new message count.
893            // TODO Do it in onLoadFinished(). Unfortunately
894            // resetNewMessageCount() ends up a
895            // db operation, which causes a onContentChanged notification, which
896            // makes cursor
897            // loaders to requery. Until we fix ContentProvider (don't notify
898            // unrelated cursors)
899            // we need to do it here.
900            resetNewMessageCount(mActivity, mMailboxId, getAccountId());
901            return MessagesAdapter.createLoader(getActivity(), mMailboxId);
902        }
903
904        @Override
905        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
906            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
907                Log.d(Email.LOG_TAG,
908                        "MessageListFragment onLoadFinished(messages) mailboxId=" + mMailboxId);
909            }
910
911            // Save list view state (primarily scroll position)
912            final ListView lv = getListView();
913            final Utility.ListStateSaver lss;
914            if (mSavedListState != null) {
915                lss = mSavedListState;
916                mSavedListState = null;
917            } else {
918                lss = new Utility.ListStateSaver(lv);
919            }
920
921            // Update the list
922            mListAdapter.changeCursor(cursor);
923            setListAdapter(mListAdapter);
924            setListShown(true);
925
926            // Various post processing...
927            // (resetNewMessageCount should be here. See above.)
928            autoRefreshStaleMailbox();
929            addFooterView();
930            updateSelectionMode();
931            showSendPanelIfNecessary();
932
933            // Restore the state -- it has to be the last.
934            // (Some of the "post processing" resets the state.)
935            lss.restore(lv);
936        }
937    }
938
939    /**
940     * Reset the "new message" count.
941     * <ul>
942     * <li>If {@code mailboxId} is {@link Mailbox#QUERY_ALL_INBOXES}, reset the
943     * counts of all accounts.
944     * <li>If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code
945     * accountId} is valid, reset the count of the specified account.
946     * </ul>
947     */
948    /* protected */static void resetNewMessageCount(
949            Context context, long mailboxId, long accountId) {
950        if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
951            MailService.resetNewMessageCount(context, -1);
952        } else if (mailboxId >= 0 && accountId != -1) {
953            MailService.resetNewMessageCount(context, accountId);
954        }
955    }
956
957    /**
958     * Show/hide the "selection" action mode, according to the number of selected messages,
959     * and update the content (title and menus) if necessary.
960     */
961    public void updateSelectionMode() {
962        final int numSelected = getSelectedCount();
963        if (numSelected == 0) {
964            finishSelectionMode();
965            return;
966        }
967        if (isInSelectionMode()) {
968            updateSelectionModeView();
969        } else {
970            getActivity().startActionMode(new SelectionModeCallback());
971        }
972    }
973
974    /** Finish the "selection" action mode */
975    private void finishSelectionMode() {
976        if (isInSelectionMode()) {
977            mSelectionMode.finish();
978            mSelectionMode = null;
979        }
980    }
981
982    /** Update the "selection" action mode bar */
983    private void updateSelectionModeView() {
984        mSelectionMode.invalidate();
985    }
986
987    private class SelectionModeCallback implements ActionMode.Callback {
988        private MenuItem mMarkRead;
989        private MenuItem mMarkUnread;
990        private MenuItem mAddStar;
991        private MenuItem mRemoveStar;
992
993        @Override
994        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
995            mSelectionMode = mode;
996
997            MenuInflater inflater = getActivity().getMenuInflater();
998            inflater.inflate(R.menu.message_list_selection_mode, menu);
999            mMarkRead = menu.findItem(R.id.mark_read);
1000            mMarkUnread = menu.findItem(R.id.mark_unread);
1001            mAddStar = menu.findItem(R.id.add_star);
1002            mRemoveStar = menu.findItem(R.id.remove_star);
1003            return true;
1004        }
1005
1006        @Override
1007        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1008            int num = getSelectedCount();
1009            // Set title -- "# selected"
1010            mSelectionMode.setTitle(getActivity().getResources().getQuantityString(
1011                    R.plurals.message_view_selected_message_count, num, num));
1012
1013            // Show appropriate menu items.
1014            boolean nonStarExists = doesSelectionContainNonStarredMessage();
1015            boolean readExists = doesSelectionContainReadMessage();
1016            mMarkRead.setVisible(!readExists);
1017            mMarkUnread.setVisible(readExists);
1018            mAddStar.setVisible(nonStarExists);
1019            mRemoveStar.setVisible(!nonStarExists);
1020            return true;
1021        }
1022
1023        @Override
1024        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1025            switch (item.getItemId()) {
1026                case R.id.mark_read:
1027                case R.id.mark_unread:
1028                    onMultiToggleRead();
1029                    break;
1030                case R.id.add_star:
1031                case R.id.remove_star:
1032                    onMultiToggleFavorite();
1033                    break;
1034                case R.id.delete:
1035                    onMultiDelete();
1036                    break;
1037                case R.id.move:
1038                    onMultiMove();
1039                    break;
1040            }
1041            return true;
1042        }
1043
1044        @Override
1045        public void onDestroyActionMode(ActionMode mode) {
1046            onDeselectAll();
1047            mSelectionMode = null;
1048        }
1049    }
1050
1051    private class RefreshListener implements RefreshManager.Listener {
1052        @Override
1053        public void onMessagingError(long accountId, long mailboxId, String message) {
1054        }
1055
1056        @Override
1057        public void onRefreshStatusChanged(long accountId, long mailboxId) {
1058            updateListFooter();
1059        }
1060    }
1061
1062    /**
1063     * Object that holds the current state (right now it's only the ListView state) of the fragment.
1064     *
1065     * Used by {@link MessageListXLFragmentManager} to preserve scroll position through fragment
1066     * transitions.
1067     */
1068    public static class State implements Parcelable {
1069        private final ListStateSaver mListState;
1070
1071        private State(Parcel p) {
1072            mListState = p.readParcelable(null);
1073        }
1074
1075        private State(MessageListFragment messageListFragment) {
1076            mListState = new Utility.ListStateSaver(messageListFragment.getListView());
1077        }
1078
1079        public void restore(MessageListFragment messageListFragment) {
1080            messageListFragment.mSavedListState = mListState;
1081        }
1082
1083        @Override
1084        public int describeContents() {
1085            return 0;
1086        }
1087
1088        @Override
1089        public void writeToParcel(Parcel dest, int flags) {
1090            dest.writeParcelable(mListState, flags);
1091        }
1092
1093        public static final Parcelable.Creator<State> CREATOR
1094                = new Parcelable.Creator<State>() {
1095                    public State createFromParcel(Parcel in) {
1096                        return new State(in);
1097                    }
1098
1099                    public State[] newArray(int size) {
1100                        return new State[size];
1101                    }
1102                };
1103    }
1104
1105    public State getState() {
1106        return new State(this);
1107    }
1108}