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