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