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