MailboxListFragment.java revision 0698f4027f099a017ad7815f16572ba422d3413d
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.Utility;
24import com.android.email.provider.EmailProvider;
25import com.android.email.provider.EmailContent.Mailbox;
26import com.android.email.provider.EmailContent.Message;
27
28import android.app.Activity;
29import android.app.ListFragment;
30import android.app.LoaderManager.LoaderCallbacks;
31import android.content.ClipData;
32import android.content.ClipDescription;
33import android.content.Loader;
34import android.database.Cursor;
35import android.net.Uri;
36import android.os.Bundle;
37import android.util.Log;
38import android.view.DragEvent;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.View.OnDragListener;
42import android.view.ViewGroup;
43import android.widget.AdapterView;
44import android.widget.ListView;
45import android.widget.AdapterView.OnItemClickListener;
46
47import java.security.InvalidParameterException;
48
49/**
50 * This fragment presents a list of mailboxes for a given account.  The "API" includes the
51 * following elements which must be provided by the host Activity.
52 *
53 *  - call bindActivityInfo() to provide the account ID and set callbacks
54 *  - provide callbacks for onOpen and onRefresh
55 *  - pass-through implementations of onCreateContextMenu() and onContextItemSelected() (temporary)
56 *
57 * TODO Restoring ListView state -- don't do this when changing accounts
58 */
59public class MailboxListFragment extends ListFragment implements OnItemClickListener,
60        OnDragListener {
61    private static final String TAG = "MailboxListFragment";
62    private static final String BUNDLE_KEY_SELECTED_MAILBOX_ID
63            = "MailboxListFragment.state.selected_mailbox_id";
64    private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState";
65    private static final int LOADER_ID_MAILBOX_LIST = 1;
66    private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE
67
68    private static final int NO_DROP_TARGET = -1;
69    // Top and bottom scroll zone size, in pixels
70    private static final int SCROLL_ZONE_SIZE = 64;
71
72    private long mLastLoadedAccountId = -1;
73    private long mAccountId = -1;
74    private long mSelectedMailboxId = -1;
75
76    private RefreshManager mRefreshManager;
77
78    // UI Support
79    private Activity mActivity;
80    private MailboxesAdapter mListAdapter;
81    private Callback mCallback = EmptyCallback.INSTANCE;
82
83    private ListView mListView;
84
85    private boolean mOpenRequested;
86    private boolean mResumed;
87
88    // Color used when we're dragging over a valid drop target
89    private static final int DROP_TARGET = 0xFFFFCC33;
90    // Color used when we're dragging over a "delete" target (i.e. the trash mailbox)
91    private static final int DROP_TARGET_TRASH = 0xFFFF0000;
92
93    // True if a drag is currently in progress
94    private boolean mDragInProgress = false;
95    // The mailbox id of the dragged item's mailbox.  We use it to prevent that box from being a
96    // valid drop target
97    private long mDragItemMailboxId = -1;
98    // The adapter position that the user's finger is hovering over
99    private int mDropTargetAdapterPosition = NO_DROP_TARGET;
100    // The mailbox list item view that the user's finger is hovering over
101    private MailboxListItem mDropTargetView;
102    // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized')
103    private int mDragItemHeight = -1;
104    // STOPSHIP  Scrolling related code is preliminary and will likely be completely replaced
105    // True if we are currently scrolling under the drag item
106    private boolean mTargetScrolling = false;
107    private int mScrollSpeed = -1;
108
109    private Utility.ListStateSaver mSavedListState;
110
111    private MailboxesAdapter.Callback mMailboxesAdapterCallback = new MailboxesAdapter.Callback() {
112       @Override
113        public void onSetDropTargetBackground(MailboxListItem listItem) {
114            listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
115        }
116    };
117
118    /**
119     * Callback interface that owning activities must implement
120     */
121    public interface Callback {
122        /** Called when a mailbox (including combined mailbox) is selected. */
123        public void onMailboxSelected(long accountId, long mailboxId);
124
125        /** Called when an account is selected on the combined view. */
126        public void onAccountSelected(long accountId);
127
128        /**
129         * Called when the list updates to propagate the current mailbox name and the unread count
130         * for it.
131         *
132         * Note the reason why it's separated from onMailboxSelected is because this needs to be
133         * reported when the unread count changes without changing the current mailbox.
134         */
135        public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount);
136    }
137
138    private static class EmptyCallback implements Callback {
139        public static final Callback INSTANCE = new EmptyCallback();
140        @Override public void onMailboxSelected(long accountId, long mailboxId) { }
141        @Override public void onAccountSelected(long accountId) { }
142        @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName,
143                int unreadCount) { }
144    }
145
146    /**
147     * Called to do initial creation of a fragment.  This is called after
148     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
149     */
150    @Override
151    public void onCreate(Bundle savedInstanceState) {
152        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
153            Log.d(Email.LOG_TAG, "MailboxListFragment onCreate");
154        }
155        super.onCreate(savedInstanceState);
156
157        mActivity = getActivity();
158        mRefreshManager = RefreshManager.getInstance(mActivity);
159        mListAdapter = new MailboxesAdapter(mActivity, MailboxesAdapter.MODE_NORMAL,
160                mMailboxesAdapterCallback);
161        if (savedInstanceState != null) {
162            restoreInstanceState(savedInstanceState);
163        }
164    }
165
166    @Override
167    public View onCreateView(
168            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
169        return inflater.inflate(R.layout.mailbox_list_fragment, container, false);
170    }
171
172    @Override
173    public void onActivityCreated(Bundle savedInstanceState) {
174        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
175            Log.d(Email.LOG_TAG, "MailboxListFragment onActivityCreated");
176        }
177        super.onActivityCreated(savedInstanceState);
178
179        mListView = getListView();
180        mListView.setOnItemClickListener(this);
181        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
182        mListView.setOnDragListener(this);
183        registerForContextMenu(mListView);
184    }
185
186    public void setCallback(Callback callback) {
187        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
188    }
189
190    /**
191     * @param accountId the account we're looking at
192     */
193    public void openMailboxes(long accountId) {
194        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
195            Log.d(Email.LOG_TAG, "MailboxListFragment openMailboxes");
196        }
197        if (accountId == -1) {
198            throw new InvalidParameterException();
199        }
200        if (mAccountId == accountId) {
201            return;
202        }
203        mOpenRequested = true;
204        mAccountId = accountId;
205        if (mResumed) {
206            startLoading();
207        }
208    }
209
210    public void setSelectedMailbox(long mailboxId) {
211        mSelectedMailboxId = mailboxId;
212        if (mResumed) {
213            highlightSelectedMailbox(true);
214        }
215    }
216
217    /**
218     * Called when the Fragment is visible to the user.
219     */
220    @Override
221    public void onStart() {
222        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
223            Log.d(Email.LOG_TAG, "MailboxListFragment onStart");
224        }
225        super.onStart();
226    }
227
228    /**
229     * Called when the fragment is visible to the user and actively running.
230     */
231    @Override
232    public void onResume() {
233        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
234            Log.d(Email.LOG_TAG, "MailboxListFragment onResume");
235        }
236        super.onResume();
237        mResumed = true;
238
239        // If we're recovering from the stopped state, we don't have to reload.
240        // (when mOpenRequested = false)
241        if (mAccountId != -1 && mOpenRequested) {
242            startLoading();
243        }
244    }
245
246    @Override
247    public void onPause() {
248        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
249            Log.d(Email.LOG_TAG, "MailboxListFragment onPause");
250        }
251        mResumed = false;
252        super.onPause();
253        mSavedListState = new Utility.ListStateSaver(getListView());
254    }
255
256    /**
257     * Called when the Fragment is no longer started.
258     */
259    @Override
260    public void onStop() {
261        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
262            Log.d(Email.LOG_TAG, "MailboxListFragment onStop");
263        }
264        super.onStop();
265    }
266
267    /**
268     * Called when the fragment is no longer in use.
269     */
270    @Override
271    public void onDestroy() {
272        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
273            Log.d(Email.LOG_TAG, "MailboxListFragment onDestroy");
274        }
275        super.onDestroy();
276    }
277
278    @Override
279    public void onSaveInstanceState(Bundle outState) {
280        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
281            Log.d(Email.LOG_TAG, "MailboxListFragment onSaveInstanceState");
282        }
283        super.onSaveInstanceState(outState);
284        outState.putLong(BUNDLE_KEY_SELECTED_MAILBOX_ID, mSelectedMailboxId);
285        outState.putParcelable(BUNDLE_LIST_STATE, new Utility.ListStateSaver(getListView()));
286    }
287
288    private void restoreInstanceState(Bundle savedInstanceState) {
289        mSelectedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MAILBOX_ID);
290        mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
291    }
292
293    private void startLoading() {
294        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
295            Log.d(Email.LOG_TAG, "MailboxListFragment startLoading");
296        }
297        mOpenRequested = false;
298        // Clear the list.  (ListFragment will show the "Loading" animation)
299        setListShown(false);
300
301        // If we've already loaded for a different account, discard the previous result and
302        // start loading again.
303        // We don't want to use restartLoader(), because if the Loader is retained, we *do* want to
304        // reuse the previous result.
305        // Also, when changing accounts, we don't preserve scroll position.
306        boolean accountChanging = false;
307        if ((mLastLoadedAccountId != -1) && (mLastLoadedAccountId != mAccountId)) {
308            accountChanging = true;
309            getLoaderManager().destroyLoader(LOADER_ID_MAILBOX_LIST);
310
311            // Also, when we're changing account, update the mailbox list if stale.
312            refreshMailboxListIfStale();
313        }
314        getLoaderManager().initLoader(LOADER_ID_MAILBOX_LIST, null,
315                new MailboxListLoaderCallbacks(accountChanging));
316    }
317
318    private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> {
319        private boolean mAccountChanging;
320
321        public MailboxListLoaderCallbacks(boolean accountChanging) {
322            mAccountChanging = accountChanging;
323        }
324
325        @Override
326        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
327            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
328                Log.d(Email.LOG_TAG, "MailboxListFragment onCreateLoader");
329            }
330            return MailboxesAdapter.createLoader(getActivity(), mAccountId,
331                    MailboxesAdapter.MODE_NORMAL);
332        }
333
334        @Override
335        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
336            if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
337                Log.d(Email.LOG_TAG, "MailboxListFragment onLoadFinished");
338            }
339            mLastLoadedAccountId = mAccountId;
340
341            // Save list view state (primarily scroll position)
342            final ListView lv = getListView();
343            final Utility.ListStateSaver lss;
344            if (mAccountChanging) {
345                lss = null; // Don't preserve list state
346            } else if (mSavedListState != null) {
347                lss = mSavedListState;
348                mSavedListState = null;
349            } else {
350                lss = new Utility.ListStateSaver(lv);
351            }
352
353            if (cursor.getCount() == 0) {
354                // If there's no row, don't set it to the ListView.
355                // Instead use setListShown(false) to make ListFragment show progress icon.
356                mListAdapter.swapCursor(null);
357                setListShown(false);
358            } else {
359                // Set the adapter.
360                mListAdapter.swapCursor(cursor);
361                setListAdapter(mListAdapter);
362                setListShown(true);
363
364                // We want to make selection visible only when account is changing..
365                // i.e. Refresh caused by content changed events shouldn't scroll the list.
366                highlightSelectedMailbox(mAccountChanging);
367            }
368
369            // Restore the state
370            if (lss != null) {
371                lss.restore(lv);
372            }
373
374            // Clear this for next reload triggered by content changed events.
375            mAccountChanging = false;
376        }
377
378        @Override
379        public void onLoaderReset(Loader<Cursor> loader) {
380            mListAdapter.swapCursor(null);
381        }
382    }
383
384    public void onItemClick(AdapterView<?> parent, View view, int position,
385            long idDontUseIt /* see MailboxesAdapter */ ) {
386        final long id = mListAdapter.getId(position);
387        if (mListAdapter.isAccountRow(position)) {
388            mCallback.onAccountSelected(id);
389        } else {
390            mCallback.onMailboxSelected(mAccountId, id);
391        }
392    }
393
394    public void onRefresh() {
395        if (mAccountId != -1) {
396            mRefreshManager.refreshMailboxList(mAccountId);
397        }
398    }
399
400    private void refreshMailboxListIfStale() {
401        if (mRefreshManager.isMailboxListStale(mAccountId)) {
402            mRefreshManager.refreshMailboxList(mAccountId);
403        }
404    }
405
406    /**
407     * Highlight the selected mailbox.
408     */
409    private void highlightSelectedMailbox(boolean ensureSelectionVisible) {
410        String mailboxName = "";
411        int unreadCount = 0;
412        if (mSelectedMailboxId == -1) {
413            // No mailbox selected
414            mListView.clearChoices();
415        } else {
416            final int count = mListView.getCount();
417            for (int i = 0; i < count; i++) {
418                if (mListAdapter.getId(i) != mSelectedMailboxId) {
419                    continue;
420                }
421                mListView.setItemChecked(i, true);
422                if (ensureSelectionVisible) {
423                    Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i);
424                }
425                mailboxName = mListAdapter.getDisplayName(i);
426                unreadCount = mListAdapter.getUnreadCount(i);
427                break;
428            }
429        }
430        mCallback.onCurrentMailboxUpdated(mSelectedMailboxId, mailboxName, unreadCount);
431    }
432
433    // Drag & Drop handling
434
435    /**
436     * Update all of the list's child views with the proper target background (for now, orange if
437     * a valid target, except red if the trash; standard background otherwise)
438     */
439    private void updateChildViews() {
440        int itemCount = mListView.getChildCount();
441        // Lazily initialize the height of our list items
442        if (itemCount > 0 && mDragItemHeight < 0) {
443            mDragItemHeight = mListView.getChildAt(0).getHeight();
444        }
445        for (int i = 0; i < itemCount; i++) {
446            MailboxListItem item = (MailboxListItem)mListView.getChildAt(i);
447            item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
448        }
449    }
450
451    /**
452     * Called when our ListView gets a DRAG_EXITED event
453     */
454    private void onDragExited() {
455        // Reset the background of the current target
456        if (mDropTargetAdapterPosition != NO_DROP_TARGET) {
457            mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
458            mDropTargetAdapterPosition = NO_DROP_TARGET;
459        }
460        stopScrolling();
461    }
462
463    /**
464     * STOPSHIP: Very preliminary scrolling code
465     */
466    private void onDragLocation(DragEvent event) {
467        // The drag is somewhere in the ListView
468        if (mDragItemHeight <= 0) {
469            // This shouldn't be possible, but avoid NPE
470            return;
471        }
472        // Find out which item we're in and highlight as appropriate
473        int rawTouchY = (int)event.getY();
474        int offset = 0;
475        if (mListView.getCount() > 0) {
476            offset = mListView.getChildAt(0).getTop();
477        }
478        int targetScreenPosition = (rawTouchY - offset) / mDragItemHeight;
479        int firstVisibleItem = mListView.getFirstVisiblePosition();
480        int targetAdapterPosition = firstVisibleItem + targetScreenPosition;
481        if (targetAdapterPosition != mDropTargetAdapterPosition) {
482            if (DEBUG_DRAG_DROP) {
483                Log.d(TAG, "========== DROP TARGET " + mDropTargetAdapterPosition + " -> " +
484                        targetAdapterPosition);
485            }
486            // Unhighlight the current target, if we've got one
487            if (mDropTargetAdapterPosition != NO_DROP_TARGET) {
488                mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId);
489            }
490            // Get the new target mailbox view
491            MailboxListItem newTarget =
492                (MailboxListItem)mListView.getChildAt(targetScreenPosition);
493            // This can be null due to a bug in the framework (checking on that)
494            // In any event, we're no longer dragging in the list view if newTarget is null
495            if (newTarget == null) {
496                if (DEBUG_DRAG_DROP) {
497                    Log.d(TAG, "========== WTF??? DRAG EXITED");
498                }
499                onDragExited();
500                return;
501            } else if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) {
502                Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " TRASH");
503                newTarget.setBackgroundColor(DROP_TARGET_TRASH);
504            } else if (newTarget.isDropTarget(mDragItemMailboxId)) {
505                Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " TARGET");
506                newTarget.setBackgroundColor(DROP_TARGET);
507            } else {
508                Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " (CALL)");
509                targetAdapterPosition = NO_DROP_TARGET;
510                newTarget.setDropTargetBackground(true, mDragItemMailboxId);
511            }
512            // Save away our current position and view
513            mDropTargetAdapterPosition = targetAdapterPosition;
514            mDropTargetView = newTarget;
515        }
516
517        // STOPSHIP
518        // This is a quick-and-dirty implementation of drag-under-scroll.  To be rewritten
519        int scrollDiff = rawTouchY - (mListView.getHeight() - SCROLL_ZONE_SIZE);
520        boolean scrollDown = false;
521        boolean scrollUp = false;
522        // 1 to 4
523        int scrollSpeed = 0;
524        if (scrollDiff > 0) {
525            scrollDown = true;
526            scrollSpeed = (scrollDiff / SCROLL_ZONE_SIZE / 4) + 1;
527        } else {
528            scrollDiff = SCROLL_ZONE_SIZE - rawTouchY;
529            if (scrollDiff > 0) {
530                scrollUp = true;
531                scrollSpeed = (scrollDiff / SCROLL_ZONE_SIZE / 4) + 1;
532            }
533        }
534        if ((!mTargetScrolling || (mScrollSpeed != scrollSpeed)) && scrollDown) {
535            mScrollSpeed = scrollSpeed;
536            int itemsToScroll = mListView.getCount() - targetAdapterPosition;
537            int pixelsToScroll = (itemsToScroll+1)*mDragItemHeight;
538            mListView.smoothScrollBy(pixelsToScroll, pixelsToScroll*16/scrollSpeed/4);
539            if (DEBUG_DRAG_DROP) {
540                Log.d(TAG, "========== START TARGET SCROLLING DOWN");
541            }
542            mTargetScrolling = true;
543        } else if ((!mTargetScrolling || mScrollSpeed != scrollSpeed) && scrollUp) {
544            mScrollSpeed = scrollSpeed;
545            int pixelsToScroll = (firstVisibleItem+1)*mDragItemHeight;
546            mListView.smoothScrollBy(-pixelsToScroll, pixelsToScroll*16/scrollSpeed/4);
547            if (DEBUG_DRAG_DROP) {
548                Log.d(TAG, "========== START TARGET SCROLLING UP");
549            }
550            mTargetScrolling = true;
551        } else if (!scrollUp && !scrollDown) {
552            stopScrolling();
553        }
554    }
555
556    /**
557     * Indicate that scrolling has stopped
558     */
559    private void stopScrolling() {
560        if (mTargetScrolling) {
561            mTargetScrolling = false;
562            if (DEBUG_DRAG_DROP) {
563                Log.d(TAG, "========== STOP TARGET SCROLLING");
564            }
565            // Stop the scrolling
566            mListView.smoothScrollBy(0, 0);
567        }
568    }
569
570    private void onDragEnded() {
571        if (mDragInProgress) {
572            mDragInProgress = false;
573            // Reenable updates to the view and redraw (in case it changed)
574            mListAdapter.enableUpdates(true);
575            mListAdapter.notifyDataSetChanged();
576            // Stop highlighting targets
577            updateChildViews();
578            // Stop any scrolling that was going on
579            stopScrolling();
580        }
581    }
582
583    private boolean onDragStarted(DragEvent event) {
584        // We handle dropping of items with our email mime type
585        // If the mime type has a mailbox id appended, that is the mailbox of the item
586        // being draged
587        ClipDescription description = event.getClipDescription();
588        int mimeTypeCount = description.getMimeTypeCount();
589        for (int i = 0; i < mimeTypeCount; i++) {
590            String mimeType = description.getMimeType(i);
591            if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) {
592                if (DEBUG_DRAG_DROP) {
593                    Log.d(TAG, "========== DRAG STARTED");
594                }
595                mDragItemMailboxId = -1;
596                // See if we find a mailbox id here
597                int dash = mimeType.lastIndexOf('-');
598                if (dash > 0) {
599                    try {
600                        mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1));
601                    } catch (NumberFormatException e) {
602                        // Ignore; we just won't know the mailbox
603                    }
604                }
605                mDragInProgress = true;
606                // Stop the list from updating
607                mListAdapter.enableUpdates(false);
608                // Update the backgrounds of our child views to highlight drop targets
609                updateChildViews();
610                return true;
611            }
612        }
613        return false;
614    }
615
616    private boolean onDrop(DragEvent event) {
617        stopScrolling();
618        // If we're not on a target, we're done
619        if (mDropTargetAdapterPosition == NO_DROP_TARGET) return false;
620        final Controller controller = Controller.getInstance(mActivity);
621        ClipData clipData = event.getClipData();
622        int count = clipData.getItemCount();
623        if (DEBUG_DRAG_DROP) {
624            Log.d(TAG, "Received a drop of " + count + " items.");
625        }
626        // Extract the messageId's to move from the ClipData (set up in MessageListItem)
627        final long[] messageIds = new long[count];
628        for (int i = 0; i < count; i++) {
629            Uri uri = clipData.getItem(i).getUri();
630            String msgNum = uri.getPathSegments().get(1);
631            long id = Long.parseLong(msgNum);
632            messageIds[i] = id;
633        }
634        // Call either deleteMessage or moveMessage, depending on the target
635        Utility.runAsync(new Runnable() {
636            @Override
637            public void run() {
638                if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) {
639                    for (long messageId: messageIds) {
640                        // TODO Get this off UI thread (put in clip)
641                        Message msg = Message.restoreMessageWithId(mActivity, messageId);
642                        if (msg != null) {
643                            controller.deleteMessage(messageId, msg.mAccountKey);
644                        }
645                    }
646                } else {
647                    controller.moveMessage(messageIds, mDropTargetView.mMailboxId);
648                }
649            }});
650        return true;
651    }
652
653    @Override
654    public boolean onDrag(View view, DragEvent event) {
655        boolean result = false;
656        switch (event.getAction()) {
657            case DragEvent.ACTION_DRAG_STARTED:
658                result = onDragStarted(event);
659                break;
660            case DragEvent.ACTION_DRAG_ENTERED:
661                // The drag has entered the ListView window
662                if (DEBUG_DRAG_DROP) {
663                    Log.d(TAG, "========== DRAG ENTERED (target = " + mDropTargetAdapterPosition +
664                    ")");
665                }
666                break;
667            case DragEvent.ACTION_DRAG_EXITED:
668                // The drag has left the building
669                if (DEBUG_DRAG_DROP) {
670                    Log.d(TAG, "========== DRAG EXITED (target = " + mDropTargetAdapterPosition +
671                            ")");
672                }
673                onDragExited();
674                break;
675            case DragEvent.ACTION_DRAG_ENDED:
676                // The drag is over
677                if (DEBUG_DRAG_DROP) {
678                    Log.d(TAG, "========== DRAG ENDED");
679                }
680                onDragEnded();
681                break;
682            case DragEvent.ACTION_DRAG_LOCATION:
683                // We're moving around within our window; handle scroll, if necessary
684                onDragLocation(event);
685                break;
686            case DragEvent.ACTION_DROP:
687                // The drag item was dropped
688                if (DEBUG_DRAG_DROP) {
689                    Log.d(TAG, "========== DROP");
690                }
691                result = onDrop(event);
692                break;
693            default:
694                break;
695        }
696        return result;
697    }
698}
699