MessageListFragment.java revision 8ade2fe010797be45d5c0f9023e5d76bcc3b50a8
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.provider.EmailContent;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.Mailbox;
26import com.android.email.provider.EmailContent.MailboxColumns;
27import com.android.email.provider.EmailContent.MessageColumns;
28import com.android.email.service.MailService;
29
30import android.app.Activity;
31import android.app.Fragment;
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.Context;
35import android.database.Cursor;
36import android.net.Uri;
37import android.os.AsyncTask;
38import android.os.Bundle;
39import android.os.Handler;
40import android.view.ContextMenu;
41import android.view.LayoutInflater;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ContextMenu.ContextMenuInfo;
47import android.widget.AdapterView;
48import android.widget.ListView;
49import android.widget.TextView;
50import android.widget.Toast;
51import android.widget.AdapterView.OnItemClickListener;
52
53import java.security.InvalidParameterException;
54import java.util.HashSet;
55import java.util.Set;
56
57public class MessageListFragment extends Fragment implements OnItemClickListener,
58        MessagesAdapter.Callback {
59    private static final String STATE_SELECTED_ITEM_TOP =
60            "com.android.email.activity.MessageList.selectedItemTop";
61    private static final String STATE_SELECTED_POSITION =
62            "com.android.email.activity.MessageList.selectedPosition";
63    private static final String STATE_CHECKED_ITEMS =
64            "com.android.email.activity.MessageList.checkedItems";
65
66    // UI Support
67    private Activity mActivity;
68    private Callback mCallback = EmptyCallback.INSTANCE;
69    private ListView mListView;
70    private View mListFooterView;
71    private TextView mListFooterText;
72    private View mListFooterProgress;
73
74    private static final int LIST_FOOTER_MODE_NONE = 0;
75    private static final int LIST_FOOTER_MODE_REFRESH = 1;
76    private static final int LIST_FOOTER_MODE_MORE = 2;
77    private static final int LIST_FOOTER_MODE_SEND = 3;
78    private int mListFooterMode;
79
80    private MessagesAdapter mListAdapter;
81
82    // DB access
83    private ContentResolver mResolver;
84    private long mAccountId = -1;
85    private long mMailboxId = -1;
86    private LoadMessagesTask mLoadMessagesTask;
87    private SetFooterTask mSetFooterTask;
88
89    /* package */ static final String[] MESSAGE_PROJECTION = new String[] {
90        EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
91        MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
92        MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
93        MessageColumns.FLAGS,
94    };
95
96    // Controller access
97    private Controller mController;
98
99    // Misc members
100    private Boolean mPushModeMailbox = null;
101    private int mSavedItemTop = 0;
102    private int mSavedItemPosition = -1;
103    private int mFirstSelectedItemTop = 0;
104    private int mFirstSelectedItemPosition = -1;
105    private int mFirstSelectedItemHeight = -1;
106    private boolean mCanAutoRefresh;
107
108    /**
109     * Callback interface that owning activities must implement
110     */
111    public interface Callback {
112        /**
113         * Called when selected messages have been changed.
114         */
115        public void onSelectionChanged();
116
117        /**
118         * Called when the specified mailbox does not exist.
119         */
120        public void onMailboxNotFound();
121
122        /**
123         * Called when the user wants to open a message.
124         * Note {@code mailboxId} is of the actual mailbox of the message, which is different from
125         * {@link MessageListFragment#getMailboxId} if it's magic mailboxes.
126         */
127        public void onMessageOpen(final long messageId, final long mailboxId);
128
129        /**
130         * Called when the user wants to reply to a message.
131         */
132        public void onMessageReply(long messageId);
133
134        /**
135         * Called when the user wants to reply-all to a message.
136         */
137        public void onMessageReplyAll(long messageId);
138
139        /**
140         * Called when the user wants to forward a message.
141         */
142        public void onMessageForward(long messageId);
143    }
144
145    private static final class EmptyCallback implements Callback {
146        public static final Callback INSTANCE = new EmptyCallback();
147
148        public void onMailboxNotFound() {
149        }
150        public void onSelectionChanged() {
151        }
152        public void onMessageForward(long messageId) {
153        }
154        public void onMessageOpen(long messageId, long mailboxId) {
155        }
156        public void onMessageReply(long messageId) {
157        }
158        public void onMessageReplyAll(long messageId) {
159        }
160    }
161
162    private ListView getListView() {
163        return mListView;
164    }
165
166    private MenuInflater getMenuInflater() {
167        return mActivity.getMenuInflater();
168    }
169
170    /* package */ MessagesAdapter getAdapterForTest() {
171        return mListAdapter;
172    }
173
174    /**
175     * @return the account id or -1 if it's unknown yet.  It's also -1 if it's a magic mailbox.
176     */
177    public long getAccountId() {
178        return mAccountId;
179    }
180
181    /**
182     * @return the mailbox id, which is the value set to {@link #openMailbox(long, long)}.
183     * (Meaning it will never return -1, but may return special values,
184     * eg {@link Mailbox#QUERY_ALL_INBOXES}).
185     */
186    public long getMailboxId() {
187        return mMailboxId;
188    }
189
190    /**
191     * @return true if the mailbox is a "special" box.  (e.g. combined inbox, all starred, etc.)
192     */
193    public boolean isMagicMailbox() {
194        return mMailboxId < 0;
195    }
196
197    /**
198     * @return if it's an outbox.
199     */
200    public boolean isOutbox() {
201        return mListFooterMode == LIST_FOOTER_MODE_SEND;
202    }
203
204    /**
205     * @return the number of messages that are currently selecteed.
206     */
207    public int getSelectedCount() {
208        return mListAdapter.getSelectedSet().size();
209    }
210
211    @Override
212    public void onCreate(Bundle savedInstanceState) {
213        super.onCreate(savedInstanceState);
214        mActivity = getActivity();
215        mResolver = mActivity.getContentResolver();
216        mController = Controller.getInstance(mActivity);
217        mCanAutoRefresh = true;
218    }
219
220    @Override
221    public View onCreateView(LayoutInflater inflater, ViewGroup container,
222            Bundle savedInstanceState) {
223        mListView = (ListView) inflater.inflate(R.layout.message_list_fragment, container, false);
224        mListView.setOnItemClickListener(this);
225        mListView.setItemsCanFocus(false);
226        registerForContextMenu(mListView);
227
228        mListAdapter = new MessagesAdapter(mActivity, new Handler(), this);
229        mListView.setAdapter(mListAdapter);
230
231        mListFooterView = inflater.inflate(R.layout.message_list_item_footer, mListView, false);
232
233        // TODO extend this to properly deal with multiple mailboxes, cursor, etc.
234
235        return mListView;
236    }
237
238    @Override
239    public void onReady(Bundle savedInstanceState) {
240        super.onReady(savedInstanceState);
241        if (savedInstanceState != null) {
242            // Fragment doesn't have this method.  Call it manually.
243            onRestoreInstanceState(savedInstanceState);
244        }
245    }
246
247    public void setCallback(Callback callback) {
248        mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE;
249    }
250
251    /**
252     * Open an mailbox.
253     *
254     * @param accountId account id of the mailbox, if already known.  Pass -1 if unknown or
255     *     {@code mailboxId} is of a special mailbox.  If -1 is passed, this fragment will find it
256     *     using {@code mailboxId}, which the activity can get later with {@link #getAccountId()}.
257     *     Passing -1 is always safe, but we can skip a database lookup if specified.
258     *
259     * @param mailboxId the ID of a mailbox, or one of "special" mailbox IDs like
260     *     {@link Mailbox#QUERY_ALL_INBOXES}.  -1 is not allowed.
261     */
262    public void openMailbox(long accountId, long mailboxId) {
263        if (mailboxId == -1) {
264            throw new InvalidParameterException();
265        }
266        mAccountId = accountId;
267        mMailboxId = mailboxId;
268
269        Utility.cancelTaskInterrupt(mLoadMessagesTask);
270        mLoadMessagesTask = new LoadMessagesTask(mailboxId, accountId);
271        mLoadMessagesTask.execute();
272    }
273
274    @Override
275    public void onResume() {
276        super.onResume();
277        restoreListPosition();
278        autoRefreshStaleMailbox();
279    }
280
281    @Override
282    public void onDestroy() {
283        Utility.cancelTaskInterrupt(mLoadMessagesTask);
284        mLoadMessagesTask = null;
285        Utility.cancelTaskInterrupt(mSetFooterTask);
286        mSetFooterTask = null;
287
288        if (mListAdapter != null) {
289            mListAdapter.changeCursor(null);
290        }
291        super.onDestroy();
292    }
293
294    @Override
295    public void onSaveInstanceState(Bundle outState) {
296        super.onSaveInstanceState(outState);
297        saveListPosition();
298        outState.putInt(STATE_SELECTED_POSITION, mSavedItemPosition);
299        outState.putInt(STATE_SELECTED_ITEM_TOP, mSavedItemTop);
300        Set<Long> checkedset = mListAdapter.getSelectedSet();
301        long[] checkedarray = new long[checkedset.size()];
302        int i = 0;
303        for (Long l : checkedset) {
304            checkedarray[i] = l;
305            i++;
306        }
307        outState.putLongArray(STATE_CHECKED_ITEMS, checkedarray);
308    }
309
310    // Unit tests use it
311    /* package */ void onRestoreInstanceState(Bundle savedInstanceState) {
312        mSavedItemTop = savedInstanceState.getInt(STATE_SELECTED_ITEM_TOP, 0);
313        mSavedItemPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, -1);
314        Set<Long> checkedset = mListAdapter.getSelectedSet();
315        for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) {
316            checkedset.add(l);
317        }
318    }
319
320    /**
321     * Save the focused list item.
322     *
323     * TODO It's not really working.  Fix it.
324     */
325    private void saveListPosition() {
326        mSavedItemPosition = getListView().getSelectedItemPosition();
327        if (mSavedItemPosition >= 0 && getListView().isSelected()) {
328            mSavedItemTop = getListView().getSelectedView().getTop();
329        } else {
330            mSavedItemPosition = getListView().getFirstVisiblePosition();
331            if (mSavedItemPosition >= 0) {
332                mSavedItemTop = 0;
333                View topChild = getListView().getChildAt(0);
334                if (topChild != null) {
335                    mSavedItemTop = topChild.getTop();
336                }
337            }
338        }
339    }
340
341    /**
342     * Restore the focused list item.
343     *
344     * TODO It's not really working.  Fix it.
345     */
346    private void restoreListPosition() {
347        if (mSavedItemPosition >= 0 && mSavedItemPosition < getListView().getCount()) {
348            getListView().setSelectionFromTop(mSavedItemPosition, mSavedItemTop);
349            mSavedItemPosition = -1;
350            mSavedItemTop = 0;
351        }
352    }
353
354    /**
355     * Called when a message is clicked.
356     */
357    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
358        if (view != mListFooterView) {
359            MessageListItem itemView = (MessageListItem) view;
360            mCallback.onMessageOpen(id, itemView.mMailboxId);
361        } else {
362            doFooterClick();
363        }
364    }
365
366    public void onMultiToggleRead() {
367        onMultiToggleRead(mListAdapter.getSelectedSet());
368    }
369
370    public void onMultiToggleFavorite() {
371        onMultiToggleFavorite(mListAdapter.getSelectedSet());
372    }
373
374    public void onMultiDelete() {
375        onMultiDelete(mListAdapter.getSelectedSet());
376    }
377
378    @Override
379    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
380        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
381        // There is no context menu for the list footer
382        if (info.targetView == mListFooterView) {
383            return;
384        }
385        MessageListItem itemView = (MessageListItem) info.targetView;
386
387        Cursor c = (Cursor) getListView().getItemAtPosition(info.position);
388        String messageName = c.getString(MessagesAdapter.COLUMN_SUBJECT);
389
390        menu.setHeaderTitle(messageName);
391
392        // TODO: There is probably a special context menu for the trash
393        Mailbox mailbox = Mailbox.restoreMailboxWithId(mActivity, itemView.mMailboxId);
394        if (mailbox == null) {
395            return;
396        }
397
398        switch (mailbox.mType) {
399            case EmailContent.Mailbox.TYPE_DRAFTS:
400                getMenuInflater().inflate(R.menu.message_list_context_drafts, menu);
401                break;
402            case EmailContent.Mailbox.TYPE_OUTBOX:
403                getMenuInflater().inflate(R.menu.message_list_context_outbox, menu);
404                break;
405            case EmailContent.Mailbox.TYPE_TRASH:
406                getMenuInflater().inflate(R.menu.message_list_context_trash, menu);
407                break;
408            default:
409                getMenuInflater().inflate(R.menu.message_list_context, menu);
410                // The default menu contains "mark as read".  If the message is read, change
411                // the menu text to "mark as unread."
412                if (itemView.mRead) {
413                    menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action);
414                }
415                break;
416        }
417    }
418
419    @Override
420    public boolean onContextItemSelected(MenuItem item) {
421        AdapterView.AdapterContextMenuInfo info =
422                (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
423        MessageListItem itemView = (MessageListItem) info.targetView;
424
425        switch (item.getItemId()) {
426            case R.id.open:
427                mCallback.onMessageOpen(info.id, itemView.mMailboxId);
428                return true;
429            case R.id.delete:
430                // Don't use this.mAccountId, which can be -1 in magic mailboxes.
431                onMessageDelete(info.id, itemView.mAccountId);
432                return true;
433            case R.id.reply:
434                mCallback.onMessageReply(itemView.mMessageId);
435                return true;
436            case R.id.reply_all:
437                mCallback.onMessageReplyAll(itemView.mMessageId);
438                return true;
439            case R.id.forward:
440                mCallback.onMessageForward(itemView.mMessageId);
441                return true;
442            case R.id.mark_as_read:
443                onSetMessageRead(info.id, !itemView.mRead);
444                return true;
445        }
446        return false;
447    }
448
449    /**
450     * Refresh the list.  NOOP for special mailboxes (e.g. combined inbox).
451     */
452    public void onRefresh() {
453        if (!isMagicMailbox()) {
454            // Note we can use mAccountId here because it's not a magic mailbox, which doesn't have
455            // a specific account id.
456            mController.updateMailbox(mAccountId, mMailboxId);
457        }
458    }
459
460    public void onDeselectAll() {
461        mListAdapter.getSelectedSet().clear();
462        getListView().invalidateViews();
463        mCallback.onSelectionChanged();
464    }
465
466    /**
467     * Load more messages.  NOOP for special mailboxes (e.g. combined inbox).
468     */
469    private void onLoadMoreMessages() {
470        if (!isMagicMailbox()) {
471            mController.loadMoreMessages(mMailboxId);
472        }
473    }
474
475    public void onSendPendingMessages() {
476        if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) {
477            mController.sendPendingMessagesForAllAccounts(mActivity);
478        } else if (!isMagicMailbox()) { // Magic boxes don't have a specific account id.
479            mController.sendPendingMessages(getAccountId());
480        }
481    }
482
483    private void onMessageDelete(long messageId, long accountId) {
484        // Don't use this.mAccountId, which can be null in magic mailboxes.
485        mController.deleteMessage(messageId, accountId);
486        Utility.showToast(mActivity, mActivity.getResources().getQuantityString(
487                R.plurals.message_deleted_toast, 1));
488    }
489
490    private void onSetMessageRead(long messageId, boolean newRead) {
491        mController.setMessageRead(messageId, newRead);
492    }
493
494    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
495        mController.setMessageFavorite(messageId, newFavorite);
496    }
497
498    /**
499     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
500     * sense of the helper methods is "true=unread".
501     *
502     * @param selectedSet The current list of selected items
503     */
504    private void onMultiToggleRead(Set<Long> selectedSet) {
505        toggleMultiple(selectedSet, new MultiToggleHelper() {
506
507            public boolean getField(long messageId, Cursor c) {
508                return c.getInt(MessagesAdapter.COLUMN_READ) == 0;
509            }
510
511            public boolean setField(long messageId, Cursor c, boolean newValue) {
512                boolean oldValue = getField(messageId, c);
513                if (oldValue != newValue) {
514                    onSetMessageRead(messageId, !newValue);
515                    return true;
516                }
517                return false;
518            }
519        });
520    }
521
522    /**
523     * Toggles a set of favorites (stars)
524     *
525     * @param selectedSet The current list of selected items
526     */
527    private void onMultiToggleFavorite(Set<Long> selectedSet) {
528        toggleMultiple(selectedSet, new MultiToggleHelper() {
529
530            public boolean getField(long messageId, Cursor c) {
531                return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0;
532            }
533
534            public boolean setField(long messageId, Cursor c, boolean newValue) {
535                boolean oldValue = getField(messageId, c);
536                if (oldValue != newValue) {
537                    onSetMessageFavorite(messageId, newValue);
538                    return true;
539                }
540                return false;
541            }
542        });
543    }
544
545    private void onMultiDelete(Set<Long> selectedSet) {
546        // Clone the set, because deleting is going to thrash things
547        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
548        for (Long id : cloneSet) {
549            mController.deleteMessage(id, -1);
550        }
551        Toast.makeText(mActivity, mActivity.getResources().getQuantityString(
552                R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show();
553        selectedSet.clear();
554        mCallback.onSelectionChanged();
555    }
556
557    private interface MultiToggleHelper {
558        /**
559         * Return true if the field of interest is "set".  If one or more are false, then our
560         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
561         * @param messageId the message id of the current message
562         * @param c the cursor, positioned to the item of interest
563         * @return true if the field at this row is "set"
564         */
565        public boolean getField(long messageId, Cursor c);
566
567        /**
568         * Set or clear the field of interest.  Return true if a change was made.
569         * @param messageId the message id of the current message
570         * @param c the cursor, positioned to the item of interest
571         * @param newValue the new value to be set at this row
572         * @return true if a change was actually made
573         */
574        public boolean setField(long messageId, Cursor c, boolean newValue);
575    }
576
577    /**
578     * Toggle multiple fields in a message, using the following logic:  If one or more fields
579     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
580     *
581     * @param selectedSet the set of messages that are selected
582     * @param helper functions to implement the specific getter & setter
583     * @return the number of messages that were updated
584     */
585    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
586        Cursor c = mListAdapter.getCursor();
587        boolean anyWereFound = false;
588        boolean allWereSet = true;
589
590        c.moveToPosition(-1);
591        while (c.moveToNext()) {
592            long id = c.getInt(MessagesAdapter.COLUMN_ID);
593            if (selectedSet.contains(Long.valueOf(id))) {
594                anyWereFound = true;
595                if (!helper.getField(id, c)) {
596                    allWereSet = false;
597                    break;
598                }
599            }
600        }
601
602        int numChanged = 0;
603
604        if (anyWereFound) {
605            boolean newValue = !allWereSet;
606            c.moveToPosition(-1);
607            while (c.moveToNext()) {
608                long id = c.getInt(MessagesAdapter.COLUMN_ID);
609                if (selectedSet.contains(Long.valueOf(id))) {
610                    if (helper.setField(id, c, newValue)) {
611                        ++numChanged;
612                    }
613                }
614            }
615        }
616
617        return numChanged;
618    }
619
620    /**
621     * Test selected messages for showing appropriate labels
622     * @param selectedSet
623     * @param column_id
624     * @param defaultflag
625     * @return true when the specified flagged message is selected
626     */
627    private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) {
628        Cursor c = mListAdapter.getCursor();
629        if (c == null || c.isClosed()) {
630            return false;
631        }
632        c.moveToPosition(-1);
633        while (c.moveToNext()) {
634            long id = c.getInt(MessagesAdapter.COLUMN_ID);
635            if (selectedSet.contains(Long.valueOf(id))) {
636                if (c.getInt(column_id) == (defaultflag? 1 : 0)) {
637                    return true;
638                }
639            }
640        }
641        return false;
642    }
643
644    /**
645     * @return true if one or more non-starred messages are selected.
646     */
647    public boolean doesSelectionContainNonStarredMessage() {
648        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE,
649                false);
650    }
651
652    /**
653     * @return true if one or more read messages are selected.
654     */
655    public boolean doesSelectionContainReadMessage() {
656        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true);
657    }
658
659    /**
660     * Implements a timed refresh of "stale" mailboxes.  This should only happen when
661     * multiple conditions are true, including:
662     *   Only when the user explicitly opens the mailbox (not onResume, for example)
663     *   Only for real, non-push mailboxes
664     *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
665     */
666    private void autoRefreshStaleMailbox() {
667        if (!mCanAutoRefresh
668                || (mListAdapter.getCursor() == null) // Check if messages info is loaded
669                || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode
670                || isMagicMailbox()) { // Check if this mailbox is synthetic/combined
671            return;
672        }
673        mCanAutoRefresh = false;
674        if (!Email.mailboxRequiresRefresh(mMailboxId)) {
675            return;
676        }
677        onRefresh();
678    }
679
680    public void updateListPosition() { // TODO give it a better name
681        int listViewHeight = getListView().getHeight();
682        if (mListAdapter.getSelectedSet().size() == 1 && mFirstSelectedItemPosition >= 0
683                && mFirstSelectedItemPosition < getListView().getCount()
684                && listViewHeight < mFirstSelectedItemTop) {
685            getListView().setSelectionFromTop(mFirstSelectedItemPosition,
686                    listViewHeight - mFirstSelectedItemHeight);
687        }
688    }
689
690    /**
691     * Show/hide the progress icon on the list footer.  It's called by the host activity.
692     * TODO: It might be cleaner if the fragment listen to the controller events and show it by
693     *     itself, rather than letting the activity controll this.
694     */
695    public void showProgressIcon(boolean show) {
696        if (mListFooterProgress != null) {
697            mListFooterProgress.setVisibility(show ? View.VISIBLE : View.GONE);
698        }
699        setListFooterText(show);
700    }
701
702    // Adapter callbacks
703    public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) {
704        onSetMessageFavorite(itemView.mMessageId, newFavorite);
705    }
706
707    public void onAdapterRequery() {
708        // This updates the "multi-selection" button labels.
709        mCallback.onSelectionChanged();
710    }
711
712    public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected,
713            int mSelectedCount) {
714        if (mSelectedCount == 1 && newSelected) {
715            mFirstSelectedItemPosition = getListView().getPositionForView(itemView);
716            mFirstSelectedItemTop = itemView.getBottom();
717            mFirstSelectedItemHeight = itemView.getHeight();
718        } else {
719            mFirstSelectedItemPosition = -1;
720        }
721        mCallback.onSelectionChanged();
722    }
723
724    /**
725     * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes).
726     *
727     * Here are some rules (finish this list):
728     *
729     * Any merged, synced box (except send):  refresh
730     * Any push-mode account:  refresh
731     * Any non-push-mode account:  load more
732     * Any outbox (send again):
733     *
734     * @param mailboxId the ID of the mailbox
735     * @param accountId the ID of the account
736     */
737    private void addFooterView(long mailboxId, long accountId) {
738        // first, look for shortcuts that don't need us to spin up a DB access task
739        if (mailboxId == Mailbox.QUERY_ALL_INBOXES
740                || mailboxId == Mailbox.QUERY_ALL_UNREAD
741                || mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
742            finishFooterView(LIST_FOOTER_MODE_REFRESH);
743            return;
744        }
745        if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
746            finishFooterView(LIST_FOOTER_MODE_NONE);
747            return;
748        }
749        if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
750            finishFooterView(LIST_FOOTER_MODE_SEND);
751            return;
752        }
753
754        // We don't know enough to select the footer command type (yet), so we'll
755        // launch an async task to do the remaining lookups and decide what to do
756        mSetFooterTask = new SetFooterTask();
757        mSetFooterTask.execute(mailboxId, accountId);
758    }
759
760    private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION =
761        new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE };
762
763    private class SetFooterTask extends AsyncTask<Long, Void, Integer> {
764        /**
765         * There are two operational modes here, requiring different lookup.
766         * mailboxIs != -1:  A specific mailbox - check its type, then look up its account
767         * accountId != -1:  A specific account - look up the account
768         */
769        @Override
770        protected Integer doInBackground(Long... params) {
771            long mailboxId = params[0];
772            long accountId = params[1];
773            int mailboxType = -1;
774            if (mailboxId != -1) {
775                try {
776                    Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
777                    Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION,
778                            null, null, null);
779                    if (c.moveToFirst()) {
780                        try {
781                            accountId = c.getLong(0);
782                            mailboxType = c.getInt(1);
783                        } finally {
784                            c.close();
785                        }
786                    }
787                } catch (IllegalArgumentException iae) {
788                    // can't do any more here
789                    return LIST_FOOTER_MODE_NONE;
790                }
791            }
792            switch (mailboxType) {
793                case Mailbox.TYPE_OUTBOX:
794                    return LIST_FOOTER_MODE_SEND;
795                case Mailbox.TYPE_DRAFTS:
796                    return LIST_FOOTER_MODE_NONE;
797            }
798            if (accountId != -1) {
799                // This is inefficient but the best fix is not here but in isMessagingController
800                Account account = Account.restoreAccountWithId(mActivity, accountId);
801                if (account != null) {
802                    // TODO move this to more appropriate place
803                    // (don't change member fields on a worker thread.)
804                    mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH;
805                    if (mController.isMessagingController(account)) {
806                        return LIST_FOOTER_MODE_MORE;       // IMAP or POP
807                    } else {
808                        return LIST_FOOTER_MODE_NONE;    // EAS
809                    }
810                }
811            }
812            return LIST_FOOTER_MODE_NONE;
813        }
814
815        @Override
816        protected void onPostExecute(Integer listFooterMode) {
817            if (isCancelled()) {
818                return;
819            }
820            if (listFooterMode == null) {
821                return;
822            }
823            finishFooterView(listFooterMode);
824        }
825    }
826
827    /**
828     * Add the fixed footer view as specified, and set up the test as well.
829     *
830     * @param listFooterMode the footer mode we've determined should be used for this list
831     */
832    private void finishFooterView(int listFooterMode) {
833        mListFooterMode = listFooterMode;
834        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
835            getListView().addFooterView(mListFooterView);
836            getListView().setAdapter(mListAdapter);
837
838            mListFooterProgress = mListFooterView.findViewById(R.id.progress);
839            mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
840            setListFooterText(false);
841        }
842    }
843
844    /**
845     * Set the list footer text based on mode and "active" status
846     */
847    private void setListFooterText(boolean active) {
848        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
849            int footerTextId = 0;
850            switch (mListFooterMode) {
851                case LIST_FOOTER_MODE_REFRESH:
852                    footerTextId = active ? R.string.status_loading_more
853                                          : R.string.refresh_action;
854                    break;
855                case LIST_FOOTER_MODE_MORE:
856                    footerTextId = active ? R.string.status_loading_more
857                                          : R.string.message_list_load_more_messages_action;
858                    break;
859                case LIST_FOOTER_MODE_SEND:
860                    footerTextId = active ? R.string.status_sending_messages
861                                          : R.string.message_list_send_pending_messages_action;
862                    break;
863            }
864            mListFooterText.setText(footerTextId);
865        }
866    }
867
868    /**
869     * Handle a click in the list footer, which changes meaning depending on what we're looking at.
870     */
871    private void doFooterClick() {
872        switch (mListFooterMode) {
873            case LIST_FOOTER_MODE_NONE:         // should never happen
874                break;
875            case LIST_FOOTER_MODE_REFRESH:
876                onRefresh();
877                break;
878            case LIST_FOOTER_MODE_MORE:
879                onLoadMoreMessages();
880                break;
881            case LIST_FOOTER_MODE_SEND:
882                onSendPendingMessages();
883                break;
884        }
885    }
886
887    /**
888     * Async task for loading a single folder out of the UI thread
889     *
890     * The code here (for merged boxes) is a placeholder/hack and should be replaced.  Some
891     * specific notes:
892     * TODO:  Move the double query into a specialized URI that returns all inbox messages
893     * and do the dirty work in raw SQL in the provider.
894     * TODO:  Generalize the query generation so we can reuse it in MessageView (for next/prev)
895     */
896    private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
897
898        private final long mMailboxKey;
899        private long mAccountKey;
900
901        /**
902         * Special constructor to cache some local info
903         */
904        public LoadMessagesTask(long mailboxKey, long accountKey) {
905            mMailboxKey = mailboxKey;
906            mAccountKey = accountKey;
907        }
908
909        @Override
910        protected Cursor doInBackground(Void... params) {
911            // First, determine account id, if unknown
912            if (mAccountKey == -1) { // TODO Use constant instead of -1
913                if (isMagicMailbox()) {
914                    // Magic mailbox.  No accountid.
915                } else {
916                    EmailContent.Mailbox mailbox =
917                            EmailContent.Mailbox.restoreMailboxWithId(mActivity, mMailboxKey);
918                    if (mailbox != null) {
919                        mAccountKey = mailbox.mAccountKey;
920                    } else {
921                        // Mailbox not found.
922                        // TODO We used to close the activity in this case, but what to do now??
923                        return null;
924                    }
925                }
926            }
927
928            // Load messages
929            String selection =
930                Utility.buildMailboxIdSelection(mResolver, mMailboxKey);
931            Cursor c = mActivity.managedQuery(EmailContent.Message.CONTENT_URI, MESSAGE_PROJECTION,
932                    selection, null, EmailContent.MessageColumns.TIMESTAMP + " DESC");
933            return c;
934        }
935
936        @Override
937        protected void onPostExecute(Cursor cursor) {
938            if (isCancelled()) {
939                return;
940            }
941            if (cursor == null || cursor.isClosed()) {
942                mCallback.onMailboxNotFound();
943                return;
944            }
945            MessageListFragment.this.mAccountId = mAccountKey;
946
947            addFooterView(mMailboxKey, mAccountKey);
948
949            // TODO changeCursor(null)??
950            mListAdapter.changeCursor(cursor);
951
952            // changeCursor occurs the jumping of position in ListView, so it's need to restore
953            // the position;
954            restoreListPosition();
955            autoRefreshStaleMailbox();
956            // Reset the "new messages" count in the service, since we're seeing them now
957            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
958                MailService.resetNewMessageCount(mActivity, -1);
959            } else if (mMailboxKey >= 0 && mAccountKey != -1) {
960                MailService.resetNewMessageCount(mActivity, mAccountKey);
961            }
962        }
963    }
964}
965