• 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.app.LoaderManager.LoaderCallbacks;
23 import android.content.ClipData;
24 import android.content.ClipDescription;
25 import android.content.Context;
26 import android.content.Loader;
27 import android.database.Cursor;
28 import android.graphics.Rect;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Parcelable;
32 import android.util.Log;
33 import android.view.DragEvent;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.View.OnDragListener;
37 import android.view.ViewGroup;
38 import android.widget.AdapterView;
39 import android.widget.AdapterView.OnItemClickListener;
40 import android.widget.ListView;
41 
42 import com.android.email.Controller;
43 import com.android.email.Email;
44 import com.android.email.R;
45 import com.android.email.RefreshManager;
46 import com.android.email.provider.EmailProvider;
47 import com.android.emailcommon.Logging;
48 import com.android.emailcommon.provider.Account;
49 import com.android.emailcommon.provider.Mailbox;
50 import com.android.emailcommon.utility.EmailAsyncTask;
51 import com.android.emailcommon.utility.Utility;
52 import com.google.common.annotations.VisibleForTesting;
53 
54 import java.util.Timer;
55 
56 /**
57  * This fragment presents a list of mailboxes for a given account or the combined mailboxes.
58  *
59  * This fragment has several parameters that determine the current view.
60  *
61  * <pre>
62  * Parameters:
63  * - Account ID.
64  *   - Set via {@link #newInstance}.
65  *   - Can be obtained with {@link #getAccountId()}.
66  *   - Will not change throughout fragment lifecycle.
67  *   - Either an actual account ID, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
68  *
69  * - "Highlight enabled?" flag
70  *   - Set via {@link #newInstance}.
71  *   - Can be obtained with {@link #getEnableHighlight()}.
72  *   - Will not change throughout fragment lifecycle.
73  *   - If {@code true}, we highlight the "selected" mailbox (used only on 2-pane).
74  *   - Note even if it's {@code true}, there may be no highlighted mailbox.
75  *     (This usually happens on 2-pane before the UI controller finds the Inbox to highlight.)
76  *
77  * - "Parent" mailbox ID
78  *   - Stored in {@link #mParentMailboxId}
79  *   - Changes as the user navigates through nested mailboxes.
80  *   - Initialized using the {@code mailboxId} parameter for {@link #newInstance}
81  *     in {@link #setInitialParentAndHighlight()}.
82  *
83  * - "Highlighted" mailbox
84  *   - Only used when highlighting is enabled.  (Otherwise always {@link Mailbox#NO_MAILBOX}.)
85  *     i.e. used only on two-pane.
86  *   - Stored in {@link #mHighlightedMailboxId}
87  *   - Initialized using the {@code mailboxId} parameter for {@link #newInstance}
88  *     in {@link #setInitialParentAndHighlight()}.
89  *
90  *   - Can be changed any time, using {@link #setHighlightedMailbox(long)}.
91  *
92  *   - If set, it's considered "selected", and we highlight the list item.
93  *
94  *   - (It should always be the ID of the list item selected in the list view, but we store it in
95  *     a member for efficiency.)
96  *
97  *   - Sometimes, we need to set the highlighted mailbox while we're still loading data.
98  *     In this case, we can't update {@link #mHighlightedMailboxId} right away, but need to do so
99  *     in when the next data set arrives, in
100  *     {@link MailboxListFragment.MailboxListLoaderCallbacks#onLoadFinished}.  For this, we use
101  *     we store the mailbox ID in {@link #mNextHighlightedMailboxId} and update
102  *     {@link #mHighlightedMailboxId} in onLoadFinished.
103  *
104  *
105  * The "selected" is defined using the "parent" and "highlighted" mailboxes.
106  * - "Selected" mailbox  (also sometimes called "current".)
107  *   - This is what the user thinks it's now selected.
108  *
109  *   - Can be obtained with {@link #getSelectedMailboxId()}
110  *   - If the "highlighted" mailbox exists, it's the "selected."  Otherwise, the "parent"
111  *     is considered "selected."
112  *   - This is what is passed to {@link Callback#onMailboxSelected}.
113  * </pre>
114  *
115  *
116  * This fragment shows the content in one of the three following views, depending on the
117  * parameters above.
118  *
119  * <pre>
120  * 1. Combined view
121  *   - Used if the account ID == {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
122  *   - Parent mailbox is always {@link Mailbox#NO_MAILBOX}.
123  *   - List contains:
124  *     - combined mailboxes
125  *     - all accounts
126  *
127  * 2. Root view for an account
128  *   - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and
129  *     Parent mailbox == {@link Mailbox#NO_MAILBOX}
130  *   - List contains
131  *     - all the top level mailboxes for the selected account.
132  *
133  * 3. Root view for a mailbox.  (nested view)
134  *   - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and
135  *     Parent mailbox != {@link Mailbox#NO_MAILBOX}
136  *   - List contains:
137  *     - parent mailbox (determined by "parent" mailbox ID)
138  *     - all child mailboxes of the parent mailbox.
139  * </pre>
140  *
141  *
142  * Note that when a fragment is put in the back stack, it'll lose the content view but the fragment
143  * itself is not destroyed.  If you call {@link #getListView()} in this state it'll throw
144  * an {@link IllegalStateException}.  So,
145  * - If code is supposed to be executed only when the fragment has the content view, use
146  *   {@link #getListView()} directly to make sure it doesn't accidentally get executed when there's
147  *   no views.
148  * - Otherwise, make sure to check if the fragment has views with {@link #isViewCreated()}
149  *   before touching any views.
150  */
151 public class MailboxListFragment extends ListFragment implements OnItemClickListener,
152         OnDragListener {
153     private static final String TAG = "MailboxListFragment";
154 
155     private static final String BUNDLE_KEY_PARENT_MAILBOX_ID
156             = "MailboxListFragment.state.parent_mailbox_id";
157     private static final String BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID
158             = "MailboxListFragment.state.selected_mailbox_id";
159     private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState";
160     private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE
161 
162     /** No drop target is available where the user is currently hovering over */
163     private static final int NO_DROP_TARGET = -1;
164     // Total height of the top and bottom scroll zones, in pixels
165     private static final int SCROLL_ZONE_SIZE = 64;
166     // The amount of time to scroll by one pixel, in ms
167     private static final int SCROLL_SPEED = 4;
168 
169     /** Arbitrary number for use with the loader manager */
170     private static final int MAILBOX_LOADER_ID = 1;
171 
172     /** Argument name(s) */
173     private static final String ARG_ACCOUNT_ID = "accountId";
174     private static final String ARG_ENABLE_HIGHLIGHT = "enablehighlight";
175     private static final String ARG_INITIAL_CURRENT_MAILBOX_ID = "initialParentMailboxId";
176 
177     private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
178 
179     /** Rectangle used for hit testing children */
180     private static final Rect sTouchFrame = new Rect();
181 
182     private RefreshManager mRefreshManager;
183 
184     // UI Support
185     private Activity mActivity;
186     private MailboxFragmentAdapter mListAdapter;
187     private Callback mCallback = EmptyCallback.INSTANCE;
188 
189     // See the class javadoc
190     private long mParentMailboxId;
191     private long mHighlightedMailboxId;
192 
193     /**
194      * Becomes {@code true} once we determine which mailbox to use as the parent.
195      */
196     private boolean mParentDetermined;
197 
198     /**
199      * ID of the mailbox that should be highlighted when the next cursor is loaded.
200      */
201     private long mNextHighlightedMailboxId = Mailbox.NO_MAILBOX;
202 
203     // True if a drag is currently in progress
204     private boolean mDragInProgress;
205     /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */
206     private long mDragItemMailboxId = -1;
207     /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */
208     private int mDropTargetId = NO_DROP_TARGET;
209     // The mailbox list item view that the user's finger is hovering over
210     private MailboxListItem mDropTargetView;
211     // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized')
212     private int mDragItemHeight = -1;
213     /** {@code true} if we are currently scrolling under the drag item */
214     private boolean mTargetScrolling;
215 
216     private Parcelable mSavedListState;
217 
218     private final MailboxFragmentAdapter.Callback mMailboxesAdapterCallback =
219             new MailboxFragmentAdapter.Callback() {
220         @Override
221         public void onBind(MailboxListItem listItem) {
222             listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
223         }
224     };
225 
226     /**
227      * Callback interface that owning activities must implement
228      */
229     public interface Callback {
230         /**
231          * Called when any mailbox (even a combined mailbox) is selected.
232          *
233          * @param accountId
234          *          The ID of the owner account of the selected mailbox.
235          *          Or {@link Account#ACCOUNT_ID_COMBINED_VIEW} if it's a combined mailbox.
236          * @param mailboxId
237          *          The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0],
238          *          or a combined mailbox ID [e.g. {@link Mailbox#QUERY_ALL_INBOXES}].
239          * @param nestedNavigation {@code true} if the event is caused by nested mailbox navigation,
240          *          that is, going up or drilling-in to a child mailbox.
241          */
onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation)242         public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation);
243 
244         /** Called when an account is selected on the combined view. */
onAccountSelected(long accountId)245         public void onAccountSelected(long accountId);
246 
247         /**
248          * Called when the parent mailbox is changing.
249          */
onParentMailboxChanged()250         public void onParentMailboxChanged();
251     }
252 
253     private static class EmptyCallback implements Callback {
254         public static final Callback INSTANCE = new EmptyCallback();
onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation)255         @Override public void onMailboxSelected(long accountId, long mailboxId,
256                 boolean nestedNavigation) { }
onAccountSelected(long accountId)257         @Override public void onAccountSelected(long accountId) { }
258         @Override
onParentMailboxChanged()259         public void onParentMailboxChanged() { }
260     }
261 
262     /**
263      * Returns the index of the view located at the specified coordinates in the given list.
264      * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned.
265      */
pointToIndex(ListView list, int x, int y)266     private static int pointToIndex(ListView list, int x, int y) {
267         final int count = list.getChildCount();
268         for (int i = count - 1; i >= 0; i--) {
269             final View child = list.getChildAt(i);
270             if (child.getVisibility() == View.VISIBLE) {
271                 child.getHitRect(sTouchFrame);
272                 if (sTouchFrame.contains(x, y)) {
273                     return i;
274                 }
275             }
276         }
277         return NO_DROP_TARGET;
278     }
279 
280     /**
281      * Create a new instance with initialization parameters.
282      *
283      * This fragment should be created only with this method.  (Arguments should always be set.)
284      *
285      * @param accountId The ID of the account we want to view
286      * @param initialCurrentMailboxId ID of the mailbox of interest.
287      *        Pass {@link Mailbox#NO_MAILBOX} to show top-level mailboxes.
288      * @param enableHighlight {@code true} if highlighting is enabled on the current screen
289      *        configuration.  (We don't highlight mailboxes on one-pane.)
290      */
newInstance(long accountId, long initialCurrentMailboxId, boolean enableHighlight)291     public static MailboxListFragment newInstance(long accountId, long initialCurrentMailboxId,
292             boolean enableHighlight) {
293         final MailboxListFragment instance = new MailboxListFragment();
294         final Bundle args = new Bundle();
295         args.putLong(ARG_ACCOUNT_ID, accountId);
296         args.putLong(ARG_INITIAL_CURRENT_MAILBOX_ID, initialCurrentMailboxId);
297         args.putBoolean(ARG_ENABLE_HIGHLIGHT, enableHighlight);
298         instance.setArguments(args);
299         return instance;
300     }
301 
302     /**
303      * The account ID the mailbox is associated with. Do not use directly; instead, use
304      * {@link #getAccountId()}.
305      * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
306      * constructs, this <em>must</em> be considered immutable.
307      */
308     private Long mImmutableAccountId;
309 
310     /**
311      * {@code initialCurrentMailboxId} passed to {@link #newInstance}.
312      * Do not use directly; instead, use {@link #getInitialCurrentMailboxId()}.
313      * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
314      * constructs, this <em>must</em> be considered immutable.
315      */
316     private long mImmutableInitialCurrentMailboxId;
317 
318     /**
319      * {@code enableHighlight} passed to {@link #newInstance}.
320      * Do not use directly; instead, use {@link #getEnableHighlight()}.
321      * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
322      * constructs, this <em>must</em> be considered immutable.
323      */
324     private boolean mImmutableEnableHighlight;
325 
initializeArgCache()326     private void initializeArgCache() {
327         if (mImmutableAccountId != null) return;
328         mImmutableAccountId = getArguments().getLong(ARG_ACCOUNT_ID);
329         mImmutableInitialCurrentMailboxId = getArguments().getLong(ARG_INITIAL_CURRENT_MAILBOX_ID);
330         mImmutableEnableHighlight = getArguments().getBoolean(ARG_ENABLE_HIGHLIGHT);
331     }
332 
333     /**
334      * @return {@code accountId} passed to {@link #newInstance}.  Safe to call even before onCreate.
335      */
getAccountId()336     public long getAccountId() {
337         initializeArgCache();
338         return mImmutableAccountId;
339     }
340 
341     /**
342      * @return {@code initialCurrentMailboxId} passed to {@link #newInstance}.
343      * Safe to call even before onCreate.
344      */
getInitialCurrentMailboxId()345     public long getInitialCurrentMailboxId() {
346         initializeArgCache();
347         return mImmutableInitialCurrentMailboxId;
348     }
349 
350     /**
351      * @return {@code enableHighlight} passed to {@link #newInstance}.
352      * Safe to call even before onCreate.
353      */
getEnableHighlight()354     public boolean getEnableHighlight() {
355         initializeArgCache();
356         return mImmutableEnableHighlight;
357     }
358 
359     @Override
onAttach(Activity activity)360     public void onAttach(Activity activity) {
361         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
362             Log.d(Logging.LOG_TAG, this + " onAttach");
363         }
364         super.onAttach(activity);
365     }
366 
367     /**
368      * Called to do initial creation of a fragment.  This is called after
369      * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
370      */
371     @Override
onCreate(Bundle savedInstanceState)372     public void onCreate(Bundle savedInstanceState) {
373         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
374             Log.d(Logging.LOG_TAG, this + " onCreate");
375         }
376         super.onCreate(savedInstanceState);
377 
378         mActivity = getActivity();
379         mRefreshManager = RefreshManager.getInstance(mActivity);
380         mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback);
381         setListAdapter(mListAdapter); // It's safe to do even before the list view is created.
382 
383         if (savedInstanceState == null) {
384             setInitialParentAndHighlight();
385         } else {
386             restoreInstanceState(savedInstanceState);
387         }
388     }
389 
390     /**
391      * Set {@link #mParentMailboxId} and {@link #mHighlightedMailboxId} from the fragment arguments.
392      */
setInitialParentAndHighlight()393     private void setInitialParentAndHighlight() {
394         final long initialMailboxId = getInitialCurrentMailboxId();
395         if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) {
396             // For the combined view, always show the top-level, but highlight the "current".
397             mParentMailboxId = Mailbox.NO_MAILBOX;
398         } else {
399             // Inbox needs special care.
400             // Note we can't get the mailbox type on the UI thread but this method *can* be used...
401             final long inboxId = Mailbox.findMailboxOfType(getActivity(), getAccountId(),
402                     Mailbox.TYPE_INBOX);
403             if (initialMailboxId == inboxId) {
404                 // If Inbox is set as the initial current, we show the top level mailboxes
405                 // with inbox highlighted.
406                 mParentMailboxId = Mailbox.NO_MAILBOX;
407             } else {
408                 // Otherwise, try using the "current" as the "parent" (and also highlight it).
409                 // If it has no children, we go up in onLoadFinished().
410                 mParentMailboxId = initialMailboxId;
411             }
412         }
413         // Highlight the mailbox of interest
414         if (getEnableHighlight()) {
415             mHighlightedMailboxId = initialMailboxId;
416         }
417     }
418 
419     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)420     public View onCreateView(
421             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
422         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
423             Log.d(Logging.LOG_TAG, this + " onCreateView");
424         }
425         return inflater.inflate(R.layout.mailbox_list_fragment, container, false);
426     }
427 
428     /**
429      * @return true if the content view is created and not destroyed yet. (i.e. between
430      * {@link #onCreateView} and {@link #onDestroyView}.
431      */
isViewCreated()432     private boolean isViewCreated() {
433         return getView() != null;
434     }
435 
436     @Override
onActivityCreated(Bundle savedInstanceState)437     public void onActivityCreated(Bundle savedInstanceState) {
438         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
439             Log.d(Logging.LOG_TAG, this + " onActivityCreated");
440         }
441         super.onActivityCreated(savedInstanceState);
442 
443         // Note we can't do this in onCreateView.
444         // getListView() is only usable after onCreateView().
445         final ListView lv = getListView();
446         lv.setOnItemClickListener(this);
447         lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
448         lv.setOnDragListener(this);
449 
450         startLoading(mParentMailboxId, mHighlightedMailboxId);
451 
452         UiUtilities.installFragment(this);
453     }
454 
setCallback(Callback callback)455     public void setCallback(Callback callback) {
456         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
457     }
458 
459     /**
460      * Called when the Fragment is visible to the user.
461      */
462     @Override
onStart()463     public void onStart() {
464         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
465             Log.d(Logging.LOG_TAG, this + " onStart");
466         }
467         super.onStart();
468     }
469 
470     /**
471      * Called when the fragment is visible to the user and actively running.
472      */
473     @Override
onResume()474     public void onResume() {
475         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
476             Log.d(Logging.LOG_TAG, this + " onResume");
477         }
478         super.onResume();
479 
480         // Fetch the latest mailbox list from the server here if stale so that the user always
481         // sees the (reasonably) up-to-date mailbox list, without pressing "refresh".
482         final long accountId = getAccountId();
483         if (mRefreshManager.isMailboxListStale(accountId)) {
484             mRefreshManager.refreshMailboxList(accountId);
485         }
486     }
487 
488     @Override
onPause()489     public void onPause() {
490         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
491             Log.d(Logging.LOG_TAG, this + " onPause");
492         }
493         mSavedListState = getListView().onSaveInstanceState();
494         super.onPause();
495     }
496 
497     /**
498      * Called when the Fragment is no longer started.
499      */
500     @Override
onStop()501     public void onStop() {
502         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
503             Log.d(Logging.LOG_TAG, this + " onStop");
504         }
505         super.onStop();
506     }
507 
508     @Override
onDestroyView()509     public void onDestroyView() {
510         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
511             Log.d(Logging.LOG_TAG, this + " onDestroyView");
512         }
513         UiUtilities.uninstallFragment(this);
514         super.onDestroyView();
515     }
516 
517     /**
518      * Called when the fragment is no longer in use.
519      */
520     @Override
onDestroy()521     public void onDestroy() {
522         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
523             Log.d(Logging.LOG_TAG, this + " onDestroy");
524         }
525         mTaskTracker.cancellAllInterrupt();
526         super.onDestroy();
527     }
528 
529     @Override
onDetach()530     public void onDetach() {
531         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
532             Log.d(Logging.LOG_TAG, this + " onDetach");
533         }
534         super.onDetach();
535     }
536 
537     @Override
onSaveInstanceState(Bundle outState)538     public void onSaveInstanceState(Bundle outState) {
539         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
540             Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
541         }
542         super.onSaveInstanceState(outState);
543         outState.putLong(BUNDLE_KEY_PARENT_MAILBOX_ID, mParentMailboxId);
544         outState.putLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID, mHighlightedMailboxId);
545         if (isViewCreated()) {
546             outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState());
547         }
548     }
549 
restoreInstanceState(Bundle savedInstanceState)550     private void restoreInstanceState(Bundle savedInstanceState) {
551         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
552             Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
553         }
554         mParentMailboxId = savedInstanceState.getLong(BUNDLE_KEY_PARENT_MAILBOX_ID);
555         mNextHighlightedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID);
556         mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
557     }
558 
559     /**
560      * @return "Selected" mailbox ID.
561      */
getSelectedMailboxId()562     public long getSelectedMailboxId() {
563         return (mHighlightedMailboxId != Mailbox.NO_MAILBOX) ? mHighlightedMailboxId
564                 : mParentMailboxId;
565     }
566 
567     /**
568      * @return {@code true} if top-level mailboxes are shown.  {@code false} otherwise.
569      */
isRoot()570     private boolean isRoot() {
571         return mParentMailboxId == Mailbox.NO_MAILBOX;
572     }
573 
574     /**
575      * Navigate one level up in the mailbox hierarchy. Does nothing if at the root account view.
576      */
navigateUp()577     public boolean navigateUp() {
578         if (isRoot()) {
579             return false;
580         }
581         FindParentMailboxTask.ResultCallback callback = new FindParentMailboxTask.ResultCallback() {
582             @Override public void onResult(long nextParentMailboxId,
583                     long nextHighlightedMailboxId, long nextSelectedMailboxId) {
584 
585                 startLoading(nextParentMailboxId, nextHighlightedMailboxId);
586             }
587         };
588         new FindParentMailboxTask(
589                 getActivity().getApplicationContext(), mTaskTracker, getAccountId(),
590                 getEnableHighlight(), mParentMailboxId, mHighlightedMailboxId, callback
591                 ).cancelPreviousAndExecuteParallel((Void[]) null);
592         return true;
593     }
594 
595     /**
596      * @return {@code true} if the fragment is showing nested mailboxes and we can go one level up.
597      *         {@code false} otherwise, meaning we're showing the top level mailboxes *OR*
598      *         we're still loading initial data and we can't determine if we're going to show
599      *         top-level or not.
600      */
canNavigateUp()601     public boolean canNavigateUp() {
602         if (!mParentDetermined) {
603             return false; // We can't determine yet...
604         }
605         return !isRoot();
606     }
607 
608     /**
609      * A task to determine what parent mailbox ID/highlighted mailbox ID to use for the "UP"
610      * navigation, given the current parent mailbox ID, the highlighted mailbox ID, and {@link
611      * #mEnableHighlight}.
612      */
613     @VisibleForTesting
614     static class FindParentMailboxTask extends EmailAsyncTask<Void, Void, Long[]> {
615         public interface ResultCallback {
616             /**
617              * Callback to get the result.
618              *
619              * @param nextParentMailboxId ID of the mailbox to use
620              * @param nextHighlightedMailboxId ID of the mailbox to highlight
621              * @param nextSelectedMailboxId ID of the mailbox to notify with
622              *        {@link Callback#onMailboxSelected}.
623              */
onResult(long nextParentMailboxId, long nextHighlightedMailboxId, long nextSelectedMailboxId)624             public void onResult(long nextParentMailboxId, long nextHighlightedMailboxId,
625                     long nextSelectedMailboxId);
626         }
627 
628         private final Context mContext;
629         private final long mAccountId;
630         private final boolean mEnableHighlight;
631         private final long mParentMailboxId;
632         private final long mHighlightedMailboxId;
633         private final ResultCallback mCallback;
634 
FindParentMailboxTask(Context context, EmailAsyncTask.Tracker taskTracker, long accountId, boolean enableHighlight, long parentMailboxId, long highlightedMailboxId, ResultCallback callback)635         public FindParentMailboxTask(Context context, EmailAsyncTask.Tracker taskTracker,
636                 long accountId, boolean enableHighlight, long parentMailboxId,
637                 long highlightedMailboxId, ResultCallback callback) {
638             super(taskTracker);
639             mContext = context;
640             mAccountId = accountId;
641             mEnableHighlight = enableHighlight;
642             mParentMailboxId = parentMailboxId;
643             mHighlightedMailboxId = highlightedMailboxId;
644             mCallback = callback;
645         }
646 
647         @Override
doInBackground(Void... params)648         protected Long[] doInBackground(Void... params) {
649             Mailbox parentMailbox = Mailbox.restoreMailboxWithId(mContext, mParentMailboxId);
650             final long nextParentId = (parentMailbox == null) ? Mailbox.NO_MAILBOX
651                     : parentMailbox.mParentKey;
652             final long nextHighlightedId;
653             final long nextSelectedId;
654             if (mEnableHighlight) {
655                 // If the "parent" is highlighted before the transition, it should still be
656                 // highlighted after the upper level view.
657                 if (mParentMailboxId == mHighlightedMailboxId) {
658                     nextHighlightedId = mParentMailboxId;
659                 } else {
660                     // Otherwise, the next parent will be highlighted, unless we're going up to
661                     // the root, in which case Inbox should be highlighted.
662                     if (nextParentId == Mailbox.NO_MAILBOX) {
663                         nextHighlightedId = Mailbox.findMailboxOfType(mContext, mAccountId,
664                                 Mailbox.TYPE_INBOX);
665                     } else {
666                         nextHighlightedId = nextParentId;
667                     }
668                 }
669 
670                 // Highlighted one will be "selected".
671                 nextSelectedId = nextHighlightedId;
672 
673             } else { // !mEnableHighlight
674                 nextHighlightedId = Mailbox.NO_MAILBOX;
675 
676                 // Parent will be selected.
677                 nextSelectedId = nextParentId;
678             }
679             return new Long[]{nextParentId, nextHighlightedId, nextSelectedId};
680         }
681 
682         @Override
onSuccess(Long[] result)683         protected void onSuccess(Long[] result) {
684             mCallback.onResult(result[0], result[1], result[2]);
685         }
686     }
687 
688     /**
689      * Starts the loader.
690      *
691      * @param parentMailboxId Mailbox ID to be used as the "parent" mailbox
692      * @param highlightedMailboxId Mailbox ID that should be highlighted when the data is loaded.
693      */
startLoading(long parentMailboxId, long highlightedMailboxId)694     private void startLoading(long parentMailboxId, long highlightedMailboxId) {
695         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
696             Log.d(Logging.LOG_TAG, this + " startLoading  parent=" + parentMailboxId
697                     + " highlighted=" + highlightedMailboxId);
698         }
699         final LoaderManager lm = getLoaderManager();
700         boolean parentMailboxChanging = false;
701 
702         // Parent mailbox changing -- destroy the current loader to force reload.
703         if (mParentMailboxId != parentMailboxId) {
704             lm.destroyLoader(MAILBOX_LOADER_ID);
705             setListShown(false);
706             parentMailboxChanging = true;
707         }
708         mParentMailboxId = parentMailboxId;
709         if (getEnableHighlight()) {
710             mNextHighlightedMailboxId = highlightedMailboxId;
711         }
712 
713         lm.initLoader(MAILBOX_LOADER_ID, null, new MailboxListLoaderCallbacks());
714 
715         if (parentMailboxChanging) {
716             mCallback.onParentMailboxChanged();
717         }
718     }
719 
720     /**
721      * Highlight the given mailbox.
722      *
723      * If data is already loaded, it just sets {@link #mHighlightedMailboxId} and highlight the
724      * corresponding list item.  (And if the corresponding list item is not found,
725      * {@link #mHighlightedMailboxId} is set to {@link Mailbox#NO_MAILBOX})
726      *
727      * If we're still loading data, it sets {@link #mNextHighlightedMailboxId} instead, and then
728      * it'll be set to {@link #mHighlightedMailboxId} in
729      * {@link MailboxListLoaderCallbacks#onLoadFinished}.
730      *
731      * @param mailboxId The ID of the mailbox to highlight.
732      */
setHighlightedMailbox(long mailboxId)733     public void setHighlightedMailbox(long mailboxId) {
734         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
735             Log.d(Logging.LOG_TAG, this + " setHighlightedMailbox  mailbox=" + mailboxId);
736         }
737         if (!getEnableHighlight()) {
738             return;
739         }
740         if (mHighlightedMailboxId == mailboxId) {
741             return; // already highlighted.
742         }
743         if (mListAdapter.getCursor() == null) {
744             // List not loaded yet.  Just remember the ID here and let onLoadFinished() update
745             // mHighlightedMailboxId.
746             mNextHighlightedMailboxId = mailboxId;
747             return;
748         }
749         mHighlightedMailboxId = mailboxId;
750         updateHighlightedMailbox(true);
751     }
752 
753     // TODO This class probably should be made static. There are many calls into the enclosing
754     // class and we need to be cautious about what we call while in these callbacks
755     private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> {
756         private boolean mIsFirstLoad;
757 
758         @Override
onCreateLoader(int id, Bundle args)759         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
760             if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
761                 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onCreateLoader");
762             }
763             mIsFirstLoad = true;
764             if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) {
765                 return MailboxFragmentAdapter.createCombinedViewLoader(getActivity());
766             } else {
767                 return MailboxFragmentAdapter.createMailboxesLoader(getActivity(), getAccountId(),
768                         mParentMailboxId);
769             }
770         }
771 
772         @Override
onLoadFinished(Loader<Cursor> loader, Cursor cursor)773         public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
774             if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
775                 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoadFinished  count="
776                         + cursor.getCount());
777             }
778             // Note in onLoadFinished we can assume the view is created.
779             // The loader manager doesn't deliver results when a fragment is stopped.
780 
781             // If we're showing a nested mailboxes, and the current parent mailbox has no children,
782             // go up.
783             if (getAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW) {
784                 MailboxFragmentAdapter.CursorWithExtras c =
785                         (MailboxFragmentAdapter.CursorWithExtras) cursor;
786                 if ((c.mChildCount == 0) && !isRoot()) {
787                     // Always swap out the cursor so we don't hold a reference to a stale one.
788                     mListAdapter.swapCursor(cursor);
789                     navigateUp();
790                     return;
791                 }
792             }
793 
794             // Save list view state (primarily scroll position)
795             final ListView lv = getListView();
796             final Parcelable listState;
797             if (mSavedListState != null) {
798                 listState = mSavedListState;
799                 mSavedListState = null;
800             } else {
801                 listState = lv.onSaveInstanceState();
802             }
803 
804             if (cursor.getCount() == 0) {
805                 // There's no row -- call setListShown(false) to make ListFragment show progress
806                 // icon.
807                 mListAdapter.swapCursor(null);
808                 setListShown(false);
809 
810             } else {
811                 mParentDetermined = true; // Okay now we're sure which mailbox is the parent.
812 
813                 mListAdapter.swapCursor(cursor);
814                 setListShown(true);
815 
816                 // Restore the list state, so scroll position is restored - this has to happen
817                 // prior to setting the checked/highlighted mailbox below.
818                 lv.onRestoreInstanceState(listState);
819 
820                 // Update the highlighted mailbox
821                 if (mNextHighlightedMailboxId != Mailbox.NO_MAILBOX) {
822                     setHighlightedMailbox(mNextHighlightedMailboxId);
823                     mNextHighlightedMailboxId = Mailbox.NO_MAILBOX;
824                 }
825 
826                 // We want to make visible the selection only for the first load.
827                 // Re-load caused by content changed events shouldn't scroll the list.
828                 if (!updateHighlightedMailbox(mIsFirstLoad)) {
829                     // This may happen if the mailbox to be selected is not actually in the list
830                     // that was loaded. Let the user just pick one manually if needed.
831                     return;
832                 }
833             }
834 
835             // List has been reloaded; clear any drop target information
836             mDropTargetId = NO_DROP_TARGET;
837             mDropTargetView = null;
838 
839             mIsFirstLoad = false;
840         }
841 
842         @Override
onLoaderReset(Loader<Cursor> loader)843         public void onLoaderReset(Loader<Cursor> loader) {
844             if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
845                 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoaderReset");
846             }
847             mListAdapter.swapCursor(null);
848         }
849     }
850 
851     /**
852      * {@inheritDoc}
853      * <p>
854      * @param doNotUse <em>IMPORTANT</em>: Do not use this parameter. The ID in the list widget
855      * must be a positive value. However, we rely on negative IDs for special mailboxes. Instead,
856      * we use the ID returned by {@link MailboxFragmentAdapter#getId(int)}.
857      */
858     @Override
onItemClick(AdapterView<?> parent, View view, int position, long doNotUse)859     public void onItemClick(AdapterView<?> parent, View view, int position, long doNotUse) {
860         final long id = mListAdapter.getId(position);
861         if (mListAdapter.isAccountRow(position)) {
862             mCallback.onAccountSelected(id);
863         } else if (mListAdapter.isMailboxRow(position)) {
864             // Save account-id.  (Need to do this before startLoading() below, which will destroy
865             // the current loader and make the mListAdapter lose the cursor.
866             // Note, don't just use getAccountId().  A mailbox may tied to a different account ID
867             // from getAccountId().  (Currently "Starred" does so.)
868             long accountId = mListAdapter.getAccountId(position);
869             boolean nestedNavigation = false;
870             if (((MailboxListItem) view).isNavigable() && (id != mParentMailboxId)) {
871                 // Drill-in.  Selected one will be the next parent, and it'll also be highlighted.
872                 startLoading(id, id);
873                 nestedNavigation = true;
874             }
875             if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
876                 // Virtual mailboxes, such as "Starred", will have a "combined view" ID. However,
877                 // we really want to relay the current active account, so that
878                 // things like per-account starred mailboxes work as expected.
879                 accountId = getAccountId();
880             }
881             mCallback.onMailboxSelected(accountId, id, nestedNavigation);
882         }
883     }
884 
885     /**
886      * Really highlight the mailbox for {@link #mHighlightedMailboxId} on the list view.
887      *
888      * Note if a list item for {@link #mHighlightedMailboxId} is not found,
889      * {@link #mHighlightedMailboxId} will be set to {@link Mailbox#NO_MAILBOX}.
890      *
891      * @return false when the highlighted mailbox seems to be gone; i.e. if
892      *         {@link #mHighlightedMailboxId} is set but not found in the list.
893      */
updateHighlightedMailbox(boolean ensureSelectionVisible)894     private boolean updateHighlightedMailbox(boolean ensureSelectionVisible) {
895         if (!getEnableHighlight() || !isViewCreated()) {
896             return true; // Nothing to highlight
897         }
898         final ListView lv = getListView();
899         boolean found = false;
900         if (mHighlightedMailboxId == Mailbox.NO_MAILBOX) {
901             // No mailbox selected
902             lv.clearChoices();
903             found = true;
904         } else {
905             // TODO Don't mix list view & list adapter indices. This is a recipe for disaster.
906             final int count = lv.getCount();
907             for (int i = 0; i < count; i++) {
908                 if (mListAdapter.getId(i) != mHighlightedMailboxId) {
909                     continue;
910                 }
911                 found = true;
912                 lv.setItemChecked(i, true);
913                 if (ensureSelectionVisible) {
914                     Utility.listViewSmoothScrollToPosition(getActivity(), lv, i);
915                 }
916                 break;
917             }
918         }
919         if (!found) {
920             mHighlightedMailboxId = Mailbox.NO_MAILBOX;
921         }
922         return found;
923     }
924 
925     // Drag & Drop handling
926 
927     /**
928      * Update all of the list's child views with the proper target background (for now, orange if
929      * a valid target, except red if the trash; standard background otherwise)
930      */
updateChildViews()931     private void updateChildViews() {
932         final ListView lv = getListView();
933         int itemCount = lv.getChildCount();
934         // Lazily initialize the height of our list items
935         if (itemCount > 0 && mDragItemHeight < 0) {
936             mDragItemHeight = lv.getChildAt(0).getHeight();
937         }
938         for (int i = 0; i < itemCount; i++) {
939             final View child = lv.getChildAt(i);
940             if (!(child instanceof MailboxListItem)) {
941                 continue;
942             }
943             MailboxListItem item = (MailboxListItem) child;
944             item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
945         }
946     }
947 
948     /**
949      * Called when the user has dragged outside of the mailbox list area.
950      */
onDragExited()951     private void onDragExited() {
952         // Reset the background of the current target
953         if (mDropTargetView != null) {
954             mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
955             mDropTargetView = null;
956         }
957         mDropTargetId = NO_DROP_TARGET;
958         stopScrolling();
959     }
960 
961     /**
962      * Called while dragging;  highlight possible drop targets, and auto scroll the list.
963      */
onDragLocation(DragEvent event)964     private void onDragLocation(DragEvent event) {
965         final ListView lv = getListView();
966         // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop
967         // if the list is being updated [i.e. navigated to another mailbox]
968         if (mDragItemHeight <= 0) {
969             // This shouldn't be possible, but avoid NPE
970             Log.w(TAG, "drag item height is not set");
971             return;
972         }
973         // Find out which item we're in and highlight as appropriate
974         final int rawTouchX = (int) event.getX();
975         final int rawTouchY = (int) event.getY();
976         final int viewIndex = pointToIndex(lv, rawTouchX, rawTouchY);
977         int targetId = viewIndex;
978         if (targetId != mDropTargetId) {
979             if (DEBUG_DRAG_DROP) {
980                 Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId);
981             }
982             // Remove highlight the current target; if there was one
983             if (mDropTargetView != null) {
984                 mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId);
985                 mDropTargetView = null;
986             }
987             // Get the new target mailbox view
988             final View childView = lv.getChildAt(viewIndex);
989             final MailboxListItem newTarget;
990             if (childView == null) {
991                 // In any event, we're no longer dragging in the list view if newTarget is null
992                 if (DEBUG_DRAG_DROP) {
993                     Log.d(TAG, "=== Drag off the list");
994                 }
995                 newTarget = null;
996                 final int childCount = lv.getChildCount();
997                 if (viewIndex >= childCount) {
998                     // Touching beyond the end of the list; may happen for small lists
999                     onDragExited();
1000                     return;
1001                 } else {
1002                     // We should never get here
1003                     Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount);
1004                 }
1005             } else if (!(childView instanceof MailboxListItem)) {
1006                 // We're over a header suchas "Recent folders".  We shouldn't finish DnD, but
1007                 // drop should be disabled.
1008                 newTarget = null;
1009                 targetId = NO_DROP_TARGET;
1010             } else {
1011                 newTarget = (MailboxListItem) childView;
1012                 if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) {
1013                     if (DEBUG_DRAG_DROP) {
1014                         Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId);
1015                     }
1016                     newTarget.setDropTrashBackground();
1017                 } else if (newTarget.isDropTarget(mDragItemMailboxId)) {
1018                     if (DEBUG_DRAG_DROP) {
1019                         Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId);
1020                     }
1021                     newTarget.setDropActiveBackground();
1022                 } else {
1023                     if (DEBUG_DRAG_DROP) {
1024                         Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId);
1025                     }
1026                     newTarget.setDropTargetBackground(true, mDragItemMailboxId);
1027                     targetId = NO_DROP_TARGET;
1028                 }
1029             }
1030             // Save away our current position and view
1031             mDropTargetId = targetId;
1032             mDropTargetView = newTarget;
1033         }
1034 
1035         // This is a quick-and-dirty implementation of drag-under-scroll; something like this
1036         // should eventually find its way into the framework
1037         int scrollDiff = rawTouchY - (lv.getHeight() - SCROLL_ZONE_SIZE);
1038         boolean scrollDown = (scrollDiff > 0);
1039         boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY);
1040         if (!mTargetScrolling && scrollDown) {
1041             int itemsToScroll = lv.getCount() - lv.getLastVisiblePosition();
1042             int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight;
1043             lv.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
1044             if (DEBUG_DRAG_DROP) {
1045                 Log.d(TAG, "=== Start scrolling list down");
1046             }
1047             mTargetScrolling = true;
1048         } else if (!mTargetScrolling && scrollUp) {
1049             int pixelsToScroll = (lv.getFirstVisiblePosition() + 1) * mDragItemHeight;
1050             lv.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
1051             if (DEBUG_DRAG_DROP) {
1052                 Log.d(TAG, "=== Start scrolling list up");
1053             }
1054             mTargetScrolling = true;
1055         } else if (!scrollUp && !scrollDown) {
1056             stopScrolling();
1057         }
1058     }
1059 
1060     /**
1061      * Indicate that scrolling has stopped
1062      */
stopScrolling()1063     private void stopScrolling() {
1064         final ListView lv = getListView();
1065         if (mTargetScrolling) {
1066             mTargetScrolling = false;
1067             if (DEBUG_DRAG_DROP) {
1068                 Log.d(TAG, "=== Stop scrolling list");
1069             }
1070             // Stop the scrolling
1071             lv.smoothScrollBy(0, 0);
1072         }
1073     }
1074 
onDragEnded()1075     private void onDragEnded() {
1076         if (mDragInProgress) {
1077             mDragInProgress = false;
1078             // Reenable updates to the view and redraw (in case it changed)
1079             MailboxFragmentAdapter.enableUpdates(true);
1080             mListAdapter.notifyDataSetChanged();
1081             // Stop highlighting targets
1082             updateChildViews();
1083             // Stop any scrolling that was going on
1084             stopScrolling();
1085         }
1086     }
1087 
onDragStarted(DragEvent event)1088     private boolean onDragStarted(DragEvent event) {
1089         // We handle dropping of items with our email mime type
1090         // If the mime type has a mailbox id appended, that is the mailbox of the item
1091         // being draged
1092         ClipDescription description = event.getClipDescription();
1093         int mimeTypeCount = description.getMimeTypeCount();
1094         for (int i = 0; i < mimeTypeCount; i++) {
1095             String mimeType = description.getMimeType(i);
1096             if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) {
1097                 if (DEBUG_DRAG_DROP) {
1098                     Log.d(TAG, "=== Drag started");
1099                 }
1100                 mDragItemMailboxId = -1;
1101                 // See if we find a mailbox id here
1102                 int dash = mimeType.lastIndexOf('-');
1103                 if (dash > 0) {
1104                     try {
1105                         mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1));
1106                     } catch (NumberFormatException e) {
1107                         // Ignore; we just won't know the mailbox
1108                     }
1109                 }
1110                 mDragInProgress = true;
1111                 // Stop the list from updating
1112                 MailboxFragmentAdapter.enableUpdates(false);
1113                 // Update the backgrounds of our child views to highlight drop targets
1114                 updateChildViews();
1115                 return true;
1116             }
1117         }
1118         return false;
1119     }
1120 
1121     /**
1122      * Perform a "drop" action. If the user is not on top of a valid drop target, no action
1123      * is performed.
1124      * @return {@code true} if the drop action was performed. Otherwise {@code false}.
1125      */
onDrop(DragEvent event)1126     private boolean onDrop(DragEvent event) {
1127         stopScrolling();
1128         // If we're not on a target, we're done
1129         if (mDropTargetId == NO_DROP_TARGET) {
1130             return false;
1131         }
1132         final Controller controller = Controller.getInstance(mActivity);
1133         ClipData clipData = event.getClipData();
1134         int count = clipData.getItemCount();
1135         if (DEBUG_DRAG_DROP) {
1136             Log.d(TAG, "=== Dropping " + count + " items.");
1137         }
1138         // Extract the messageId's to move from the ClipData (set up in MessageListItem)
1139         final long[] messageIds = new long[count];
1140         for (int i = 0; i < count; i++) {
1141             Uri uri = clipData.getItemAt(i).getUri();
1142             String msgNum = uri.getPathSegments().get(1);
1143             long id = Long.parseLong(msgNum);
1144             messageIds[i] = id;
1145         }
1146         // Call either deleteMessage or moveMessage, depending on the target
1147         if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) {
1148             controller.deleteMessages(messageIds);
1149         } else {
1150             controller.moveMessages(messageIds, mDropTargetView.mMailboxId);
1151         }
1152         return true;
1153     }
1154 
1155     @Override
onDrag(View view, DragEvent event)1156     public boolean onDrag(View view, DragEvent event) {
1157         boolean result = false;
1158         switch (event.getAction()) {
1159             case DragEvent.ACTION_DRAG_STARTED:
1160                 result = onDragStarted(event);
1161                 break;
1162             case DragEvent.ACTION_DRAG_ENTERED:
1163                 // The drag has entered the ListView window
1164                 if (DEBUG_DRAG_DROP) {
1165                     Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId);
1166                 }
1167                 break;
1168             case DragEvent.ACTION_DRAG_EXITED:
1169                 // The drag has left the building
1170                 if (DEBUG_DRAG_DROP) {
1171                     Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId);
1172                 }
1173                 onDragExited();
1174                 break;
1175             case DragEvent.ACTION_DRAG_ENDED:
1176                 // The drag is over
1177                 if (DEBUG_DRAG_DROP) {
1178                     Log.d(TAG, "=== Drag ended");
1179                 }
1180                 onDragEnded();
1181                 break;
1182             case DragEvent.ACTION_DRAG_LOCATION:
1183                 // We're moving around within our window; handle scroll, if necessary
1184                 onDragLocation(event);
1185                 break;
1186             case DragEvent.ACTION_DROP:
1187                 // The drag item was dropped
1188                 if (DEBUG_DRAG_DROP) {
1189                     Log.d(TAG, "=== Drop");
1190                 }
1191                 result = onDrop(event);
1192                 break;
1193             default:
1194                 break;
1195         }
1196         return result;
1197     }
1198 }
1199