MessageListFragment.java revision b9b6ce1d36c33e5663c33db191d5dded27df0d5f
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 android.app.Activity;
20import android.app.ListFragment;
21import android.app.LoaderManager;
22import android.content.ClipData;
23import android.content.ContentUris;
24import android.content.Loader;
25import android.content.res.Configuration;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.graphics.Canvas;
29import android.graphics.Point;
30import android.graphics.PointF;
31import android.graphics.Rect;
32import android.graphics.Typeface;
33import android.graphics.drawable.Drawable;
34import android.os.Bundle;
35import android.os.Parcelable;
36import android.text.TextPaint;
37import android.util.Log;
38import android.view.ActionMode;
39import android.view.DragEvent;
40import android.view.LayoutInflater;
41import android.view.Menu;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.MotionEvent;
45import android.view.View;
46import android.view.View.DragShadowBuilder;
47import android.view.View.OnDragListener;
48import android.view.View.OnTouchListener;
49import android.view.ViewGroup;
50import android.widget.AdapterView;
51import android.widget.AdapterView.OnItemClickListener;
52import android.widget.AdapterView.OnItemLongClickListener;
53import android.widget.ListView;
54import android.widget.TextView;
55import android.widget.Toast;
56
57import com.android.email.Controller;
58import com.android.email.Email;
59import com.android.email.MessageListContext;
60import com.android.email.NotificationController;
61import com.android.email.R;
62import com.android.email.RefreshManager;
63import com.android.email.activity.MessagesAdapter.SearchResultsCursor;
64import com.android.email.provider.EmailProvider;
65import com.android.emailcommon.Logging;
66import com.android.emailcommon.provider.Account;
67import com.android.emailcommon.provider.EmailContent.Message;
68import com.android.emailcommon.provider.Mailbox;
69import com.android.emailcommon.utility.EmailAsyncTask;
70import com.android.emailcommon.utility.Utility;
71import com.google.common.annotations.VisibleForTesting;
72import com.google.common.collect.Maps;
73
74import java.util.HashMap;
75import java.util.Set;
76
77/**
78 * Message list.
79 *
80 * See the class javadoc for {@link MailboxListFragment} for notes on {@link #getListView()} and
81 * {@link #isViewCreated()}.
82 */
83public class MessageListFragment extends ListFragment
84        implements OnItemClickListener, OnItemLongClickListener, MessagesAdapter.Callback,
85        MoveMessageToDialog.Callback, OnDragListener, OnTouchListener {
86    private static final String BUNDLE_LIST_STATE = "MessageListFragment.state.listState";
87    private static final String BUNDLE_KEY_SELECTED_MESSAGE_ID
88            = "messageListFragment.state.listState.selected_message_id";
89
90    private static final int LOADER_ID_MESSAGES_LOADER = 1;
91
92    /** Argument name(s) */
93    private static final String ARG_LIST_CONTEXT = "listContext";
94
95    // Controller access
96    private Controller mController;
97    private RefreshManager mRefreshManager;
98    private final RefreshListener mRefreshListener = new RefreshListener();
99
100    // UI Support
101    private Activity mActivity;
102    private Callback mCallback = EmptyCallback.INSTANCE;
103    private boolean mIsViewCreated;
104
105    private View mListPanel;
106    private View mListFooterView;
107    private TextView mListFooterText;
108    private View mListFooterProgress;
109    private ViewGroup mSearchHeader;
110    private ViewGroup mWarningContainer;
111    private TextView mSearchHeaderText;
112    private TextView mSearchHeaderCount;
113
114    private static final int LIST_FOOTER_MODE_NONE = 0;
115    private static final int LIST_FOOTER_MODE_MORE = 1;
116    private int mListFooterMode;
117
118    private MessagesAdapter mListAdapter;
119    private boolean mIsFirstLoad;
120
121    /** ID of the message to hightlight. */
122    private long mSelectedMessageId = -1;
123
124    private Account mAccount;
125    private Mailbox mMailbox;
126    /** The original mailbox being searched, if this list is showing search results. */
127    private Mailbox mSearchedMailbox;
128    private boolean mIsEasAccount;
129    private boolean mIsRefreshable;
130    private int mCountTotalAccounts;
131
132    // Misc members
133
134    private boolean mShowSendCommand;
135    private boolean mShowMoveCommand;
136
137    /**
138     * If true, we disable the CAB even if there are selected messages.
139     * It's used in portrait on the tablet when the message view becomes visible and the message
140     * list gets pushed out of the screen, in which case we want to keep the selection but the CAB
141     * should be gone.
142     */
143    private boolean mDisableCab;
144
145    /** true between {@link #onResume} and {@link #onPause}. */
146    private boolean mResumed;
147
148    /**
149     * {@link ActionMode} shown when 1 or more message is selected.
150     */
151    private ActionMode mSelectionMode;
152    private SelectionModeCallback mLastSelectionModeCallback;
153
154    private Parcelable mSavedListState;
155
156    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
157
158    /**
159     * Callback interface that owning activities must implement
160     */
161    public interface Callback {
162        public static final int TYPE_REGULAR = 0;
163        public static final int TYPE_DRAFT = 1;
164        public static final int TYPE_TRASH = 2;
165
166        /**
167         * Called when the specified mailbox does not exist.
168         */
169        public void onMailboxNotFound();
170
171        /**
172         * Called when the user wants to open a message.
173         * Note {@code mailboxId} is of the actual mailbox of the message, which is different from
174         * {@link MessageListFragment#getMailboxId} if it's magic mailboxes.
175         *
176         * @param messageId the message ID of the message
177         * @param messageMailboxId the mailbox ID of the message.
178         *     This will never take values like {@link Mailbox#QUERY_ALL_INBOXES}.
179         * @param listMailboxId the mailbox ID of the listbox shown on this fragment.
180         *     This can be that of a magic mailbox, e.g.  {@link Mailbox#QUERY_ALL_INBOXES}.
181         * @param type {@link #TYPE_REGULAR}, {@link #TYPE_DRAFT} or {@link #TYPE_TRASH}.
182         */
183        public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId,
184                int type);
185
186        /**
187         * Called when an operation is initiated that can potentially advance the current
188         * message selection (e.g. a delete operation may advance the selection).
189         * @param affectedMessages the messages the operation will apply to
190         */
191        public void onAdvancingOpAccepted(Set<Long> affectedMessages);
192
193        /**
194         * Called when a drag & drop is initiated.
195         *
196         * @return true if drag & drop is allowed
197         */
198        public boolean onDragStarted();
199
200        /**
201         * Called when a drag & drop is ended.
202         */
203        public void onDragEnded();
204    }
205
206    private static final class EmptyCallback implements Callback {
207        public static final Callback INSTANCE = new EmptyCallback();
208
209        @Override
210        public void onMailboxNotFound() {
211        }
212
213        @Override
214        public void onMessageOpen(
215                long messageId, long messageMailboxId, long listMailboxId, int type) {
216        }
217
218        @Override
219        public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
220        }
221
222        @Override
223        public boolean onDragStarted() {
224            return false; // We don't know -- err on the safe side.
225        }
226
227        @Override
228        public void onDragEnded() {
229        }
230    }
231
232    /**
233     * Create a new instance with initialization parameters.
234     *
235     * This fragment should be created only with this method.  (Arguments should always be set.)
236     *
237     * @param listContext The list context to show messages for
238     */
239    public static MessageListFragment newInstance(MessageListContext listContext) {
240        final MessageListFragment instance = new MessageListFragment();
241        final Bundle args = new Bundle();
242        args.putParcelable(ARG_LIST_CONTEXT, listContext);
243        instance.setArguments(args);
244        return instance;
245    }
246
247    /**
248     * The context describing the contents to be shown in the list.
249     * Do not use directly; instead, use the getters such as {@link #getAccountId()}.
250     * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
251     * constructs, this <em>must</em> be considered immutable.
252     */
253    private MessageListContext mListContext;
254
255    private void initializeArgCache() {
256        if (mListContext != null) return;
257        mListContext = getArguments().getParcelable(ARG_LIST_CONTEXT);
258    }
259
260    /**
261     * @return the account ID passed to {@link #newInstance}.  Safe to call even before onCreate.
262     *
263     * NOTE it may return {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
264     */
265    public long getAccountId() {
266        initializeArgCache();
267        return mListContext.mAccountId;
268    }
269
270    /**
271     * @return the mailbox ID passed to {@link #newInstance}.  Safe to call even before onCreate.
272     */
273    public long getMailboxId() {
274        initializeArgCache();
275        return mListContext.getMailboxId();
276    }
277
278    /**
279     * @return true if the mailbox is a combined mailbox.  Safe to call even before onCreate.
280     */
281    public boolean isCombinedMailbox() {
282        return getMailboxId() < 0;
283    }
284
285    public MessageListContext getListContext() {
286        initializeArgCache();
287        return mListContext;
288    }
289
290    /**
291     * @return Whether or not initial data is loaded in this list.
292     */
293    public boolean hasDataLoaded() {
294        return mCountTotalAccounts > 0;
295    }
296
297    /**
298     * @return The account object, when known. Null if not yet known.
299     */
300    public Account getAccount() {
301        return mAccount;
302    }
303
304    /**
305     * @return The mailbox where the messages belong in, when known. Null if not yet known.
306     */
307    public Mailbox getMailbox() {
308        return mMailbox;
309    }
310
311    /**
312     * @return Whether or not this message list is showing a user's inbox.
313     *     Note that combined inbox view is treated as an inbox view.
314     */
315    public boolean isInboxList() {
316        MessageListContext listContext = getListContext();
317        long accountId = listContext.mAccountId;
318        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
319            return listContext.getMailboxId() == Mailbox.QUERY_ALL_INBOXES;
320        }
321
322        if (!hasDataLoaded()) {
323            // If the data hasn't finished loading, we don't have the full mailbox - infer from ID.
324            long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
325            return listContext.getMailboxId() == inboxId;
326        }
327        return (mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_INBOX);
328    }
329
330    /**
331     * @return The mailbox being searched, when known. Null if not yet known or if not a search
332     *    result.
333     */
334    public Mailbox getSearchedMailbox() {
335        return mSearchedMailbox;
336    }
337
338    @Override
339    public void onAttach(Activity activity) {
340        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
341            Log.d(Logging.LOG_TAG, this + " onAttach");
342        }
343        super.onAttach(activity);
344    }
345
346    @Override
347    public void onCreate(Bundle savedInstanceState) {
348        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
349            Log.d(Logging.LOG_TAG, this + " onCreate");
350        }
351        super.onCreate(savedInstanceState);
352
353        mActivity = getActivity();
354        setHasOptionsMenu(true);
355        mController = Controller.getInstance(mActivity);
356        mRefreshManager = RefreshManager.getInstance(mActivity);
357
358        mListAdapter = new MessagesAdapter(mActivity, this);
359        mIsFirstLoad = true;
360    }
361
362    @Override
363    public View onCreateView(
364            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
365        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
366            Log.d(Logging.LOG_TAG, this + " onCreateView");
367        }
368        // Use a custom layout, which includes the original layout with "send messages" panel.
369        View root = inflater.inflate(R.layout.message_list_fragment,null);
370        mIsViewCreated = true;
371        mListPanel = UiUtilities.getView(root, R.id.list_panel);
372        return root;
373    }
374
375    private void initSearchHeader() {
376        if (mSearchHeader == null) {
377            ViewGroup root = (ViewGroup) getView();
378            mSearchHeader = (ViewGroup) LayoutInflater.from(mActivity).inflate(
379                    R.layout.message_list_search_header, root, false);
380            mSearchHeaderText = UiUtilities.getView(mSearchHeader, R.id.search_header_text);
381            mSearchHeaderCount = UiUtilities.getView(mSearchHeader, R.id.search_count);
382
383            // Add above the actual list.
384            root.addView(mSearchHeader, 0);
385        }
386    }
387
388    /**
389     * @return true if the content view is created and not destroyed yet. (i.e. between
390     * {@link #onCreateView} and {@link #onDestroyView}.
391     */
392    private boolean isViewCreated() {
393        // Note that we don't use "getView() != null".  This method is used in updateSelectionMode()
394        // to determine if CAB shold be shown.  But because it's called from onDestroyView(), at
395        // this point the fragment still has views but we want to hide CAB, we can't use
396        // getView() here.
397        return mIsViewCreated;
398    }
399
400    @Override
401    public void onActivityCreated(Bundle savedInstanceState) {
402        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
403            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
404        }
405        super.onActivityCreated(savedInstanceState);
406
407        final ListView lv = getListView();
408        lv.setOnItemClickListener(this);
409        lv.setOnItemLongClickListener(this);
410        lv.setOnTouchListener(this);
411        lv.setItemsCanFocus(false);
412        lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
413
414        mListFooterView = getActivity().getLayoutInflater().inflate(
415                R.layout.message_list_item_footer, lv, false);
416        setEmptyText(getString(R.string.message_list_no_messages));
417
418        if (savedInstanceState != null) {
419            // Fragment doesn't have this method.  Call it manually.
420            restoreInstanceState(savedInstanceState);
421        }
422
423        startLoading();
424
425        UiUtilities.installFragment(this);
426    }
427
428    @Override
429    public void onStart() {
430        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
431            Log.d(Logging.LOG_TAG, this + " onStart");
432        }
433        super.onStart();
434    }
435
436    @Override
437    public void onResume() {
438        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
439            Log.d(Logging.LOG_TAG, this + " onResume");
440        }
441        super.onResume();
442        adjustMessageNotification(false);
443        mRefreshManager.registerListener(mRefreshListener);
444        mResumed = true;
445    }
446
447    @Override
448    public void onPause() {
449        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
450            Log.d(Logging.LOG_TAG, this + " onPause");
451        }
452        mResumed = false;
453        mSavedListState = getListView().onSaveInstanceState();
454        adjustMessageNotification(true);
455        super.onPause();
456    }
457
458    @Override
459    public void onStop() {
460        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
461            Log.d(Logging.LOG_TAG, this + " onStop");
462        }
463        mTaskTracker.cancellAllInterrupt();
464        mRefreshManager.unregisterListener(mRefreshListener);
465
466        super.onStop();
467    }
468
469    @Override
470    public void onDestroyView() {
471        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
472            Log.d(Logging.LOG_TAG, this + " onDestroyView");
473        }
474        mIsViewCreated = false; // Clear this first for updateSelectionMode(). See isViewCreated().
475        UiUtilities.uninstallFragment(this);
476        updateSelectionMode();
477
478        // Reset the footer mode since we just blew away the footer view we were holding on to.
479        // This will get re-updated when/if this fragment is restored.
480        mListFooterMode = LIST_FOOTER_MODE_NONE;
481        super.onDestroyView();
482    }
483
484    @Override
485    public void onDestroy() {
486        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
487            Log.d(Logging.LOG_TAG, this + " onDestroy");
488        }
489
490        finishSelectionMode();
491        super.onDestroy();
492    }
493
494    @Override
495    public void onDetach() {
496        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
497            Log.d(Logging.LOG_TAG, this + " onDetach");
498        }
499        super.onDetach();
500    }
501
502    @Override
503    public void onSaveInstanceState(Bundle outState) {
504        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
505            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
506        }
507        super.onSaveInstanceState(outState);
508        mListAdapter.onSaveInstanceState(outState);
509        if (isViewCreated()) {
510            outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState());
511        }
512        outState.putLong(BUNDLE_KEY_SELECTED_MESSAGE_ID, mSelectedMessageId);
513    }
514
515    @VisibleForTesting
516    void restoreInstanceState(Bundle savedInstanceState) {
517        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
518            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
519        }
520        mListAdapter.loadState(savedInstanceState);
521        mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
522        mSelectedMessageId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MESSAGE_ID);
523    }
524
525    @Override
526    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
527        inflater.inflate(R.menu.message_list_fragment_option, menu);
528    }
529
530    @Override
531    public void onPrepareOptionsMenu(Menu menu) {
532        menu.findItem(R.id.send).setVisible(mShowSendCommand);
533    }
534
535    @Override
536    public boolean onOptionsItemSelected(MenuItem item) {
537        switch (item.getItemId()) {
538            case R.id.send:
539                onSendPendingMessages();
540                return true;
541
542        }
543        return false;
544    }
545
546    public void setCallback(Callback callback) {
547        mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE;
548    }
549
550    /**
551     * This method must be called when the fragment is hidden/shown.
552     */
553    public void onHidden(boolean hidden) {
554        // When hidden, we need to disable CAB.
555        if (hidden == mDisableCab) {
556            return;
557        }
558        mDisableCab = hidden;
559        updateSelectionMode();
560    }
561
562    public void setSelectedMessage(long messageId) {
563        if (mSelectedMessageId == messageId) {
564            return;
565        }
566        mSelectedMessageId = messageId;
567        if (mResumed) {
568            highlightSelectedMessage(true);
569        }
570    }
571
572    /**
573     * @return true if the mailbox is refreshable.  false otherwise, or unknown yet.
574     */
575    public boolean isRefreshable() {
576        return mIsRefreshable;
577    }
578
579    /**
580     * @return the number of messages that are currently selected.
581     */
582    private int getSelectedCount() {
583        return mListAdapter.getSelectedSet().size();
584    }
585
586    /**
587     * @return true if the list is in the "selection" mode.
588     */
589    public boolean isInSelectionMode() {
590        return mSelectionMode != null;
591    }
592
593    /**
594     * Called when a message is clicked.
595     */
596    @Override
597    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
598        if (view != mListFooterView) {
599            MessageListItem itemView = (MessageListItem) view;
600            onMessageOpen(itemView.mMailboxId, id);
601        } else {
602            doFooterClick();
603        }
604    }
605
606    // This is tentative drag & drop UI
607    private static class ShadowBuilder extends DragShadowBuilder {
608        private static Drawable sBackground;
609        /** Paint information for the move message text */
610        private static TextPaint sMessagePaint;
611        /** Paint information for the message count */
612        private static TextPaint sCountPaint;
613        /** The x location of any touch event; used to ensure the drag overlay is drawn correctly */
614        private static int sTouchX;
615
616        /** Width of the draggable view */
617        private final int mDragWidth;
618        /** Height of the draggable view */
619        private final int mDragHeight;
620
621        private final String mMessageText;
622        private final PointF mMessagePoint;
623
624        private final String mCountText;
625        private final PointF mCountPoint;
626        private int mOldOrientation = Configuration.ORIENTATION_UNDEFINED;
627
628        /** Margin applied to the right of count text */
629        private static float sCountMargin;
630        /** Margin applied to left of the message text */
631        private static float sMessageMargin;
632        /** Vertical offset of the drag view */
633        private static int sDragOffset;
634
635        public ShadowBuilder(View view, int count) {
636            super(view);
637            Resources res = view.getResources();
638            int newOrientation = res.getConfiguration().orientation;
639
640            mDragHeight = view.getHeight();
641            mDragWidth = view.getWidth();
642
643            // TODO: Can we define a layout for the contents of the drag area?
644            if (sBackground == null || mOldOrientation != newOrientation) {
645                mOldOrientation = newOrientation;
646
647                sBackground = res.getDrawable(R.drawable.list_pressed_holo);
648                sBackground.setBounds(0, 0, mDragWidth, mDragHeight);
649
650                sDragOffset = (int)res.getDimension(R.dimen.message_list_drag_offset);
651
652                sMessagePaint = new TextPaint();
653                float messageTextSize;
654                messageTextSize = res.getDimension(R.dimen.message_list_drag_message_font_size);
655                sMessagePaint.setTextSize(messageTextSize);
656                sMessagePaint.setTypeface(Typeface.DEFAULT_BOLD);
657                sMessagePaint.setAntiAlias(true);
658                sMessageMargin = res.getDimension(R.dimen.message_list_drag_message_right_margin);
659
660                sCountPaint = new TextPaint();
661                float countTextSize;
662                countTextSize = res.getDimension(R.dimen.message_list_drag_count_font_size);
663                sCountPaint.setTextSize(countTextSize);
664                sCountPaint.setTypeface(Typeface.DEFAULT_BOLD);
665                sCountPaint.setAntiAlias(true);
666                sCountMargin = res.getDimension(R.dimen.message_list_drag_count_left_margin);
667            }
668
669            // Calculate layout positions
670            Rect b = new Rect();
671
672            mMessageText = res.getQuantityString(R.plurals.move_messages, count, count);
673            sMessagePaint.getTextBounds(mMessageText, 0, mMessageText.length(), b);
674            mMessagePoint = new PointF(mDragWidth - b.right - sMessageMargin,
675                    (mDragHeight - b.top)/ 2);
676
677            mCountText = Integer.toString(count);
678            sCountPaint.getTextBounds(mCountText, 0, mCountText.length(), b);
679            mCountPoint = new PointF(sCountMargin,
680                    (mDragHeight - b.top) / 2);
681        }
682
683        @Override
684        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
685            shadowSize.set(mDragWidth, mDragHeight);
686            shadowTouchPoint.set(sTouchX, (mDragHeight / 2) + sDragOffset);
687        }
688
689        @Override
690        public void onDrawShadow(Canvas canvas) {
691            super.onDrawShadow(canvas);
692            sBackground.draw(canvas);
693            canvas.drawText(mMessageText, mMessagePoint.x, mMessagePoint.y, sMessagePaint);
694            canvas.drawText(mCountText, mCountPoint.x, mCountPoint.y, sCountPaint);
695        }
696    }
697
698    @Override
699    public boolean onDrag(View view, DragEvent event) {
700        switch(event.getAction()) {
701            case DragEvent.ACTION_DRAG_ENDED:
702                if (event.getResult()) {
703                    onDeselectAll(); // Clear the selection
704                }
705                mCallback.onDragEnded();
706                break;
707        }
708        return false;
709    }
710
711    @Override
712    public boolean onTouch(View v, MotionEvent event) {
713        if (event.getAction() == MotionEvent.ACTION_DOWN) {
714            // Save the touch location to draw the drag overlay at the correct location
715            ShadowBuilder.sTouchX = (int)event.getX();
716        }
717        // don't do anything, let the system process the event
718        return false;
719    }
720
721    @Override
722    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
723        if (view != mListFooterView) {
724            // Always toggle the item.
725            MessageListItem listItem = (MessageListItem) view;
726            boolean toggled = false;
727            if (!mListAdapter.isSelected(listItem)) {
728                toggleSelection(listItem);
729                toggled = true;
730            }
731
732            // Additionally, check to see if we can drag the item.
733            if (!mCallback.onDragStarted()) {
734                return toggled; // D&D not allowed.
735            }
736            // We can't move from combined accounts view
737            // We also need to check the actual mailbox to see if we can move items from it
738            final long mailboxId = getMailboxId();
739            if (mAccount == null || mMailbox == null) {
740                return false;
741            } else if (mailboxId > 0 && !mMailbox.canHaveMessagesMoved()) {
742                return false;
743            }
744            // Start drag&drop.
745
746            // Create ClipData with the Uri of the message we're long clicking
747            ClipData data = ClipData.newUri(mActivity.getContentResolver(),
748                    MessageListItem.MESSAGE_LIST_ITEMS_CLIP_LABEL, Message.CONTENT_URI.buildUpon()
749                    .appendPath(Long.toString(listItem.mMessageId))
750                    .appendQueryParameter(
751                            EmailProvider.MESSAGE_URI_PARAMETER_MAILBOX_ID,
752                            Long.toString(mailboxId))
753                            .build());
754            Set<Long> selectedMessageIds = mListAdapter.getSelectedSet();
755            int size = selectedMessageIds.size();
756            // Add additional Uri's for any other selected messages
757            for (Long messageId: selectedMessageIds) {
758                if (messageId.longValue() != listItem.mMessageId) {
759                    data.addItem(new ClipData.Item(
760                            ContentUris.withAppendedId(Message.CONTENT_URI, messageId)));
761                }
762            }
763            // Start dragging now
764            listItem.setOnDragListener(this);
765            listItem.startDrag(data, new ShadowBuilder(listItem, size), null, 0);
766            return true;
767        }
768        return false;
769    }
770
771    private void toggleSelection(MessageListItem itemView) {
772        itemView.invalidate();
773        mListAdapter.toggleSelected(itemView);
774    }
775
776    /**
777     * Called when a message on the list is selected
778     *
779     * @param messageMailboxId the actual mailbox ID of the message.  Note it's different than
780     *        what is returned by {@link #getMailboxId()} for combined mailboxes.
781     *        ({@link #getMailboxId()} may return special mailbox values such as
782     *        {@link Mailbox#QUERY_ALL_INBOXES})
783     * @param messageId ID of the message to open.
784     */
785    private void onMessageOpen(final long messageMailboxId, final long messageId) {
786        new MessageOpenTask(messageMailboxId, messageId).cancelPreviousAndExecuteParallel();
787    }
788
789    /**
790     * Task to look up the mailbox type for a message, and kicks the callback.
791     */
792    private class MessageOpenTask extends EmailAsyncTask<Void, Void, Integer> {
793        private final long mMessageMailboxId;
794        private final long mMessageId;
795
796        public MessageOpenTask(long messageMailboxId, long messageId) {
797            super(mTaskTracker);
798            mMessageMailboxId = messageMailboxId;
799            mMessageId = messageId;
800        }
801
802        @Override
803        protected Integer doInBackground(Void... params) {
804            // Restore the mailbox type.  Note we can't use mMailbox.mType here, because
805            // we don't have mMailbox for combined mailbox.
806            // ("All Starred" can contain any kind of messages.)
807            switch (Mailbox.getMailboxType(mActivity, mMessageMailboxId)) {
808                case Mailbox.TYPE_DRAFTS:
809                    return Callback.TYPE_DRAFT;
810                case Mailbox.TYPE_TRASH:
811                    return Callback.TYPE_TRASH;
812                default:
813                    return Callback.TYPE_REGULAR;
814            }
815        }
816
817        @Override
818        protected void onSuccess(Integer type) {
819            if (type == null) {
820                return;
821            }
822            mCallback.onMessageOpen(mMessageId, mMessageMailboxId, getMailboxId(), type);
823        }
824    }
825
826    private void showMoveMessagesDialog(Set<Long> selectedSet) {
827        long[] messageIds = Utility.toPrimitiveLongArray(selectedSet);
828        MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(messageIds, this);
829        dialog.show(getFragmentManager(), "dialog");
830    }
831
832    @Override
833    public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
834        mCallback.onAdvancingOpAccepted(Utility.toLongSet(messageIds));
835        ActivityHelper.moveMessages(getActivity(), newMailboxId, messageIds);
836
837        // Move is async, so we can't refresh now.  Instead, just clear the selection.
838        onDeselectAll();
839    }
840
841    /**
842     * Refresh the list.  NOOP for special mailboxes (e.g. combined inbox).
843     *
844     * Note: Manual refresh is enabled even for push accounts.
845     */
846    public void onRefresh(boolean userRequest) {
847        if (mIsRefreshable) {
848            mRefreshManager.refreshMessageList(getAccountId(), getMailboxId(), userRequest);
849        }
850    }
851
852    private void onDeselectAll() {
853        mListAdapter.clearSelection();
854        if (isInSelectionMode()) {
855            finishSelectionMode();
856        }
857    }
858
859    /**
860     * Load more messages.  NOOP for special mailboxes (e.g. combined inbox).
861     */
862    private void onLoadMoreMessages() {
863        if (mIsRefreshable) {
864            mRefreshManager.loadMoreMessages(getAccountId(), getMailboxId());
865        }
866    }
867
868    public void onSendPendingMessages() {
869        RefreshManager rm = RefreshManager.getInstance(mActivity);
870        if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) {
871            rm.sendPendingMessagesForAllAccounts();
872        } else if (mMailbox != null) { // Magic boxes don't have a specific account id.
873            rm.sendPendingMessages(mMailbox.mAccountKey);
874        }
875    }
876
877    /**
878     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
879     * sense of the helper methods is "true=unread"; this may be called from the UI thread
880     *
881     * @param selectedSet The current list of selected items
882     */
883    private void toggleRead(Set<Long> selectedSet) {
884        toggleMultiple(selectedSet, new MultiToggleHelper() {
885
886            @Override
887            public boolean getField(Cursor c) {
888                return c.getInt(MessagesAdapter.COLUMN_READ) == 0;
889            }
890
891            @Override
892            public void setField(long messageId, boolean newValue) {
893                mController.setMessageReadSync(messageId, !newValue);
894            }
895        });
896    }
897
898    /**
899     * Toggles a set of favorites (stars); this may be called from the UI thread
900     *
901     * @param selectedSet The current list of selected items
902     */
903    private void toggleFavorite(Set<Long> selectedSet) {
904        toggleMultiple(selectedSet, new MultiToggleHelper() {
905
906            @Override
907            public boolean getField(Cursor c) {
908                return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0;
909            }
910
911            @Override
912            public void setField(long messageId, boolean newValue) {
913                mController.setMessageFavoriteSync(messageId, newValue);
914             }
915        });
916    }
917
918    private void deleteMessages(Set<Long> selectedSet) {
919        final long[] messageIds = Utility.toPrimitiveLongArray(selectedSet);
920        mController.deleteMessages(messageIds);
921        Toast.makeText(mActivity, mActivity.getResources().getQuantityString(
922                R.plurals.message_deleted_toast, messageIds.length), Toast.LENGTH_SHORT).show();
923        selectedSet.clear();
924        // Message deletion is async... Can't refresh the list immediately.
925    }
926
927    private interface MultiToggleHelper {
928        /**
929         * Return true if the field of interest is "set".  If one or more are false, then our
930         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
931         * @param c the cursor, positioned to the item of interest
932         * @return true if the field at this row is "set"
933         */
934        public boolean getField(Cursor c);
935
936        /**
937         * Set or clear the field of interest; setField is called asynchronously via EmailAsyncTask
938         * @param messageId the message id of the current message
939         * @param newValue the new value to be set at this row
940         */
941        public void setField(long messageId, boolean newValue);
942    }
943
944    /**
945     * Toggle multiple fields in a message, using the following logic:  If one or more fields
946     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.  Provider
947     * calls are applied asynchronously in setField
948     *
949     * @param selectedSet the set of messages that are selected
950     * @param helper functions to implement the specific getter & setter
951     */
952    private void toggleMultiple(final Set<Long> selectedSet, final MultiToggleHelper helper) {
953        final Cursor c = mListAdapter.getCursor();
954        if (c == null || c.isClosed()) {
955            return;
956        }
957
958        final HashMap<Long, Boolean> setValues = Maps.newHashMap();
959        boolean allWereSet = true;
960
961        c.moveToPosition(-1);
962        while (c.moveToNext()) {
963            long id = c.getInt(MessagesAdapter.COLUMN_ID);
964            if (selectedSet.contains(id)) {
965                boolean value = helper.getField(c);
966                setValues.put(id, value);
967                allWereSet = allWereSet && value;
968            }
969        }
970
971        if (!setValues.isEmpty()) {
972            final boolean newValue = !allWereSet;
973            c.moveToPosition(-1);
974            // TODO: we should probably put up a dialog or some other progress indicator for this.
975            EmailAsyncTask.runAsyncParallel(new Runnable() {
976               @Override
977                public void run() {
978                   for (long id : setValues.keySet()) {
979                       if (setValues.get(id) != newValue) {
980                           helper.setField(id, newValue);
981                       }
982                   }
983                }});
984        }
985    }
986
987    /**
988     * Test selected messages for showing appropriate labels
989     * @param selectedSet
990     * @param columnId
991     * @param defaultflag
992     * @return true when the specified flagged message is selected
993     */
994    private boolean testMultiple(Set<Long> selectedSet, int columnId, boolean defaultflag) {
995        final Cursor c = mListAdapter.getCursor();
996        if (c == null || c.isClosed()) {
997            return false;
998        }
999        c.moveToPosition(-1);
1000        while (c.moveToNext()) {
1001            long id = c.getInt(MessagesAdapter.COLUMN_ID);
1002            if (selectedSet.contains(Long.valueOf(id))) {
1003                if (c.getInt(columnId) == (defaultflag ? 1 : 0)) {
1004                    return true;
1005                }
1006            }
1007        }
1008        return false;
1009    }
1010
1011    /**
1012     * @return true if one or more non-starred messages are selected.
1013     */
1014    public boolean doesSelectionContainNonStarredMessage() {
1015        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE,
1016                false);
1017    }
1018
1019    /**
1020     * @return true if one or more read messages are selected.
1021     */
1022    public boolean doesSelectionContainReadMessage() {
1023        return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true);
1024    }
1025
1026    /**
1027     * Implements a timed refresh of "stale" mailboxes.  This should only happen when
1028     * multiple conditions are true, including:
1029     *   Only refreshable mailboxes.
1030     *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
1031     * Note we do this even if it's a push account; even on Exchange only inbox can be pushed.
1032     */
1033    private void autoRefreshStaleMailbox() {
1034        if (!mIsRefreshable) {
1035            // Not refreshable (special box such as drafts, or magic boxes)
1036            return;
1037        }
1038        if (!mRefreshManager.isMailboxStale(getMailboxId())) {
1039            return;
1040        }
1041        onRefresh(false);
1042    }
1043
1044    /** Implements {@link MessagesAdapter.Callback} */
1045    @Override
1046    public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) {
1047        mController.setMessageFavorite(itemView.mMessageId, newFavorite);
1048    }
1049
1050    /** Implements {@link MessagesAdapter.Callback} */
1051    @Override
1052    public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected,
1053            int mSelectedCount) {
1054        updateSelectionMode();
1055    }
1056
1057    private void updateSearchHeader(Cursor cursor) {
1058        MessageListContext listContext = getListContext();
1059        if (!listContext.isSearch() || cursor == null) {
1060            UiUtilities.setVisibilitySafe(mSearchHeader, View.GONE);
1061            return;
1062        }
1063
1064        SearchResultsCursor searchCursor = (SearchResultsCursor) cursor;
1065        initSearchHeader();
1066        mSearchHeader.setVisibility(View.VISIBLE);
1067        String header = String.format(
1068                mActivity.getString(R.string.search_header_text_fmt),
1069                listContext.getSearchParams().mFilter);
1070        mSearchHeaderText.setText(header);
1071        int resultCount = searchCursor.getResultsCount();
1072        // Don't show a negative value here; this means that the server request failed
1073        // TODO Use some other text for this case (e.g. "search failed")?
1074        if (resultCount < 0) {
1075            resultCount = 0;
1076        }
1077        mSearchHeaderCount.setText(UiUtilities.getMessageCountForUi(
1078                mActivity, resultCount, false /* replaceZeroWithBlank */));
1079    }
1080
1081    private int determineFooterMode() {
1082        int result = LIST_FOOTER_MODE_NONE;
1083        if ((mMailbox == null)
1084                || (mMailbox.mType == Mailbox.TYPE_OUTBOX)
1085                || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) {
1086            return result; // No footer
1087        }
1088        if (mMailbox.mType == Mailbox.TYPE_SEARCH) {
1089            // Determine how many results have been loaded.
1090            Cursor c = mListAdapter.getCursor();
1091            if (c == null || c.isClosed()) {
1092                // Unknown yet - don't do anything.
1093                return result;
1094            }
1095            int total = ((SearchResultsCursor) c).getResultsCount();
1096            int loaded = c.getCount();
1097
1098            if (loaded < total) {
1099                result = LIST_FOOTER_MODE_MORE;
1100            }
1101        } else if (!mIsEasAccount) {
1102            // IMAP, POP has "load more" for regular mailboxes.
1103            result = LIST_FOOTER_MODE_MORE;
1104        }
1105        return result;
1106    }
1107
1108    private void updateFooterView() {
1109        // Only called from onLoadFinished -- always has views.
1110        int mode = determineFooterMode();
1111        if (mListFooterMode == mode) {
1112            return;
1113        }
1114        mListFooterMode = mode;
1115
1116        ListView lv = getListView();
1117        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
1118            lv.addFooterView(mListFooterView);
1119            if (getListAdapter() != null) {
1120                // Already have an adapter - reset it to force the mode. But save the scroll
1121                // position so that we don't get kicked to the top.
1122                Parcelable listState = lv.onSaveInstanceState();
1123                setListAdapter(mListAdapter);
1124                lv.onRestoreInstanceState(listState);
1125            }
1126
1127            mListFooterProgress = mListFooterView.findViewById(R.id.progress);
1128            mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
1129        } else {
1130            lv.removeFooterView(mListFooterView);
1131        }
1132        updateListFooter();
1133    }
1134
1135    /**
1136     * Set the list footer text based on mode and the current "network active" status
1137     */
1138    private void updateListFooter() {
1139        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
1140            int footerTextId = 0;
1141            switch (mListFooterMode) {
1142                case LIST_FOOTER_MODE_MORE:
1143                    boolean active = mRefreshManager.isMessageListRefreshing(getMailboxId());
1144                    footerTextId = active ? R.string.status_loading_messages
1145                            : R.string.message_list_load_more_messages_action;
1146                    mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE);
1147                    break;
1148            }
1149            mListFooterText.setText(footerTextId);
1150        }
1151    }
1152
1153    /**
1154     * Handle a click in the list footer, which changes meaning depending on what we're looking at.
1155     */
1156    private void doFooterClick() {
1157        switch (mListFooterMode) {
1158            case LIST_FOOTER_MODE_NONE: // should never happen
1159                break;
1160            case LIST_FOOTER_MODE_MORE:
1161                onLoadMoreMessages();
1162                break;
1163        }
1164    }
1165
1166    private void showSendCommand(boolean show) {
1167        if (show != mShowSendCommand) {
1168            mShowSendCommand = show;
1169            mActivity.invalidateOptionsMenu();
1170        }
1171    }
1172
1173    private void updateMailboxSpecificActions() {
1174        final boolean isOutbox = (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX)
1175                || ((mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_OUTBOX));
1176        showSendCommand(isOutbox && (mListAdapter != null) && (mListAdapter.getCount() > 0));
1177
1178        // A null account/mailbox means we're in a combined view. We show the move icon there,
1179        // even though it may be the case that we can't move messages from one of the mailboxes.
1180        // There's no good way to tell that right now, though.
1181        mShowMoveCommand = (mAccount == null || mAccount.supportsMoveMessages(getActivity()))
1182                && (mMailbox == null || mMailbox.canHaveMessagesMoved());
1183
1184        // Enable mailbox specific actions on the UIController level if needed.
1185        mActivity.invalidateOptionsMenu();
1186    }
1187
1188    /**
1189     * Adjusts message notification depending upon the state of the fragment and the currently
1190     * viewed mailbox. If the fragment is resumed, notifications for the current mailbox may
1191     * be suspended. Otherwise, notifications may be re-activated. Not all mailbox types are
1192     * supported for notifications. These include (but are not limited to) special mailboxes
1193     * such as {@link Mailbox#QUERY_ALL_DRAFTS}, {@link Mailbox#QUERY_ALL_FAVORITES}, etc...
1194     *
1195     * @param updateLastSeenKey If {@code true}, the last seen message key for the currently
1196     *                          viewed mailbox will be updated.
1197     */
1198    private void adjustMessageNotification(boolean updateLastSeenKey) {
1199        final long accountId = getAccountId();
1200        final long mailboxId = getMailboxId();
1201        if (mailboxId == Mailbox.QUERY_ALL_INBOXES || mailboxId > 0) {
1202            if (updateLastSeenKey) {
1203                Utility.updateLastSeenMessageKey(mActivity, accountId);
1204            }
1205            NotificationController notifier = NotificationController.getInstance(mActivity);
1206            notifier.suspendMessageNotification(mResumed, accountId);
1207        }
1208    }
1209
1210    private void startLoading() {
1211        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
1212            Log.d(Logging.LOG_TAG, this + " startLoading");
1213        }
1214        // Clear the list. (ListFragment will show the "Loading" animation)
1215        showSendCommand(false);
1216        updateSearchHeader(null);
1217
1218        // Start loading...
1219        final LoaderManager lm = getLoaderManager();
1220        lm.initLoader(LOADER_ID_MESSAGES_LOADER, null, new MessagesLoaderCallback());
1221    }
1222
1223    /** Timeout to show a warning, since some IMAP searches could take a long time. */
1224    private final int SEARCH_WARNING_DELAY_MS = 10000;
1225
1226    private void onSearchLoadTimeout() {
1227        // Search is taking too long. Show an error message.
1228        ViewGroup root = (ViewGroup) getView();
1229        Activity host = getActivity();
1230        if (root != null && host != null) {
1231            mListPanel.setVisibility(View.GONE);
1232            mWarningContainer = (ViewGroup) LayoutInflater.from(host).inflate(
1233                    R.layout.message_list_warning, root, false);
1234            TextView title = UiUtilities.getView(mWarningContainer, R.id.message_title);
1235            TextView message = UiUtilities.getView(mWarningContainer, R.id.message_warning);
1236            title.setText(R.string.search_slow_warning_title);
1237            message.setText(R.string.search_slow_warning_message);
1238            root.addView(mWarningContainer);
1239        }
1240    }
1241
1242    /**
1243     * Loader callbacks for message list.
1244     */
1245    private class MessagesLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
1246        @Override
1247        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1248            final MessageListContext listContext = getListContext();
1249            if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
1250                Log.d(Logging.LOG_TAG, MessageListFragment.this
1251                        + " onCreateLoader(messages) listContext=" + listContext);
1252            }
1253
1254            if (mListContext.isSearch()) {
1255                final MessageListContext searchInfo = mListContext;
1256
1257                // Search results are not primed with local data, and so will usually be slow.
1258                // In some cases, they could take a long time to return, so we need to be robust.
1259                setListShownNoAnimation(false);
1260                Utility.getMainThreadHandler().postDelayed(new Runnable() {
1261                    @Override
1262                    public void run() {
1263                        if (mListContext != searchInfo) {
1264                            // Different list is being shown now.
1265                            return;
1266                        }
1267                        if (!mIsFirstLoad) {
1268                            // Something already returned. No need to do anything.
1269                            return;
1270                        }
1271                        onSearchLoadTimeout();
1272                    }
1273                }, SEARCH_WARNING_DELAY_MS);
1274            }
1275
1276            mIsFirstLoad = true;
1277            return MessagesAdapter.createLoader(getActivity(), listContext);
1278        }
1279
1280        @Override
1281        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
1282            if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
1283                Log.d(Logging.LOG_TAG, MessageListFragment.this
1284                        + " onLoadFinished(messages) mailboxId=" + getMailboxId());
1285            }
1286            MessagesAdapter.MessagesCursor cursor = (MessagesAdapter.MessagesCursor) c;
1287
1288            if (!cursor.mIsFound) {
1289                mCallback.onMailboxNotFound();
1290                return;
1291            }
1292
1293            // Get the "extras" part.
1294            mAccount = cursor.mAccount;
1295            mMailbox = cursor.mMailbox;
1296            mIsEasAccount = cursor.mIsEasAccount;
1297            mIsRefreshable = cursor.mIsRefreshable;
1298            mCountTotalAccounts = cursor.mCountTotalAccounts;
1299
1300            // Suspend message notifications as long as we're resumed
1301            adjustMessageNotification(false);
1302
1303            // If this is a search mailbox, set the query; otherwise, clear it
1304            if (mIsFirstLoad) {
1305                if (mMailbox != null && mMailbox.mType == Mailbox.TYPE_SEARCH) {
1306                    mListAdapter.setQuery(getListContext().getSearchParams().mFilter);
1307                    mSearchedMailbox = ((SearchResultsCursor) c).getSearchedMailbox();
1308                } else {
1309                    mListAdapter.setQuery(null);
1310                    mSearchedMailbox = null;
1311                }
1312                updateMailboxSpecificActions();
1313
1314                // Show chips if combined view.
1315                mListAdapter.setShowColorChips(isCombinedMailbox() && mCountTotalAccounts > 1);
1316            }
1317
1318            // Update the list
1319            mListAdapter.swapCursor(cursor);
1320
1321            // Various post processing...
1322            updateSearchHeader(cursor);
1323            autoRefreshStaleMailbox();
1324            updateFooterView();
1325            updateSelectionMode();
1326
1327            // We want to make visible the selection only for the first load.
1328            // Re-load caused by content changed events shouldn't scroll the list.
1329            highlightSelectedMessage(mIsFirstLoad);
1330
1331            if (mIsFirstLoad) {
1332                UiUtilities.setVisibilitySafe(mWarningContainer, View.GONE);
1333                mListPanel.setVisibility(View.VISIBLE);
1334
1335                // Setting the adapter will automatically transition from "Loading" to showing
1336                // the list, which could show "No messages". Avoid showing that on the first sync,
1337                // if we know we're still potentially loading more.
1338                if (!isEmptyAndLoading(cursor)) {
1339                    setListAdapter(mListAdapter);
1340                }
1341            } else if ((getListAdapter() == null) && !isEmptyAndLoading(cursor)) {
1342                setListAdapter(mListAdapter);
1343            }
1344
1345            // Restore the state -- this step has to be the last, because Some of the
1346            // "post processing" seems to reset the scroll position.
1347            if (mSavedListState != null) {
1348                getListView().onRestoreInstanceState(mSavedListState);
1349                mSavedListState = null;
1350            }
1351
1352            mIsFirstLoad = false;
1353        }
1354
1355        /**
1356         * Determines whether or not the list is empty, but we're still potentially loading data.
1357         * This represents an ambiguous state where we may not want to show "No messages", since
1358         * it may still just be loading.
1359         */
1360        private boolean isEmptyAndLoading(Cursor cursor) {
1361            return (cursor.getCount() == 0)
1362                        && mRefreshManager.isMessageListRefreshing(mMailbox.mId);
1363        }
1364
1365        @Override
1366        public void onLoaderReset(Loader<Cursor> loader) {
1367            if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
1368                Log.d(Logging.LOG_TAG, MessageListFragment.this
1369                        + " onLoaderReset(messages)");
1370            }
1371            mListAdapter.swapCursor(null);
1372            mAccount = null;
1373            mMailbox = null;
1374            mSearchedMailbox = null;
1375            mCountTotalAccounts = 0;
1376        }
1377    }
1378
1379    /**
1380     * Show/hide the "selection" action mode, according to the number of selected messages and
1381     * the visibility of the fragment.
1382     * Also update the content (title and menus) if necessary.
1383     */
1384    public void updateSelectionMode() {
1385        final int numSelected = getSelectedCount();
1386        if ((numSelected == 0) || mDisableCab || !isViewCreated()) {
1387            finishSelectionMode();
1388            return;
1389        }
1390        if (isInSelectionMode()) {
1391            updateSelectionModeView();
1392        } else {
1393            mLastSelectionModeCallback = new SelectionModeCallback();
1394            getActivity().startActionMode(mLastSelectionModeCallback);
1395        }
1396    }
1397
1398
1399    /**
1400     * Finish the "selection" action mode.
1401     *
1402     * Note this method finishes the contextual mode, but does *not* clear the selection.
1403     * If you want to do so use {@link #onDeselectAll()} instead.
1404     */
1405    private void finishSelectionMode() {
1406        if (isInSelectionMode()) {
1407            mLastSelectionModeCallback.mClosedByUser = false;
1408            mSelectionMode.finish();
1409        }
1410    }
1411
1412    /** Update the "selection" action mode bar */
1413    private void updateSelectionModeView() {
1414        mSelectionMode.invalidate();
1415    }
1416
1417    private class SelectionModeCallback implements ActionMode.Callback {
1418        private MenuItem mMarkRead;
1419        private MenuItem mMarkUnread;
1420        private MenuItem mAddStar;
1421        private MenuItem mRemoveStar;
1422        private MenuItem mMove;
1423
1424        /* package */ boolean mClosedByUser = true;
1425
1426        @Override
1427        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1428            mSelectionMode = mode;
1429
1430            MenuInflater inflater = getActivity().getMenuInflater();
1431            inflater.inflate(R.menu.message_list_fragment_cab_options, menu);
1432            mMarkRead = menu.findItem(R.id.mark_read);
1433            mMarkUnread = menu.findItem(R.id.mark_unread);
1434            mAddStar = menu.findItem(R.id.add_star);
1435            mRemoveStar = menu.findItem(R.id.remove_star);
1436            mMove = menu.findItem(R.id.move);
1437            return true;
1438        }
1439
1440        @Override
1441        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1442            int num = getSelectedCount();
1443            // Set title -- "# selected"
1444            mSelectionMode.setTitle(getActivity().getResources().getQuantityString(
1445                    R.plurals.message_view_selected_message_count, num, num));
1446
1447            // Show appropriate menu items.
1448            boolean nonStarExists = doesSelectionContainNonStarredMessage();
1449            boolean readExists = doesSelectionContainReadMessage();
1450            mMarkRead.setVisible(!readExists);
1451            mMarkUnread.setVisible(readExists);
1452            mAddStar.setVisible(nonStarExists);
1453            mRemoveStar.setVisible(!nonStarExists);
1454            mMove.setVisible(mShowMoveCommand);
1455            return true;
1456        }
1457
1458        @Override
1459        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1460            Set<Long> selectedConversations = mListAdapter.getSelectedSet();
1461            if (selectedConversations.isEmpty()) return true;
1462            switch (item.getItemId()) {
1463                case R.id.mark_read:
1464                    // Note - marking as read does not trigger auto-advance.
1465                    toggleRead(selectedConversations);
1466                    break;
1467                case R.id.mark_unread:
1468                    mCallback.onAdvancingOpAccepted(selectedConversations);
1469                    toggleRead(selectedConversations);
1470                    break;
1471                case R.id.add_star:
1472                case R.id.remove_star:
1473                    // TODO: removing a star can be a destructive command and cause auto-advance
1474                    // if the current mailbox shown is favorites.
1475                    toggleFavorite(selectedConversations);
1476                    break;
1477                case R.id.delete:
1478                    mCallback.onAdvancingOpAccepted(selectedConversations);
1479                    deleteMessages(selectedConversations);
1480                    break;
1481                case R.id.move:
1482                    showMoveMessagesDialog(selectedConversations);
1483                    break;
1484            }
1485            return true;
1486        }
1487
1488        @Override
1489        public void onDestroyActionMode(ActionMode mode) {
1490            // Clear this before onDeselectAll() to prevent onDeselectAll() from trying to close the
1491            // contextual mode again.
1492            mSelectionMode = null;
1493            if (mClosedByUser) {
1494                // Clear selection, only when the contextual mode is explicitly closed by the user.
1495                //
1496                // We close the contextual mode when the fragment becomes temporary invisible
1497                // (i.e. mIsVisible == false) too, in which case we want to keep the selection.
1498                onDeselectAll();
1499            }
1500        }
1501    }
1502
1503    private class RefreshListener implements RefreshManager.Listener {
1504        @Override
1505        public void onMessagingError(long accountId, long mailboxId, String message) {
1506        }
1507
1508        @Override
1509        public void onRefreshStatusChanged(long accountId, long mailboxId) {
1510            updateListFooter();
1511        }
1512    }
1513
1514    /**
1515     * Highlight the selected message.
1516     */
1517    private void highlightSelectedMessage(boolean ensureSelectionVisible) {
1518        if (!isViewCreated()) {
1519            return;
1520        }
1521
1522        final ListView lv = getListView();
1523        if (mSelectedMessageId == -1) {
1524            // No message selected
1525            lv.clearChoices();
1526            return;
1527        }
1528
1529        final int count = lv.getCount();
1530        for (int i = 0; i < count; i++) {
1531            if (lv.getItemIdAtPosition(i) != mSelectedMessageId) {
1532                continue;
1533            }
1534            lv.setItemChecked(i, true);
1535            if (ensureSelectionVisible) {
1536                Utility.listViewSmoothScrollToPosition(getActivity(), lv, i);
1537            }
1538            break;
1539        }
1540    }
1541}
1542