MailboxListFragment.java revision f7036b737907f06df1a06507754b28596cf8225e
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.drawable.Drawable;
40import android.net.Uri;
41import android.os.Bundle;
42import android.util.Log;
43import android.view.DragEvent;
44import android.view.LayoutInflater;
45import android.view.View;
46import android.view.View.OnDragListener;
47import android.view.ViewGroup;
48import android.widget.AdapterView;
49import android.widget.ListView;
50import android.widget.AdapterView.OnItemClickListener;
51
52import java.security.InvalidParameterException;
53
54/**
55 * This fragment presents a list of mailboxes for a given account.  The "API" includes the
56 * following elements which must be provided by the host Activity.
57 *
58 *  - call bindActivityInfo() to provide the account ID and set callbacks
59 *  - provide callbacks for onOpen and onRefresh
60 *  - pass-through implementations of onCreateContextMenu() and onContextItemSelected() (temporary)
61 *
62 * TODO Restoring ListView state -- don't do this when changing accounts
63 */
64public class MailboxListFragment extends ListFragment implements OnItemClickListener,
65        OnDragListener {
66    private static final String TAG = "MailboxListFragment";
67    private static final String BUNDLE_KEY_SELECTED_MAILBOX_ID
68            = "MailboxListFragment.state.selected_mailbox_id";
69    private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState";
70    private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE
71
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    /** Arbitrary number for use with the loader manager */
78    private static final int MAILBOX_LOADER_ID = 1;
79
80    // TODO Clean up usage of mailbox ID. We use both '-1' and '0' to mean "not selected". To
81    // confuse matters, the database uses '-1' for "no mailbox" and '0' for "invalid mailbox".
82    // Once legacy accounts properly support nested folders, we need to make sure we're only
83    // ever using '-1'.
84    // STOPSHIP Change value to '-1' when legacy protocols support folders
85    private final static long DEFAULT_MAILBOX_ID = 0;
86
87    private RefreshManager mRefreshManager;
88
89    // UI Support
90    private Activity mActivity;
91    private MailboxesAdapter mListAdapter;
92    private Callback mCallback = EmptyCallback.INSTANCE;
93
94    private ListView mListView;
95
96    private boolean mResumed;
97
98    // Colors used for drop targets
99    private static Integer sDropTrashColor;
100    private static Drawable sDropActiveDrawable;
101
102    private long mLastLoadedAccountId = -1;
103    private long mAccountId = -1;
104    private long mSelectedMailboxId = DEFAULT_MAILBOX_ID;
105    /** The ID of the mailbox that we have been asked to load */
106    private long mLoadedMailboxId = -1;
107
108    private boolean mOpenRequested;
109
110    // True if a drag is currently in progress
111    private boolean mDragInProgress = false;
112    // The mailbox id of the dragged item's mailbox.  We use it to prevent that box from being a
113    // valid drop target
114    private long mDragItemMailboxId = -1;
115    // The adapter position that the user's finger is hovering over
116    private int mDropTargetAdapterPosition = 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    // True if we are currently scrolling under the drag item
122    private boolean mTargetScrolling;
123
124    private Utility.ListStateSaver mSavedListState;
125
126    private MailboxesAdapter.Callback mMailboxesAdapterCallback = 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         * @param accountId
140         *          The ID of the account for which a mailbox was selected
141         * @param mailboxId
142         *          The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0],
143         *          or a special mailbox ID [e.g. {@link MessageListXLFragmentManager#NO_MAILBOX},
144         *          {@link Mailbox#QUERY_ALL_INBOXES}, etc...].
145         */
146        public void onMailboxSelected(long accountId, long mailboxId);
147
148        /** Called when an account is selected on the combined view. */
149        public void onAccountSelected(long accountId);
150
151        /**
152         * Called when the list updates to propagate the current mailbox name and the unread count
153         * for it.
154         *
155         * Note the reason why it's separated from onMailboxSelected is because this needs to be
156         * reported when the unread count changes without changing the current mailbox.
157         */
158        public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount);
159    }
160
161    private static class EmptyCallback implements Callback {
162        public static final Callback INSTANCE = new EmptyCallback();
163        @Override public void onMailboxSelected(long accountId, long mailboxId) { }
164        @Override public void onAccountSelected(long accountId) { }
165        @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName,
166                int unreadCount) { }
167    }
168
169    /**
170     * Called to do initial creation of a fragment.  This is called after
171     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
172     */
173    @Override
174    public void onCreate(Bundle savedInstanceState) {
175        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
176            Log.d(Logging.LOG_TAG, "MailboxListFragment onCreate");
177        }
178        super.onCreate(savedInstanceState);
179
180        mActivity = getActivity();
181        mRefreshManager = RefreshManager.getInstance(mActivity);
182        mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback);
183        if (savedInstanceState != null) {
184            restoreInstanceState(savedInstanceState);
185        }
186        if (sDropTrashColor == null) {
187            Resources res = getResources();
188            sDropTrashColor = res.getColor(R.color.mailbox_drop_destructive_bg_color);
189            sDropActiveDrawable = res.getDrawable(R.drawable.list_activated_holo);
190        }
191    }
192
193    @Override
194    public View onCreateView(
195            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
196        return inflater.inflate(R.layout.mailbox_list_fragment, container, false);
197    }
198
199    @Override
200    public void onActivityCreated(Bundle savedInstanceState) {
201        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
202            Log.d(Logging.LOG_TAG, "MailboxListFragment onActivityCreated");
203        }
204        super.onActivityCreated(savedInstanceState);
205
206        mListView = getListView();
207        mListView.setOnItemClickListener(this);
208        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
209        mListView.setOnDragListener(this);
210        registerForContextMenu(mListView);
211    }
212
213    public void setCallback(Callback callback) {
214        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
215    }
216
217    private void clearContent() {
218        getLoaderManager().destroyLoader(MAILBOX_LOADER_ID);
219
220        mLastLoadedAccountId = -1;
221        mAccountId = -1;
222        mSelectedMailboxId = DEFAULT_MAILBOX_ID;
223        mLoadedMailboxId = -1;
224
225        mOpenRequested = false;
226        mDragInProgress = false;
227
228        if (mListAdapter != null) {
229            mListAdapter.swapCursor(null);
230        }
231        setListShownNoAnimation(false);
232    }
233
234    /**
235     * Opens the top-level mailboxes for the given account ID. If the account is currently
236     * loaded, no actions will be performed. To forcefully load the list of top-level
237     * mailboxes use {@link #openMailboxes(long, boolean)}
238     * @param accountId The ID of the account we want to view
239     */
240    public void openMailboxes(long accountId) {
241        openMailboxes(accountId, false);
242    }
243
244    /**
245     * Opens the top-level mailboxes for the given account ID. If the account is currently
246     * loaded, the list of top-level mailbox will not be reloaded unless <code>forceReload</code>
247     * is <code>true</code>.
248     * @param accountId The ID of the account we want to view
249     * @param forceReload If <code>true</code>, always load the list of top-level mailboxes.
250     * Otherwise, only load the list of top-level mailboxes if the account changes.
251     */
252    public void openMailboxes(long accountId, boolean forceReload) {
253        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
254            Log.d(Logging.LOG_TAG, "MailboxListFragment openMailboxes");
255        }
256        if (accountId == -1) {
257            throw new InvalidParameterException();
258        }
259        if (!forceReload && mAccountId == accountId) {
260            return;
261        }
262        clearContent();
263        mOpenRequested = true;
264        mAccountId = accountId;
265        if (mResumed) {
266            startLoading();
267        }
268    }
269
270    /**
271     * Selects the given mailbox ID and possibly navigates to it. This loads any mailboxes
272     * contained within it and may cause the mailbox list to be updated. If the current fragment
273     * is not in the resumed state or if the mailbox cannot be navigated to, the given mailbox
274     * will only be selected. The mailbox is assumed to be associated with the account passed
275     * into {@link #openMailboxes(long)}.
276     * @param mailboxId The ID of the mailbox to select and navigate to.
277     */
278    public void navigateToMailbox(long mailboxId) {
279        setSelectedMailbox(mailboxId);
280        if (mResumed && isNavigable(mailboxId)) {
281            startLoading();
282        }
283    }
284
285    /**
286     * Returns whether or not the specified mailbox can be navigated to.
287     */
288    private boolean isNavigable(long mailboxId) {
289        final int count = mListAdapter.getCount();
290        for (int i = 0; i < count; i++) {
291            if (mListAdapter.getId(i) != mSelectedMailboxId) {
292                continue;
293            }
294            return mListAdapter.isNavigable(i);
295        }
296        return false;
297    }
298
299    /**
300     * Sets the selected mailbox to the given ID. Sub-folders will not be loaded.
301     * @param mailboxId The ID of the mailbox to select.
302     */
303    public void setSelectedMailbox(long mailboxId) {
304        mSelectedMailboxId = mailboxId;
305        if (mResumed) {
306            highlightSelectedMailbox(true);
307        }
308    }
309
310    /**
311     * Called when the Fragment is visible to the user.
312     */
313    @Override
314    public void onStart() {
315        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
316            Log.d(Logging.LOG_TAG, "MailboxListFragment onStart");
317        }
318        super.onStart();
319    }
320
321    /**
322     * Called when the fragment is visible to the user and actively running.
323     */
324    @Override
325    public void onResume() {
326        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
327            Log.d(Logging.LOG_TAG, "MailboxListFragment onResume");
328        }
329        super.onResume();
330        mResumed = true;
331
332        // If we're recovering from the stopped state, we don't have to reload.
333        // (when mOpenRequested = false)
334        if (mAccountId != -1 && mOpenRequested) {
335            startLoading();
336        }
337    }
338
339    @Override
340    public void onPause() {
341        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
342            Log.d(Logging.LOG_TAG, "MailboxListFragment onPause");
343        }
344        mResumed = false;
345        super.onPause();
346        mSavedListState = new Utility.ListStateSaver(getListView());
347    }
348
349    /**
350     * Called when the Fragment is no longer started.
351     */
352    @Override
353    public void onStop() {
354        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
355            Log.d(Logging.LOG_TAG, "MailboxListFragment onStop");
356        }
357        super.onStop();
358    }
359
360    /**
361     * Called when the fragment is no longer in use.
362     */
363    @Override
364    public void onDestroy() {
365        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
366            Log.d(Logging.LOG_TAG, "MailboxListFragment onDestroy");
367        }
368        super.onDestroy();
369    }
370
371    @Override
372    public void onSaveInstanceState(Bundle outState) {
373        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
374            Log.d(Logging.LOG_TAG, "MailboxListFragment onSaveInstanceState");
375        }
376        super.onSaveInstanceState(outState);
377        outState.putLong(BUNDLE_KEY_SELECTED_MAILBOX_ID, mSelectedMailboxId);
378        outState.putParcelable(BUNDLE_LIST_STATE, new Utility.ListStateSaver(getListView()));
379    }
380
381    private void restoreInstanceState(Bundle savedInstanceState) {
382        mSelectedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MAILBOX_ID);
383        mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
384    }
385
386    private void startLoading() {
387        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
388            Log.d(Logging.LOG_TAG, "MailboxListFragment startLoading");
389        }
390        mOpenRequested = false;
391        // Clear the list.  (ListFragment will show the "Loading" animation)
392        setListShown(false);
393
394        // If we've already loaded for a different account OR if we've loaded for a different
395        // mailbox, discard the previous result and load again.
396        boolean saveListState = true;
397        final LoaderManager lm = getLoaderManager();
398        long lastLoadedMailboxId = mLoadedMailboxId;
399        mLoadedMailboxId = mSelectedMailboxId;
400        if ((lastLoadedMailboxId != mSelectedMailboxId) ||
401                ((mLastLoadedAccountId != -1) && (mLastLoadedAccountId != mAccountId))) {
402            lm.destroyLoader(MAILBOX_LOADER_ID);
403            saveListState = false;
404            refreshMailboxListIfStale();
405        }
406        /**
407         * Don't use {@link LoaderManager#restartLoader(int, Bundle, LoaderCallbacks)}, because
408         * we want to reuse the previous result if the Loader has been retained.
409         */
410        lm.initLoader(MAILBOX_LOADER_ID, null,
411                new MailboxListLoaderCallbacks(saveListState, mLoadedMailboxId));
412    }
413
414    // TODO This class probably should be made static. There are many calls into the enclosing
415    // class and we need to be cautious about what we call while in these callbacks
416    private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> {
417        private boolean mSaveListState;
418        private final long mMailboxId;
419
420        public MailboxListLoaderCallbacks(boolean saveListState, long mailboxId) {
421            mSaveListState = saveListState;
422            mMailboxId = mailboxId;
423        }
424
425        @Override
426        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
427            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
428                Log.d(Logging.LOG_TAG, "MailboxListFragment onCreateLoader");
429            }
430            return MailboxFragmentAdapter.createLoader(getActivity(), mAccountId, mMailboxId);
431        }
432
433        @Override
434        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
435            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
436                Log.d(Logging.LOG_TAG, "MailboxListFragment onLoadFinished");
437            }
438            if (mMailboxId != mLoadedMailboxId) {
439                return;
440            }
441            mLastLoadedAccountId = mAccountId;
442
443            // Save list view state (primarily scroll position)
444            final ListView lv = getListView();
445            final Utility.ListStateSaver lss;
446            if (!mSaveListState) {
447                lss = null; // Don't preserve list state
448            } else if (mSavedListState != null) {
449                lss = mSavedListState;
450                mSavedListState = null;
451            } else {
452                lss = new Utility.ListStateSaver(lv);
453            }
454
455            if (cursor.getCount() == 0) {
456                // If there's no row, don't set it to the ListView.
457                // Instead use setListShown(false) to make ListFragment show progress icon.
458                mListAdapter.swapCursor(null);
459                setListShown(false);
460            } else {
461                // Set the adapter.
462                mListAdapter.swapCursor(cursor);
463                setListAdapter(mListAdapter);
464                setListShown(true);
465
466                // We want to make selection visible only when account is changing..
467                // i.e. Refresh caused by content changed events shouldn't scroll the list.
468                highlightSelectedMailbox(!mSaveListState);
469            }
470
471            // Restore the state
472            if (lss != null) {
473                lss.restore(lv);
474            }
475
476            // Clear this for next reload triggered by content changed events.
477            mSaveListState = true;
478        }
479
480        @Override
481        public void onLoaderReset(Loader<Cursor> loader) {
482            if (mMailboxId != mLoadedMailboxId) {
483                return;
484            }
485            mListAdapter.swapCursor(null);
486        }
487    }
488
489    public void onItemClick(AdapterView<?> parent, View view, int position,
490            long idDontUseIt /* see MailboxesAdapter */ ) {
491        final long id = mListAdapter.getId(position);
492        if (mListAdapter.isAccountRow(position)) {
493            mCallback.onAccountSelected(id);
494        } else {
495            mCallback.onMailboxSelected(mAccountId, id);
496        }
497    }
498
499    public void onRefresh() {
500        if (mAccountId != -1) {
501            mRefreshManager.refreshMailboxList(mAccountId);
502        }
503    }
504
505    private void refreshMailboxListIfStale() {
506        if (mRefreshManager.isMailboxListStale(mAccountId)) {
507            mRefreshManager.refreshMailboxList(mAccountId);
508        }
509    }
510
511    /**
512     * Highlight the selected mailbox.
513     */
514    private void highlightSelectedMailbox(boolean ensureSelectionVisible) {
515        String mailboxName = "";
516        int unreadCount = 0;
517        if (mSelectedMailboxId == DEFAULT_MAILBOX_ID) {
518            // No mailbox selected
519            mListView.clearChoices();
520        } else {
521            // TODO Don't mix list view & list adapter indices. This is a recipe for disaster.
522            final int count = mListView.getCount();
523            for (int i = 0; i < count; i++) {
524                if (mListAdapter.getId(i) != mSelectedMailboxId) {
525                    continue;
526                }
527                mListView.setItemChecked(i, true);
528                if (ensureSelectionVisible) {
529                    Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i);
530                }
531                mailboxName = mListAdapter.getDisplayName(mActivity, i);
532                unreadCount = mListAdapter.getUnreadCount(i);
533                break;
534            }
535        }
536        mCallback.onCurrentMailboxUpdated(mSelectedMailboxId, mailboxName, unreadCount);
537    }
538
539    // Drag & Drop handling
540
541    /**
542     * Update all of the list's child views with the proper target background (for now, orange if
543     * a valid target, except red if the trash; standard background otherwise)
544     */
545    private void updateChildViews() {
546        int itemCount = mListView.getChildCount();
547        // Lazily initialize the height of our list items
548        if (itemCount > 0 && mDragItemHeight < 0) {
549            mDragItemHeight = mListView.getChildAt(0).getHeight();
550        }
551        for (int i = 0; i < itemCount; i++) {
552            MailboxListItem item = (MailboxListItem)mListView.getChildAt(i);
553            item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
554        }
555    }
556
557    /**
558     * Called when our ListView gets a DRAG_EXITED event
559     */
560    private void onDragExited() {
561        // Reset the background of the current target
562        if (mDropTargetAdapterPosition != NO_DROP_TARGET) {
563            mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
564            mDropTargetAdapterPosition = NO_DROP_TARGET;
565        }
566        stopScrolling();
567    }
568
569    /**
570     * Called while dragging;  highlight possible drop targets, and autoscroll the list.
571     */
572    private void onDragLocation(DragEvent event) {
573        // The drag is somewhere in the ListView
574        if (mDragItemHeight <= 0) {
575            // This shouldn't be possible, but avoid NPE
576            return;
577        }
578        // Find out which item we're in and highlight as appropriate
579        int rawTouchY = (int)event.getY();
580        int offset = 0;
581        if (mListView.getCount() > 0) {
582            offset = mListView.getChildAt(0).getTop();
583        }
584        int targetScreenPosition = (rawTouchY - offset) / mDragItemHeight;
585        int firstVisibleItem = mListView.getFirstVisiblePosition();
586        int targetAdapterPosition = firstVisibleItem + targetScreenPosition;
587        if (targetAdapterPosition != mDropTargetAdapterPosition) {
588            if (DEBUG_DRAG_DROP) {
589                Log.d(TAG, "========== DROP TARGET " + mDropTargetAdapterPosition + " -> " +
590                        targetAdapterPosition);
591            }
592            // Unhighlight the current target, if we've got one
593            if (mDropTargetAdapterPosition != NO_DROP_TARGET) {
594                mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId);
595            }
596            // Get the new target mailbox view
597            MailboxListItem newTarget =
598                (MailboxListItem)mListView.getChildAt(targetScreenPosition);
599            // This can be null due to a bug in the framework (checking on that)
600            // In any event, we're no longer dragging in the list view if newTarget is null
601            if (newTarget == null) {
602                if (DEBUG_DRAG_DROP) {
603                    Log.d(TAG, "========== WTF??? DRAG EXITED");
604                }
605                onDragExited();
606                return;
607            } else if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) {
608                if (DEBUG_DRAG_DROP) {
609                    Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " TRASH");
610                }
611                newTarget.setBackgroundColor(sDropTrashColor);
612            } else if (newTarget.isDropTarget(mDragItemMailboxId)) {
613                if (DEBUG_DRAG_DROP) {
614                    Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " TARGET");
615                }
616                newTarget.setBackgroundDrawable(sDropActiveDrawable);
617            } else {
618                if (DEBUG_DRAG_DROP) {
619                    Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " (CALL)");
620                }
621                targetAdapterPosition = NO_DROP_TARGET;
622                newTarget.setDropTargetBackground(true, mDragItemMailboxId);
623            }
624            // Save away our current position and view
625            mDropTargetAdapterPosition = targetAdapterPosition;
626            mDropTargetView = newTarget;
627        }
628
629        // This is a quick-and-dirty implementation of drag-under-scroll; something like this
630        // should eventually find its way into the framework
631        int scrollDiff = rawTouchY - (mListView.getHeight() - SCROLL_ZONE_SIZE);
632        boolean scrollDown = (scrollDiff > 0);
633        boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY);
634        if (!mTargetScrolling && scrollDown) {
635            int itemsToScroll = mListView.getCount() - targetAdapterPosition;
636            int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight;
637            mListView.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
638            if (DEBUG_DRAG_DROP) {
639                Log.d(TAG, "========== START TARGET SCROLLING DOWN");
640            }
641            mTargetScrolling = true;
642        } else if (!mTargetScrolling && scrollUp) {
643            int pixelsToScroll = (firstVisibleItem + 1) * mDragItemHeight;
644            mListView.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
645            if (DEBUG_DRAG_DROP) {
646                Log.d(TAG, "========== START TARGET SCROLLING UP");
647            }
648            mTargetScrolling = true;
649        } else if (!scrollUp && !scrollDown) {
650            stopScrolling();
651        }
652    }
653
654    /**
655     * Indicate that scrolling has stopped
656     */
657    private void stopScrolling() {
658        if (mTargetScrolling) {
659            mTargetScrolling = false;
660            if (DEBUG_DRAG_DROP) {
661                Log.d(TAG, "========== STOP TARGET SCROLLING");
662            }
663            // Stop the scrolling
664            mListView.smoothScrollBy(0, 0);
665        }
666    }
667
668    private void onDragEnded() {
669        if (mDragInProgress) {
670            mDragInProgress = false;
671            // Reenable updates to the view and redraw (in case it changed)
672            MailboxesAdapter.enableUpdates(true);
673            mListAdapter.notifyDataSetChanged();
674            // Stop highlighting targets
675            updateChildViews();
676            // Stop any scrolling that was going on
677            stopScrolling();
678        }
679    }
680
681    private boolean onDragStarted(DragEvent event) {
682        // We handle dropping of items with our email mime type
683        // If the mime type has a mailbox id appended, that is the mailbox of the item
684        // being draged
685        ClipDescription description = event.getClipDescription();
686        int mimeTypeCount = description.getMimeTypeCount();
687        for (int i = 0; i < mimeTypeCount; i++) {
688            String mimeType = description.getMimeType(i);
689            if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) {
690                if (DEBUG_DRAG_DROP) {
691                    Log.d(TAG, "========== DRAG STARTED");
692                }
693                mDragItemMailboxId = -1;
694                // See if we find a mailbox id here
695                int dash = mimeType.lastIndexOf('-');
696                if (dash > 0) {
697                    try {
698                        mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1));
699                    } catch (NumberFormatException e) {
700                        // Ignore; we just won't know the mailbox
701                    }
702                }
703                mDragInProgress = true;
704                // Stop the list from updating
705                MailboxesAdapter.enableUpdates(false);
706                // Update the backgrounds of our child views to highlight drop targets
707                updateChildViews();
708                return true;
709            }
710        }
711        return false;
712    }
713
714    private boolean onDrop(DragEvent event) {
715        stopScrolling();
716        // If we're not on a target, we're done
717        if (mDropTargetAdapterPosition == NO_DROP_TARGET) return false;
718        final Controller controller = Controller.getInstance(mActivity);
719        ClipData clipData = event.getClipData();
720        int count = clipData.getItemCount();
721        if (DEBUG_DRAG_DROP) {
722            Log.d(TAG, "Received a drop of " + count + " items.");
723        }
724        // Extract the messageId's to move from the ClipData (set up in MessageListItem)
725        final long[] messageIds = new long[count];
726        for (int i = 0; i < count; i++) {
727            Uri uri = clipData.getItemAt(i).getUri();
728            String msgNum = uri.getPathSegments().get(1);
729            long id = Long.parseLong(msgNum);
730            messageIds[i] = id;
731        }
732        // Call either deleteMessage or moveMessage, depending on the target
733        EmailAsyncTask.runAsyncSerial(new Runnable() {
734            @Override
735            public void run() {
736                if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) {
737                    for (long messageId: messageIds) {
738                        // TODO Get this off UI thread (put in clip)
739                        Message msg = Message.restoreMessageWithId(mActivity, messageId);
740                        if (msg != null) {
741                            controller.deleteMessage(messageId, msg.mAccountKey);
742                        }
743                    }
744                } else {
745                    controller.moveMessage(messageIds, mDropTargetView.mMailboxId);
746                }
747            }
748        });
749        return true;
750    }
751
752    @Override
753    public boolean onDrag(View view, DragEvent event) {
754        boolean result = false;
755        switch (event.getAction()) {
756            case DragEvent.ACTION_DRAG_STARTED:
757                result = onDragStarted(event);
758                break;
759            case DragEvent.ACTION_DRAG_ENTERED:
760                // The drag has entered the ListView window
761                if (DEBUG_DRAG_DROP) {
762                    Log.d(TAG, "========== DRAG ENTERED (target = " + mDropTargetAdapterPosition +
763                    ")");
764                }
765                break;
766            case DragEvent.ACTION_DRAG_EXITED:
767                // The drag has left the building
768                if (DEBUG_DRAG_DROP) {
769                    Log.d(TAG, "========== DRAG EXITED (target = " + mDropTargetAdapterPosition +
770                            ")");
771                }
772                onDragExited();
773                break;
774            case DragEvent.ACTION_DRAG_ENDED:
775                // The drag is over
776                if (DEBUG_DRAG_DROP) {
777                    Log.d(TAG, "========== DRAG ENDED");
778                }
779                onDragEnded();
780                break;
781            case DragEvent.ACTION_DRAG_LOCATION:
782                // We're moving around within our window; handle scroll, if necessary
783                onDragLocation(event);
784                break;
785            case DragEvent.ACTION_DROP:
786                // The drag item was dropped
787                if (DEBUG_DRAG_DROP) {
788                    Log.d(TAG, "========== DROP");
789                }
790                result = onDrop(event);
791                break;
792            default:
793                break;
794        }
795        return result;
796    }
797}
798