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