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