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