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