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