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