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