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