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