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