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