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