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