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