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