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