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