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