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