MailboxListFragment.java revision aee368d205679a9b29f9955a48a8f413e42833fc
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import com.android.email.Controller;
20import com.android.email.Email;
21import com.android.email.R;
22import com.android.email.RefreshManager;
23import com.android.email.provider.EmailProvider;
24import com.android.emailcommon.Logging;
25import com.android.emailcommon.provider.EmailContent.Mailbox;
26import com.android.emailcommon.provider.EmailContent.Message;
27import com.android.emailcommon.utility.EmailAsyncTask;
28import com.android.emailcommon.utility.Utility;
29
30import android.app.Activity;
31import android.app.ListFragment;
32import android.app.LoaderManager;
33import android.app.LoaderManager.LoaderCallbacks;
34import android.content.ClipData;
35import android.content.ClipDescription;
36import android.content.Loader;
37import android.content.res.Resources;
38import android.database.Cursor;
39import android.graphics.Rect;
40import android.graphics.drawable.Drawable;
41import android.net.Uri;
42import android.os.Bundle;
43import android.os.Parcelable;
44import android.util.Log;
45import android.view.DragEvent;
46import android.view.LayoutInflater;
47import android.view.View;
48import android.view.View.OnDragListener;
49import android.view.ViewGroup;
50import android.widget.AdapterView;
51import android.widget.AdapterView.OnItemClickListener;
52import android.widget.ListView;
53
54import java.security.InvalidParameterException;
55import java.util.Timer;
56import java.util.TimerTask;
57
58/**
59 * This fragment presents a list of mailboxes for a given account.  The "API" includes the
60 * following elements which must be provided by the host Activity.
61 *
62 *  - call bindActivityInfo() to provide the account ID and set callbacks
63 *  - provide callbacks for onOpen and onRefresh
64 *  - pass-through implementations of onCreateContextMenu() and onContextItemSelected() (temporary)
65 *
66 * TODO Restoring ListView state -- don't do this when changing accounts
67 * TODO Restoring scroll position on screen rotation is broken.
68 * TODO Remove clearContent -> most probably this is the cause for the scroll position not being
69 *      restored issue.
70 */
71public class MailboxListFragment extends ListFragment implements OnItemClickListener,
72        OnDragListener {
73    private static final String TAG = "MailboxListFragment";
74    private static final String BUNDLE_KEY_SELECTED_MAILBOX_ID
75            = "MailboxListFragment.state.selected_mailbox_id";
76    private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState";
77    private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE
78    /** While in drag-n-drop, amount of time before it auto expands; in ms */
79    private static final long AUTO_EXPAND_DELAY = 750L;
80
81    /** No drop target is available where the user is currently hovering over */
82    private static final int NO_DROP_TARGET = -1;
83    // Total height of the top and bottom scroll zones, in pixels
84    private static final int SCROLL_ZONE_SIZE = 64;
85    // The amount of time to scroll by one pixel, in ms
86    private static final int SCROLL_SPEED = 4;
87
88    /** Arbitrary number for use with the loader manager */
89    private static final int MAILBOX_LOADER_ID = 1;
90
91    /** Argument name(s) */
92    private static final String ARG_ACCOUNT_ID = "accountId";
93    private static final String ARG_PARENT_MAILBOX_ID = "parentMailboxId";
94
95    // TODO Clean up usage of mailbox ID. We use both '-1' and '0' to mean "not selected". To
96    // confuse matters, the database uses '-1' for "no mailbox" and '0' for "invalid mailbox".
97    // Once legacy accounts properly support nested folders, we need to make sure we're only
98    // ever using '-1'.
99    // STOPSHIP Change value to '-1' when legacy protocols support folders
100    private final static long DEFAULT_MAILBOX_ID = 0;
101
102    /** Timer to auto-expand folder lists during drag-n-drop */
103    private static final Timer sDragTimer = new Timer();
104    /** Rectangle used for hit testing children */
105    private static final Rect sTouchFrame = new Rect();
106
107    private RefreshManager mRefreshManager;
108
109    // UI Support
110    private Activity mActivity;
111    private MailboxesAdapter mListAdapter;
112    private Callback mCallback = EmptyCallback.INSTANCE;
113
114    private ListView mListView;
115
116    private boolean mResumed;
117
118    // Colors used for drop targets
119    private static Integer sDropTrashColor;
120    private static Drawable sDropActiveDrawable;
121
122    private long mLastLoadedAccountId = -1;
123    private long mAccountId = -1;
124    private long mParentMailboxId = DEFAULT_MAILBOX_ID;
125    private long mSelectedMailboxId = -1;
126    /** The ID of the mailbox that we have been asked to load */
127    private long mLoadedMailboxId = -1;
128
129    private boolean mOpenRequested;
130
131    // True if a drag is currently in progress
132    private boolean mDragInProgress;
133    /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */
134    private long mDragItemMailboxId = -1;
135    /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */
136    private int mDropTargetId = NO_DROP_TARGET;
137    // The mailbox list item view that the user's finger is hovering over
138    private MailboxListItem mDropTargetView;
139    // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized')
140    private int mDragItemHeight = -1;
141    /** Task that actually does the work to auto-expand folder lists during drag-n-drop */
142    private TimerTask mDragTimerTask;
143    // True if we are currently scrolling under the drag item
144    private boolean mTargetScrolling;
145
146    private Parcelable mSavedListState;
147
148    private final MailboxesAdapter.Callback mMailboxesAdapterCallback =
149            new MailboxesAdapter.Callback() {
150        @Override
151        public void onBind(MailboxListItem listItem) {
152            listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
153        }
154    };
155
156    /**
157     * Callback interface that owning activities must implement
158     */
159    public interface Callback {
160        /**
161         * STOPSHIP split this into separate callbacks.
162         * - Drill in to a mailbox and open a mailbox (= show message list) are different operations
163         *   on the phone
164         * - Regular navigation and navigation for D&D are different; the latter case we probably
165         *   want to go back to the original mailbox afterwards.  (Need another callback for this)
166         *
167         * Called when any mailbox (even a combined mailbox) is selected.
168         * @param accountId
169         *          The ID of the account for which a mailbox was selected
170         * @param mailboxId
171         *          The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0],
172         *          or a special mailbox ID
173         *          [e.g. {@link #ROOT_PARENT_MAILBOX_ID} for "All Folders" to open
174         *          the root folders, {@link Mailbox#QUERY_ALL_INBOXES}, etc...].
175         * @param navigate navigate to the mailbox.
176         * @param dragDrop true if D&D is in progress.
177         */
178        public void onMailboxSelected(long accountId, long mailboxId, boolean navigate,
179                boolean dragDrop);
180
181        /** Called when an account is selected on the combined view. */
182        public void onAccountSelected(long accountId);
183
184        /**
185         * Called when the list updates to propagate the current mailbox name and the unread count
186         * for it.
187         *
188         * Note the reason why it's separated from onMailboxSelected is because this needs to be
189         * reported when the unread count changes without changing the current mailbox.
190         */
191        public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount);
192    }
193
194    private static class EmptyCallback implements Callback {
195        public static final Callback INSTANCE = new EmptyCallback();
196        @Override public void onMailboxSelected(long accountId, long mailboxId, boolean navigate,
197                boolean dragDrop) {
198        }
199        @Override public void onAccountSelected(long accountId) { }
200        @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName,
201                int unreadCount) { }
202    }
203
204    /**
205     * Returns the index of the view located at the specified coordinates in the given list.
206     * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned.
207     */
208    private static int pointToIndex(ListView list, int x, int y) {
209        final int count = list.getChildCount();
210        for (int i = count - 1; i >= 0; i--) {
211            final View child = list.getChildAt(i);
212            if (child.getVisibility() == View.VISIBLE) {
213                child.getHitRect(sTouchFrame);
214                if (sTouchFrame.contains(x, y)) {
215                    return i;
216                }
217            }
218        }
219        return NO_DROP_TARGET;
220    }
221
222    /**
223     * Create a new instance with initialization parameters.
224     *
225     * This fragment should be created only with this method.  (Arguments should always be set.)
226     */
227    public static MailboxListFragment newInstance(long accountId, long parentMailboxId) {
228        final MailboxListFragment instance = new MailboxListFragment();
229        final Bundle args = new Bundle();
230        args.putLong(ARG_ACCOUNT_ID, accountId);
231        args.putLong(ARG_PARENT_MAILBOX_ID, parentMailboxId);
232        instance.setArguments(args);
233        return instance;
234    }
235
236    /**
237     * Called to do initial creation of a fragment.  This is called after
238     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
239     */
240    @Override
241    public void onCreate(Bundle savedInstanceState) {
242        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
243            Log.d(Logging.LOG_TAG, "MailboxListFragment onCreate");
244        }
245        super.onCreate(savedInstanceState);
246
247        mActivity = getActivity();
248        mRefreshManager = RefreshManager.getInstance(mActivity);
249        mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback);
250        if (savedInstanceState != null) {
251            restoreInstanceState(savedInstanceState);
252        }
253        if (sDropTrashColor == null) {
254            Resources res = getResources();
255            sDropTrashColor = res.getColor(R.color.mailbox_drop_destructive_bg_color);
256            sDropActiveDrawable = res.getDrawable(R.drawable.list_activated_holo);
257        }
258    }
259
260    @Override
261    public View onCreateView(
262            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
263        return inflater.inflate(R.layout.mailbox_list_fragment, container, false);
264    }
265
266    @Override
267    public void onActivityCreated(Bundle savedInstanceState) {
268        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
269            Log.d(Logging.LOG_TAG, "MailboxListFragment onActivityCreated");
270        }
271        super.onActivityCreated(savedInstanceState);
272
273        mListView = getListView();
274        mListView.setOnItemClickListener(this);
275        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
276        mListView.setOnDragListener(this);
277        registerForContextMenu(mListView);
278
279        final Bundle args = getArguments();
280        // STOPSHIP remove the check.  Right now it's needed for the obsolete phone activities.
281        if (args != null) {
282            openMailboxes(args.getLong(ARG_ACCOUNT_ID), args.getLong(ARG_PARENT_MAILBOX_ID));
283        }
284    }
285
286    public void setCallback(Callback callback) {
287        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
288    }
289
290    private void clearContent() {
291        getLoaderManager().destroyLoader(MAILBOX_LOADER_ID);
292
293        mLastLoadedAccountId = -1;
294        mAccountId = -1;
295        mParentMailboxId = DEFAULT_MAILBOX_ID;
296        mSelectedMailboxId = -1;
297        mLoadedMailboxId = -1;
298
299        mOpenRequested = false;
300        mDropTargetId = NO_DROP_TARGET;
301        mDropTargetView = null;
302
303        if (mListAdapter != null) {
304            mListAdapter.swapCursor(null);
305        }
306        setListShownNoAnimation(false);
307    }
308
309    /**
310     * Opens the top-level mailboxes for the given account ID. If the account is currently
311     * loaded, the list of top-level mailbox will not be reloaded unless <code>forceReload</code>
312     * is <code>true</code>.
313     * @param accountId The ID of the account we want to view
314     * @param parentMailboxId The ID of the parent mailbox.  Use {@link Mailbox#PARENT_KEY_NONE}
315     *     to open the root.
316     * Otherwise, only load the list of top-level mailboxes if the account changes.
317     */
318    // STOPSHIP Make it private once phone activities are gone
319    void openMailboxes(long accountId, long parentMailboxId) {
320        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
321            Log.d(Logging.LOG_TAG, "MailboxListFragment openMailboxes");
322        }
323        if (accountId == -1) {
324            throw new InvalidParameterException();
325        }
326        // Normalize -- STOPSHIP should be removed when DEFAULT_MAILBOX_ID becomes -1.
327        if (parentMailboxId == Mailbox.PARENT_KEY_NONE) {
328            parentMailboxId = DEFAULT_MAILBOX_ID;
329        }
330
331        if ((mAccountId == accountId) && (mParentMailboxId == parentMailboxId)) {
332            return;
333        }
334        clearContent();
335        mOpenRequested = true;
336        mAccountId = accountId;
337        mParentMailboxId = parentMailboxId;
338        if (mResumed) {
339            startLoading();
340        }
341    }
342
343    /**
344     * Returns whether or not the specified mailbox can be navigated to.
345     */
346    private boolean isNavigable(long mailboxId) {
347        final int count = mListView.getCount();
348        for (int i = 0; i < count; i++) {
349            final MailboxListItem item = (MailboxListItem) mListView.getChildAt(i);
350            if (item.mMailboxId != mailboxId) {
351                continue;
352            }
353            return item.isNavigable();
354        }
355        return false;
356    }
357
358    /**
359     * Sets the selected mailbox to the given ID. Sub-folders will not be loaded.
360     * @param mailboxId The ID of the mailbox to select.
361     */
362    public void setSelectedMailbox(long mailboxId) {
363        mSelectedMailboxId = mailboxId;
364        if (mResumed) {
365            highlightSelectedMailbox(true);
366        }
367    }
368
369    /**
370     * Called when the Fragment is visible to the user.
371     */
372    @Override
373    public void onStart() {
374        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
375            Log.d(Logging.LOG_TAG, "MailboxListFragment onStart");
376        }
377        super.onStart();
378    }
379
380    /**
381     * Called when the fragment is visible to the user and actively running.
382     */
383    @Override
384    public void onResume() {
385        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
386            Log.d(Logging.LOG_TAG, "MailboxListFragment onResume");
387        }
388        super.onResume();
389        mResumed = true;
390
391        // If we're recovering from the stopped state, we don't have to reload.
392        // (when mOpenRequested = false)
393        if (mAccountId != -1 && mOpenRequested) {
394            startLoading();
395        }
396    }
397
398    @Override
399    public void onPause() {
400        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
401            Log.d(Logging.LOG_TAG, "MailboxListFragment onPause");
402        }
403        mResumed = false;
404        super.onPause();
405        mSavedListState = getListView().onSaveInstanceState();
406    }
407
408    /**
409     * Called when the Fragment is no longer started.
410     */
411    @Override
412    public void onStop() {
413        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
414            Log.d(Logging.LOG_TAG, "MailboxListFragment onStop");
415        }
416        super.onStop();
417    }
418
419    /**
420     * Called when the fragment is no longer in use.
421     */
422    @Override
423    public void onDestroy() {
424        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
425            Log.d(Logging.LOG_TAG, "MailboxListFragment onDestroy");
426        }
427        super.onDestroy();
428    }
429
430    @Override
431    public void onSaveInstanceState(Bundle outState) {
432        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
433            Log.d(Logging.LOG_TAG, "MailboxListFragment onSaveInstanceState");
434        }
435        super.onSaveInstanceState(outState);
436        outState.putLong(BUNDLE_KEY_SELECTED_MAILBOX_ID, mSelectedMailboxId);
437        outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState());
438    }
439
440    private void restoreInstanceState(Bundle savedInstanceState) {
441        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
442            Log.d(Logging.LOG_TAG, "MailboxListFragment restoreInstanceState");
443        }
444        mSelectedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MAILBOX_ID);
445        mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
446    }
447
448    private void startLoading() {
449        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
450            Log.d(Logging.LOG_TAG, "MailboxListFragment startLoading");
451        }
452        mOpenRequested = false;
453        // Clear the list.  (ListFragment will show the "Loading" animation)
454        setListShown(false);
455
456        // If we've already loaded for a different account OR if we've loaded for a different
457        // mailbox, discard the previous result and load again.
458        boolean saveListState = true;
459        final LoaderManager lm = getLoaderManager();
460        long lastLoadedMailboxId = mLoadedMailboxId;
461        mLoadedMailboxId = mParentMailboxId;
462        if ((lastLoadedMailboxId != mParentMailboxId) ||
463                ((mLastLoadedAccountId != -1) && (mLastLoadedAccountId != mAccountId))) {
464            lm.destroyLoader(MAILBOX_LOADER_ID);
465            saveListState = false;
466            refreshMailboxListIfStale();
467        }
468        /**
469         * Don't use {@link LoaderManager#restartLoader(int, Bundle, LoaderCallbacks)}, because
470         * we want to reuse the previous result if the Loader has been retained.
471         */
472        lm.initLoader(MAILBOX_LOADER_ID, null,
473                new MailboxListLoaderCallbacks(saveListState, mLoadedMailboxId));
474    }
475
476    // TODO This class probably should be made static. There are many calls into the enclosing
477    // class and we need to be cautious about what we call while in these callbacks
478    private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> {
479        private boolean mSaveListState;
480        private final long mMailboxId;
481
482        public MailboxListLoaderCallbacks(boolean saveListState, long mailboxId) {
483            mSaveListState = saveListState;
484            mMailboxId = mailboxId;
485        }
486
487        @Override
488        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
489            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
490                Log.d(Logging.LOG_TAG, "MailboxListFragment onCreateLoader");
491            }
492            return MailboxFragmentAdapter.createLoader(getActivity(), mAccountId, mMailboxId);
493        }
494
495        @Override
496        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
497            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
498                Log.d(Logging.LOG_TAG, "MailboxListFragment onLoadFinished");
499            }
500            if (mMailboxId != mLoadedMailboxId) {
501                return;
502            }
503            mLastLoadedAccountId = mAccountId;
504
505            // Save list view state (primarily scroll position)
506            final ListView lv = getListView();
507            final Parcelable listState;
508            if (!mSaveListState) {
509                listState = null; // Don't preserve list state
510            } else if (mSavedListState != null) {
511                listState = mSavedListState;
512                mSavedListState = null;
513            } else {
514                listState = lv.onSaveInstanceState();
515            }
516
517            if (cursor.getCount() == 0) {
518                // If there's no row, don't set it to the ListView.
519                // Instead use setListShown(false) to make ListFragment show progress icon.
520                mListAdapter.swapCursor(null);
521                setListShown(false);
522            } else {
523                // Set the adapter.
524                mListAdapter.swapCursor(cursor);
525                setListAdapter(mListAdapter);
526                setListShown(true);
527
528                // We want to make selection visible only when account is changing..
529                // i.e. Refresh caused by content changed events shouldn't scroll the list.
530                highlightSelectedMailbox(!mSaveListState);
531            }
532
533            // List has been reloaded; clear any drop target information
534            mDropTargetId = NO_DROP_TARGET;
535            mDropTargetView = null;
536
537            // Restore the state
538            if (listState != null) {
539                lv.onRestoreInstanceState(listState);
540            }
541
542            // Clear this for next reload triggered by content changed events.
543            mSaveListState = true;
544        }
545
546        @Override
547        public void onLoaderReset(Loader<Cursor> loader) {
548            if (mMailboxId != mLoadedMailboxId) {
549                return;
550            }
551            mListAdapter.swapCursor(null);
552        }
553    }
554
555    public void onItemClick(AdapterView<?> parent, View view, int position,
556            long idDontUseIt /* see MailboxesAdapter */ ) {
557        final long id = mListAdapter.getId(position);
558        if (mListAdapter.isAccountRow(position)) {
559            mCallback.onAccountSelected(id);
560        } else {
561            // STOPSHIP On phone, we need a way to open a message list without navigating to the
562            // mailbox.
563            mCallback.onMailboxSelected(mAccountId, id, isNavigable(id), false);
564        }
565    }
566
567    public void onRefresh() {
568        if (mAccountId != -1) {
569            mRefreshManager.refreshMailboxList(mAccountId);
570        }
571    }
572
573    private void refreshMailboxListIfStale() {
574        if (mRefreshManager.isMailboxListStale(mAccountId)) {
575            mRefreshManager.refreshMailboxList(mAccountId);
576        }
577    }
578
579    /**
580     * Highlight the selected mailbox.
581     */
582    private void highlightSelectedMailbox(boolean ensureSelectionVisible) {
583        String mailboxName = "";
584        int unreadCount = 0;
585        if (mSelectedMailboxId == -1) {
586            // No mailbox selected
587            mListView.clearChoices();
588        } else {
589            // TODO Don't mix list view & list adapter indices. This is a recipe for disaster.
590            final int count = mListView.getCount();
591            for (int i = 0; i < count; i++) {
592                if (mListAdapter.getId(i) != mSelectedMailboxId) {
593                    continue;
594                }
595                mListView.setItemChecked(i, true);
596                if (ensureSelectionVisible) {
597                    Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i);
598                }
599                mailboxName = mListAdapter.getDisplayName(mActivity, i);
600                unreadCount = mListAdapter.getUnreadCount(i);
601                break;
602            }
603        }
604        mCallback.onCurrentMailboxUpdated(mSelectedMailboxId, mailboxName, unreadCount);
605    }
606
607    // Drag & Drop handling
608
609    /**
610     * Update all of the list's child views with the proper target background (for now, orange if
611     * a valid target, except red if the trash; standard background otherwise)
612     */
613    private void updateChildViews() {
614        int itemCount = mListView.getChildCount();
615        // Lazily initialize the height of our list items
616        if (itemCount > 0 && mDragItemHeight < 0) {
617            mDragItemHeight = mListView.getChildAt(0).getHeight();
618        }
619        for (int i = 0; i < itemCount; i++) {
620            MailboxListItem item = (MailboxListItem)mListView.getChildAt(i);
621            item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
622        }
623    }
624
625    /**
626     * Starts the timer responsible for auto-selecting mailbox items while in drag-n-drop.
627     * If there is already an active task, we first try to cancel it. There are only two
628     * reasons why a new timer may not be started. First, if we are unable to cancel a
629     * previous timer, we must assume that a new mailbox has already been loaded. Second,
630     * if the target item is not permitted to be auto selected.
631     * @param newTarget The drag target that needs to be auto selected
632     */
633    private void startDragTimer(final MailboxListItem newTarget) {
634        boolean canceledInTime = mDragTimerTask == null || stopDragTimer();
635        if (canceledInTime
636                && newTarget != null
637                && newTarget.isNavigable()
638                && newTarget.isDropTarget(mDragItemMailboxId)) {
639            mDragTimerTask = new TimerTask() {
640                @Override
641                public void run() {
642                    mActivity.runOnUiThread(new Runnable() {
643                        @Override
644                        public void run() {
645                            stopDragTimer();
646                            // STOPSHIP Revisit this -- probably we need a different callback
647                            // so that when D&D finishes we can go back to the original mailbox.
648                            mCallback.onMailboxSelected(mAccountId, newTarget.mMailboxId, true,
649                                    true);
650                        }
651                    });
652                }
653            };
654            sDragTimer.schedule(mDragTimerTask, AUTO_EXPAND_DELAY);
655        }
656    }
657
658    /**
659     * Stops the timer responsible for auto-selecting mailbox items while in drag-n-drop.
660     * If the timer is not active, nothing will happen.
661     * @return Whether or not the timer was interrupted. {@link TimerTask#cancel()}.
662     */
663    private boolean stopDragTimer() {
664        boolean timerInterrupted = false;
665        synchronized (sDragTimer) {
666            if (mDragTimerTask != null) {
667                timerInterrupted = mDragTimerTask.cancel();
668                mDragTimerTask = null;
669            }
670        }
671        return timerInterrupted;
672    }
673
674    /**
675     * Called when the user has dragged outside of the mailbox list area.
676     */
677    private void onDragExited() {
678        // Reset the background of the current target
679        if (mDropTargetView != null) {
680            mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
681            mDropTargetView = null;
682        }
683        mDropTargetId = NO_DROP_TARGET;
684        stopDragTimer();
685        stopScrolling();
686    }
687
688    /**
689     * Called while dragging;  highlight possible drop targets, and auto scroll the list.
690     */
691    private void onDragLocation(DragEvent event) {
692        // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop
693        // if the list is being updated [i.e. navigated to another mailbox]
694        if (mDragItemHeight <= 0) {
695            // This shouldn't be possible, but avoid NPE
696            Log.w(TAG, "drag item height is not set");
697            return;
698        }
699        // Find out which item we're in and highlight as appropriate
700        final int rawTouchX = (int) event.getX();
701        final int rawTouchY = (int) event.getY();
702        final int viewIndex = pointToIndex(mListView, rawTouchX, rawTouchY);
703        int targetId = viewIndex;
704        if (targetId != mDropTargetId) {
705            if (DEBUG_DRAG_DROP) {
706                Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId);
707            }
708            // Remove highlight the current target; if there was one
709            if (mDropTargetView != null) {
710                mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId);
711                mDropTargetView = null;
712            }
713            // Get the new target mailbox view
714            final MailboxListItem newTarget = (MailboxListItem) mListView.getChildAt(viewIndex);
715            if (newTarget == null) {
716                // In any event, we're no longer dragging in the list view if newTarget is null
717                if (DEBUG_DRAG_DROP) {
718                    Log.d(TAG, "=== Drag off the list");
719                }
720                final int childCount = mListView.getChildCount();
721                if (viewIndex >= childCount) {
722                    // Touching beyond the end of the list; may happen for small lists
723                    onDragExited();
724                    return;
725                } else {
726                    // We should never get here
727                    Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount);
728                }
729            } else if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) {
730                if (DEBUG_DRAG_DROP) {
731                    Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId);
732                }
733                newTarget.setBackgroundColor(sDropTrashColor);
734            } else if (newTarget.isDropTarget(mDragItemMailboxId)) {
735                if (DEBUG_DRAG_DROP) {
736                    Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId);
737                }
738                newTarget.setBackgroundDrawable(sDropActiveDrawable);
739            } else {
740                if (DEBUG_DRAG_DROP) {
741                    Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId);
742                }
743                newTarget.setDropTargetBackground(true, mDragItemMailboxId);
744                targetId = NO_DROP_TARGET;
745            }
746            // Save away our current position and view
747            mDropTargetId = targetId;
748            mDropTargetView = newTarget;
749            startDragTimer(newTarget);
750        }
751
752        // This is a quick-and-dirty implementation of drag-under-scroll; something like this
753        // should eventually find its way into the framework
754        int scrollDiff = rawTouchY - (mListView.getHeight() - SCROLL_ZONE_SIZE);
755        boolean scrollDown = (scrollDiff > 0);
756        boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY);
757        if (!mTargetScrolling && scrollDown) {
758            int itemsToScroll = mListView.getCount() - mListView.getLastVisiblePosition();
759            int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight;
760            mListView.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
761            if (DEBUG_DRAG_DROP) {
762                Log.d(TAG, "=== Start scrolling list down");
763            }
764            mTargetScrolling = true;
765        } else if (!mTargetScrolling && scrollUp) {
766            int pixelsToScroll = (mListView.getFirstVisiblePosition() + 1) * mDragItemHeight;
767            mListView.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
768            if (DEBUG_DRAG_DROP) {
769                Log.d(TAG, "=== Start scrolling list up");
770            }
771            mTargetScrolling = true;
772        } else if (!scrollUp && !scrollDown) {
773            stopScrolling();
774        }
775    }
776
777    /**
778     * Indicate that scrolling has stopped
779     */
780    private void stopScrolling() {
781        if (mTargetScrolling) {
782            mTargetScrolling = false;
783            if (DEBUG_DRAG_DROP) {
784                Log.d(TAG, "=== Stop scrolling list");
785            }
786            // Stop the scrolling
787            mListView.smoothScrollBy(0, 0);
788        }
789    }
790
791    private void onDragEnded() {
792        stopDragTimer();
793        if (mDragInProgress) {
794            mDragInProgress = false;
795            // Reenable updates to the view and redraw (in case it changed)
796            MailboxesAdapter.enableUpdates(true);
797            mListAdapter.notifyDataSetChanged();
798            // Stop highlighting targets
799            updateChildViews();
800            // Stop any scrolling that was going on
801            stopScrolling();
802        }
803    }
804
805    private boolean onDragStarted(DragEvent event) {
806        // We handle dropping of items with our email mime type
807        // If the mime type has a mailbox id appended, that is the mailbox of the item
808        // being draged
809        ClipDescription description = event.getClipDescription();
810        int mimeTypeCount = description.getMimeTypeCount();
811        for (int i = 0; i < mimeTypeCount; i++) {
812            String mimeType = description.getMimeType(i);
813            if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) {
814                if (DEBUG_DRAG_DROP) {
815                    Log.d(TAG, "=== Drag started");
816                }
817                mDragItemMailboxId = -1;
818                // See if we find a mailbox id here
819                int dash = mimeType.lastIndexOf('-');
820                if (dash > 0) {
821                    try {
822                        mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1));
823                    } catch (NumberFormatException e) {
824                        // Ignore; we just won't know the mailbox
825                    }
826                }
827                mDragInProgress = true;
828                // Stop the list from updating
829                MailboxesAdapter.enableUpdates(false);
830                // Update the backgrounds of our child views to highlight drop targets
831                updateChildViews();
832                return true;
833            }
834        }
835        return false;
836    }
837
838    /**
839     * Perform a "drop" action. If the user is not on top of a valid drop target, no action
840     * is performed.
841     * @return {@code true} if the drop action was performed. Otherwise {@code false}.
842     */
843    private boolean onDrop(DragEvent event) {
844        stopDragTimer();
845        stopScrolling();
846        // If we're not on a target, we're done
847        if (mDropTargetId == NO_DROP_TARGET) {
848            return false;
849        }
850        final Controller controller = Controller.getInstance(mActivity);
851        ClipData clipData = event.getClipData();
852        int count = clipData.getItemCount();
853        if (DEBUG_DRAG_DROP) {
854            Log.d(TAG, "=== Dropping " + count + " items.");
855        }
856        // Extract the messageId's to move from the ClipData (set up in MessageListItem)
857        final long[] messageIds = new long[count];
858        for (int i = 0; i < count; i++) {
859            Uri uri = clipData.getItemAt(i).getUri();
860            String msgNum = uri.getPathSegments().get(1);
861            long id = Long.parseLong(msgNum);
862            messageIds[i] = id;
863        }
864        final MailboxListItem targetItem = mDropTargetView;
865        // Call either deleteMessage or moveMessage, depending on the target
866        EmailAsyncTask.runAsyncSerial(new Runnable() {
867            @Override
868            public void run() {
869                if (targetItem.mMailboxType == Mailbox.TYPE_TRASH) {
870                    for (long messageId: messageIds) {
871                        // TODO Get this off UI thread (put in clip)
872                        Message msg = Message.restoreMessageWithId(mActivity, messageId);
873                        if (msg != null) {
874                            controller.deleteMessage(messageId, msg.mAccountKey);
875                        }
876                    }
877                } else {
878                    controller.moveMessage(messageIds, targetItem.mMailboxId);
879                }
880            }
881        });
882        return true;
883    }
884
885    @Override
886    public boolean onDrag(View view, DragEvent event) {
887        boolean result = false;
888        switch (event.getAction()) {
889            case DragEvent.ACTION_DRAG_STARTED:
890                result = onDragStarted(event);
891                break;
892            case DragEvent.ACTION_DRAG_ENTERED:
893                // The drag has entered the ListView window
894                if (DEBUG_DRAG_DROP) {
895                    Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId);
896                }
897                break;
898            case DragEvent.ACTION_DRAG_EXITED:
899                // The drag has left the building
900                if (DEBUG_DRAG_DROP) {
901                    Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId);
902                }
903                onDragExited();
904                break;
905            case DragEvent.ACTION_DRAG_ENDED:
906                // The drag is over
907                if (DEBUG_DRAG_DROP) {
908                    Log.d(TAG, "=== Drag ended");
909                }
910                onDragEnded();
911                break;
912            case DragEvent.ACTION_DRAG_LOCATION:
913                // We're moving around within our window; handle scroll, if necessary
914                onDragLocation(event);
915                break;
916            case DragEvent.ACTION_DROP:
917                // The drag item was dropped
918                if (DEBUG_DRAG_DROP) {
919                    Log.d(TAG, "=== Drop");
920                }
921                result = onDrop(event);
922                break;
923            default:
924                break;
925        }
926        return result;
927    }
928}
929