MailboxListFragment.java revision d5dfd76cb90a3c73a4d4f82ddfc6b2c7cfcc95e1
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.Account;
26import com.android.emailcommon.provider.Mailbox;
27import com.android.emailcommon.utility.EmailAsyncTask;
28import com.android.emailcommon.utility.Utility;
29import com.google.common.annotations.VisibleForTesting;
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.Context;
38import android.content.Loader;
39import android.database.Cursor;
40import android.graphics.Rect;
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 or the combined mailboxes.
59 *
60 * This fragment has several parameters that determine the current view.
61 *
62 * <pre>
63 * Parameters:
64 * - Account ID.
65 *   - Set via {@link #newInstance}.
66 *   - Can be obtained with {@link #getAccountId()}.
67 *   - Will not change throughout fragment lifecycle.
68 *   - Either an actual account ID, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
69 *
70 * - "Highlight enabled?" flag
71 *   - Set via {@link #newInstance}.
72 *   - Can be obtained with {@link #getEnableHighlight()}.
73 *   - Will not change throughout fragment lifecycle.
74 *   - If {@code true}, we highlight the "selected" mailbox (used only on 2-pane).
75 *   - Note even if it's {@code true}, there may be no highlighted mailbox.
76 *     (This usually happens on 2-pane before the UI controller finds the Inbox to highlight.)
77 *
78 * - "Parent" mailbox ID
79 *   - Stored in {@link #mParentMailboxId}
80 *   - Changes as the user navigates through nested mailboxes.
81 *   - Initialized using the {@code mailboxId} parameter for {@link #newInstance}
82 *     in {@link #setInitialParentAndHighlight()}.
83 *
84 * - "Highlighted" mailbox
85 *   - Only used when highlighting is enabled.  (Otherwise always {@link Mailbox#NO_MAILBOX}.)
86 *     i.e. used only on two-pane.
87 *   - Stored in {@link #mHighlightedMailboxId}
88 *   - Initialized using the {@code mailboxId} parameter for {@link #newInstance}
89 *     in {@link #setInitialParentAndHighlight()}.
90 *
91 *   - Can be changed any time, using {@link #setHighlightedMailbox(long)}.
92 *
93 *   - If set, it's considered "selected", and we highlight the list item.
94 *
95 *   - (It should always be the ID of the list item selected in the list view, but we store it in
96 *     a member for efficiency.)
97 *
98 *   - Sometimes, we need to set the highlighted mailbox while we're still loading data.
99 *     In this case, we can't update {@link #mHighlightedMailboxId} right away, but need to do so
100 *     in when the next data set arrives, in
101 *     {@link MailboxListFragment.MailboxListLoaderCallbacks#onLoadFinished}.  For this, we use
102 *     we store the mailbox ID in {@link #mNextHighlightedMailboxId} and update
103 *     {@link #mHighlightedMailboxId} in onLoadFinished.
104 *
105 *
106 * The "selected" is defined using the "parent" and "highlighted" mailboxes.
107 * - "Selected" mailbox  (also sometimes called "current".)
108 *   - This is what the user thinks it's now selected.
109 *
110 *   - Can be obtained with {@link #getSelectedMailboxId()}
111 *   - If the "highlighted" mailbox exists, it's the "selected."  Otherwise, the "parent"
112 *     is considered "selected."
113 *   - This is what is passed to {@link Callback#onMailboxSelected}.
114 * </pre>
115 *
116 *
117 * This fragment shows the content in one of the three following views, depending on the
118 * parameters above.
119 *
120 * <pre>
121 * 1. Combined view
122 *   - Used if the account ID == {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
123 *   - Parent mailbox is always {@link Mailbox#NO_MAILBOX}.
124 *   - List contains:
125 *     - combined mailboxes
126 *     - all accounts
127 *
128 * 2. Root view for an account
129 *   - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and
130 *     Parent mailbox == {@link Mailbox#NO_MAILBOX}
131 *   - List contains
132 *     - all the top level mailboxes for the selected account.
133 *
134 * 3. Root view for a mailbox.  (nested view)
135 *   - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and
136 *     Parent mailbox != {@link Mailbox#NO_MAILBOX}
137 *   - List contains:
138 *     - parent mailbox (determined by "parent" mailbox ID)
139 *     - all child mailboxes of the parent mailbox.
140 * </pre>
141 *
142 *
143 * Note that when a fragment is put in the back stack, it'll lose the content view but the fragment
144 * itself is not destroyed.  If you call {@link #getListView()} in this state it'll throw
145 * an {@link IllegalStateException}.  So,
146 * - If code is supposed to be executed only when the fragment has the content view, use
147 *   {@link #getListView()} directly to make sure it doesn't accidentally get executed when there's
148 *   no views.
149 * - Otherwise, make sure to check if the fragment has views with {@link #isViewCreated()}
150 *   before touching any views.
151 *
152 * TODO Remove the nested folder navigation code during drag&drop.
153 */
154public class MailboxListFragment extends ListFragment implements OnItemClickListener,
155        OnDragListener {
156    private static final String TAG = "MailboxListFragment";
157
158    private static final String BUNDLE_KEY_PARENT_MAILBOX_ID
159            = "MailboxListFragment.state.parent_mailbox_id";
160    private static final String BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID
161            = "MailboxListFragment.state.selected_mailbox_id";
162    private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState";
163    private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE
164
165    /** While in drag-n-drop, amount of time before it auto expands; in ms */
166    private static final long AUTO_EXPAND_DELAY = 750L;
167
168    /** No drop target is available where the user is currently hovering over */
169    private static final int NO_DROP_TARGET = -1;
170    // Total height of the top and bottom scroll zones, in pixels
171    private static final int SCROLL_ZONE_SIZE = 64;
172    // The amount of time to scroll by one pixel, in ms
173    private static final int SCROLL_SPEED = 4;
174
175    /** Arbitrary number for use with the loader manager */
176    private static final int MAILBOX_LOADER_ID = 1;
177
178    /** Argument name(s) */
179    private static final String ARG_ACCOUNT_ID = "accountId";
180    private static final String ARG_ENABLE_HIGHLIGHT = "enablehighlight";
181    private static final String ARG_INITIAL_CURRENT_MAILBOX_ID = "initialParentMailboxId";
182
183    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
184
185    /** Timer to auto-expand folder lists during drag-n-drop */
186    private static final Timer sDragTimer = new Timer();
187    /** Rectangle used for hit testing children */
188    private static final Rect sTouchFrame = new Rect();
189
190    private RefreshManager mRefreshManager;
191
192    // UI Support
193    private Activity mActivity;
194    private MailboxFragmentAdapter mListAdapter;
195    private Callback mCallback = EmptyCallback.INSTANCE;
196
197    // See the class javadoc
198    private long mParentMailboxId;
199    private long mHighlightedMailboxId;
200
201    /**
202     * ID of the mailbox that should be highlighted when the next cursor is loaded.
203     */
204    private long mNextHighlightedMailboxId = Mailbox.NO_MAILBOX;
205
206    // True if a drag is currently in progress
207    private boolean mDragInProgress;
208    /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */
209    private long mDragItemMailboxId = -1;
210    /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */
211    private int mDropTargetId = NO_DROP_TARGET;
212    // The mailbox list item view that the user's finger is hovering over
213    private MailboxListItem mDropTargetView;
214    // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized')
215    private int mDragItemHeight = -1;
216    /** {@code true} if we are currently scrolling under the drag item */
217    private boolean mTargetScrolling;
218
219    private Parcelable mSavedListState;
220
221    private final MailboxFragmentAdapter.Callback mMailboxesAdapterCallback =
222            new MailboxFragmentAdapter.Callback() {
223        @Override
224        public void onBind(MailboxListItem listItem) {
225            listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
226        }
227    };
228
229    /**
230     * Callback interface that owning activities must implement
231     */
232    public interface Callback {
233        /**
234         * Called when any mailbox (even a combined mailbox) is selected.
235         *
236         * @param accountId
237         *          The ID of the owner account of the selected mailbox.
238         *          Or {@link Account#ACCOUNT_ID_COMBINED_VIEW} if it's a combined mailbox.
239         * @param mailboxId
240         *          The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0],
241         *          or a combined mailbox ID [e.g. {@link Mailbox#QUERY_ALL_INBOXES}].
242         * @param nestedNavigation {@code true} if the event is caused by nested mailbox navigation,
243         *          that is, going up or drilling-in to a child mailbox.
244         */
245        public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation);
246
247        /** Called when an account is selected on the combined view. */
248        public void onAccountSelected(long accountId);
249
250        /**
251         * TODO Remove it.  The behavior is not well-defined.  (Won't get called when highlight is
252         *      disabled.)
253         *      It was added only to update the action bar with the current mailbox name and the
254         *      message count.  Remove it and make the action bar watch the mailbox by itself.
255         *
256         * Called when the list updates to propagate the current mailbox name and the unread count
257         * for it.
258         *
259         * Note the reason why it's separated from onMailboxSelected is because this needs to be
260         * reported when the unread count changes without changing the current mailbox.
261         *
262         * @param mailboxId ID for the selected mailbox.  It'll never be of a combined mailbox,
263         *     and the owner account ID is always the same as
264         *     {@link MailboxListFragment#getAccountId()}.
265         */
266        public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount);
267
268        /**
269         * Called when the parent mailbox is changing.
270         */
271        public void onParentMailboxChanged();
272    }
273
274    private static class EmptyCallback implements Callback {
275        public static final Callback INSTANCE = new EmptyCallback();
276        @Override public void onMailboxSelected(long accountId, long mailboxId,
277                boolean nestedNavigation) { }
278        @Override public void onAccountSelected(long accountId) { }
279        @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName,
280                int unreadCount) { }
281        @Override
282        public void onParentMailboxChanged() { }
283    }
284
285    /**
286     * Returns the index of the view located at the specified coordinates in the given list.
287     * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned.
288     */
289    private static int pointToIndex(ListView list, int x, int y) {
290        final int count = list.getChildCount();
291        for (int i = count - 1; i >= 0; i--) {
292            final View child = list.getChildAt(i);
293            if (child.getVisibility() == View.VISIBLE) {
294                child.getHitRect(sTouchFrame);
295                if (sTouchFrame.contains(x, y)) {
296                    return i;
297                }
298            }
299        }
300        return NO_DROP_TARGET;
301    }
302
303    /**
304     * Create a new instance with initialization parameters.
305     *
306     * This fragment should be created only with this method.  (Arguments should always be set.)
307     *
308     * @param accountId The ID of the account we want to view
309     * @param initialCurrentMailboxId ID of the mailbox of interest.
310     *        Pass {@link Mailbox#NO_MAILBOX} to show top-level mailboxes.
311     * @param enableHighlight {@code true} if highlighting is enabled on the current screen
312     *        configuration.  (We don't highlight mailboxes on one-pane.)
313     */
314    public static MailboxListFragment newInstance(long accountId, long initialCurrentMailboxId,
315            boolean enableHighlight) {
316        final MailboxListFragment instance = new MailboxListFragment();
317        final Bundle args = new Bundle();
318        args.putLong(ARG_ACCOUNT_ID, accountId);
319        args.putLong(ARG_INITIAL_CURRENT_MAILBOX_ID, initialCurrentMailboxId);
320        args.putBoolean(ARG_ENABLE_HIGHLIGHT, enableHighlight);
321        instance.setArguments(args);
322        return instance;
323    }
324
325    /**
326     * The account ID the mailbox is associated with. Do not use directly; instead, use
327     * {@link #getAccountId()}.
328     * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
329     * constructs, this <em>must</em> be considered immutable.
330     */
331    private Long mImmutableAccountId;
332
333    /**
334     * {@code initialCurrentMailboxId} passed to {@link #newInstance}.
335     * Do not use directly; instead, use {@link #getInitialCurrentMailboxId()}.
336     * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
337     * constructs, this <em>must</em> be considered immutable.
338     */
339    private long mImmutableInitialCurrentMailboxId;
340
341    /**
342     * {@code enableHighlight} passed to {@link #newInstance}.
343     * Do not use directly; instead, use {@link #getEnableHighlight()}.
344     * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
345     * constructs, this <em>must</em> be considered immutable.
346     */
347    private boolean mImmutableEnableHighlight;
348
349    private void initializeArgCache() {
350        if (mImmutableAccountId != null) return;
351        mImmutableAccountId = getArguments().getLong(ARG_ACCOUNT_ID);
352        mImmutableInitialCurrentMailboxId = getArguments().getLong(ARG_INITIAL_CURRENT_MAILBOX_ID);
353        mImmutableEnableHighlight = getArguments().getBoolean(ARG_ENABLE_HIGHLIGHT);
354    }
355
356    /**
357     * @return {@code accountId} passed to {@link #newInstance}.  Safe to call even before onCreate.
358     */
359    public long getAccountId() {
360        initializeArgCache();
361        return mImmutableAccountId;
362    }
363
364    /**
365     * @return {@code initialCurrentMailboxId} passed to {@link #newInstance}.
366     * Safe to call even before onCreate.
367     */
368    public long getInitialCurrentMailboxId() {
369        initializeArgCache();
370        return mImmutableInitialCurrentMailboxId;
371    }
372
373    /**
374     * @return {@code enableHighlight} passed to {@link #newInstance}.
375     * Safe to call even before onCreate.
376     */
377    public boolean getEnableHighlight() {
378        initializeArgCache();
379        return mImmutableEnableHighlight;
380    }
381
382    @Override
383    public void onAttach(Activity activity) {
384        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
385            Log.d(Logging.LOG_TAG, this + " onAttach");
386        }
387        super.onAttach(activity);
388    }
389
390    /**
391     * Called to do initial creation of a fragment.  This is called after
392     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
393     */
394    @Override
395    public void onCreate(Bundle savedInstanceState) {
396        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
397            Log.d(Logging.LOG_TAG, this + " onCreate");
398        }
399        super.onCreate(savedInstanceState);
400
401        mActivity = getActivity();
402        mRefreshManager = RefreshManager.getInstance(mActivity);
403        mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback);
404        setListAdapter(mListAdapter); // It's safe to do even before the list view is created.
405
406        if (savedInstanceState == null) {
407            setInitialParentAndHighlight();
408        } else {
409            restoreInstanceState(savedInstanceState);
410        }
411    }
412
413    /**
414     * Set {@link #mParentMailboxId} and {@link #mHighlightedMailboxId} from the fragment arguments.
415     */
416    private void setInitialParentAndHighlight() {
417        if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) {
418            // For the combined view, always show the top-level, but highlight the "current".
419            mParentMailboxId = Mailbox.NO_MAILBOX;
420        } else {
421            // Otherwise, try using the "current" as the "parent" (and also highlight it).
422            // If it has no children, we go up in onLoadFinished().
423            mParentMailboxId = getInitialCurrentMailboxId();
424        }
425        // Highlight the mailbox of interest
426        if (getEnableHighlight()) {
427            mHighlightedMailboxId = getInitialCurrentMailboxId();
428        }
429    }
430
431    @Override
432    public View onCreateView(
433            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
434        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
435            Log.d(Logging.LOG_TAG, this + " onCreateView");
436        }
437        return inflater.inflate(R.layout.mailbox_list_fragment, container, false);
438    }
439
440    /**
441     * @return true if the content view is created and not destroyed yet. (i.e. between
442     * {@link #onCreateView} and {@link #onDestroyView}.
443     */
444    private boolean isViewCreated() {
445        return getView() != null;
446    }
447
448    @Override
449    public void onActivityCreated(Bundle savedInstanceState) {
450        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
451            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
452        }
453        super.onActivityCreated(savedInstanceState);
454
455        // Note we can't do this in onCreateView.
456        // getListView() is only usable after onCreateView().
457        final ListView lv = getListView();
458        lv.setOnItemClickListener(this);
459        lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
460        lv.setOnDragListener(this);
461
462        startLoading(mParentMailboxId, mHighlightedMailboxId);
463
464        UiUtilities.installFragment(this);
465    }
466
467    public void setCallback(Callback callback) {
468        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
469    }
470
471    /**
472     * Called when the Fragment is visible to the user.
473     */
474    @Override
475    public void onStart() {
476        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
477            Log.d(Logging.LOG_TAG, this + " onStart");
478        }
479        super.onStart();
480    }
481
482    /**
483     * Called when the fragment is visible to the user and actively running.
484     */
485    @Override
486    public void onResume() {
487        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
488            Log.d(Logging.LOG_TAG, this + " onResume");
489        }
490        super.onResume();
491
492        // Fetch the latest mailbox list from the server here if stale so that the user always
493        // sees the (reasonably) up-to-date mailbox list, without pressing "refresh".
494        final long accountId = getAccountId();
495        if (mRefreshManager.isMailboxListStale(accountId)) {
496            mRefreshManager.refreshMailboxList(accountId);
497        }
498    }
499
500    @Override
501    public void onPause() {
502        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
503            Log.d(Logging.LOG_TAG, this + " onPause");
504        }
505        mSavedListState = getListView().onSaveInstanceState();
506        super.onPause();
507    }
508
509    /**
510     * Called when the Fragment is no longer started.
511     */
512    @Override
513    public void onStop() {
514        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
515            Log.d(Logging.LOG_TAG, this + " onStop");
516        }
517        super.onStop();
518    }
519
520    @Override
521    public void onDestroyView() {
522        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
523            Log.d(Logging.LOG_TAG, this + " onDestroyView");
524        }
525        UiUtilities.uninstallFragment(this);
526        super.onDestroyView();
527    }
528
529    /**
530     * Called when the fragment is no longer in use.
531     */
532    @Override
533    public void onDestroy() {
534        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
535            Log.d(Logging.LOG_TAG, this + " onDestroy");
536        }
537        mTaskTracker.cancellAllInterrupt();
538        super.onDestroy();
539    }
540
541    @Override
542    public void onDetach() {
543        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
544            Log.d(Logging.LOG_TAG, this + " onDetach");
545        }
546        super.onDetach();
547    }
548
549    @Override
550    public void onSaveInstanceState(Bundle outState) {
551        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
552            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
553        }
554        super.onSaveInstanceState(outState);
555        outState.putLong(BUNDLE_KEY_PARENT_MAILBOX_ID, mParentMailboxId);
556        outState.putLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID, mHighlightedMailboxId);
557        if (isViewCreated()) {
558            outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState());
559        }
560    }
561
562    private void restoreInstanceState(Bundle savedInstanceState) {
563        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
564            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
565        }
566        mParentMailboxId = savedInstanceState.getLong(BUNDLE_KEY_PARENT_MAILBOX_ID);
567        mHighlightedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID);
568        mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE);
569    }
570
571    /**
572     * @return "Selected" mailbox ID.
573     */
574    public long getSelectedMailboxId() {
575        return (mHighlightedMailboxId != Mailbox.NO_MAILBOX) ? mHighlightedMailboxId
576                : mParentMailboxId;
577    }
578
579    /**
580     * @return {@code true} if top-level mailboxes are shown.  {@code false} otherwise.
581     */
582    public boolean isRoot() {
583        return mParentMailboxId == Mailbox.NO_MAILBOX;
584    }
585
586    /**
587     * Navigate one level up in the mailbox hierarchy. Does nothing if at the root account view.
588     */
589    public boolean navigateUp() {
590        if (isRoot()) {
591            return false;
592        }
593        FindParentMailboxTask.ResultCallback callback = new FindParentMailboxTask.ResultCallback() {
594            @Override public void onResult(long nextParentMailboxId,
595                    long nextHighlightedMailboxId, long nextSelectedMailboxId) {
596
597                startLoading(nextParentMailboxId, nextHighlightedMailboxId);
598
599                if (nextSelectedMailboxId != Mailbox.NO_MAILBOX) {
600                    mCallback.onMailboxSelected(getAccountId(), nextSelectedMailboxId, true);
601                }
602            }
603        };
604        new FindParentMailboxTask(
605                getActivity().getApplicationContext(), mTaskTracker, getAccountId(),
606                getEnableHighlight(), mParentMailboxId, mHighlightedMailboxId, callback
607                ).cancelPreviousAndExecuteParallel((Void[]) null);
608        return true;
609    }
610
611    /**
612     * A task to determine what parent mailbox ID/highlighted mailbox ID to use for the "UP"
613     * navigation, given the current parent mailbox ID, the highlighted mailbox ID, and {@link
614     * #mEnableHighlight}.
615     */
616    @VisibleForTesting
617    static class FindParentMailboxTask extends EmailAsyncTask<Void, Void, Long[]> {
618        public interface ResultCallback {
619            /**
620             * Callback to get the result.
621             *
622             * @param nextParentMailboxId ID of the mailbox to use
623             * @param nextHighlightedMailboxId ID of the mailbox to highlight
624             * @param nextSelectedMailboxId ID of the mailbox to notify with
625             *        {@link Callback#onMailboxSelected}.
626             */
627            public void onResult(long nextParentMailboxId, long nextHighlightedMailboxId,
628                    long nextSelectedMailboxId);
629        }
630
631        private final Context mContext;
632        private final long mAccountId;
633        private final boolean mEnableHighlight;
634        private final long mParentMailboxId;
635        private final long mHighlightedMailboxId;
636        private final ResultCallback mCallback;
637
638        public FindParentMailboxTask(Context context, EmailAsyncTask.Tracker taskTracker,
639                long accountId, boolean enableHighlight, long parentMailboxId,
640                long highlightedMailboxId, ResultCallback callback) {
641            super(taskTracker);
642            mContext = context;
643            mAccountId = accountId;
644            mEnableHighlight = enableHighlight;
645            mParentMailboxId = parentMailboxId;
646            mHighlightedMailboxId = highlightedMailboxId;
647            mCallback = callback;
648        }
649
650        @Override
651        protected Long[] doInBackground(Void... params) {
652            Mailbox parentMailbox = Mailbox.restoreMailboxWithId(mContext, mParentMailboxId);
653            final long nextParentId = (parentMailbox == null) ? Mailbox.NO_MAILBOX
654                    : parentMailbox.mParentKey;
655            final long nextHighlightedId;
656            final long nextSelectedId;
657            if (mEnableHighlight) {
658                // If the "parent" is highlighted before the transition, it should still be
659                // highlighted after the upper level view.
660                if (mParentMailboxId == mHighlightedMailboxId) {
661                    nextHighlightedId = mParentMailboxId;
662                } else {
663                    // Otherwise, the next parent will be highlighted, unless we're going up to
664                    // the root, in which case Inbox should be highlighted.
665                    if (nextParentId == Mailbox.NO_MAILBOX) {
666                        nextHighlightedId = Mailbox.findMailboxOfType(mContext, mAccountId,
667                                Mailbox.TYPE_INBOX);
668                    } else {
669                        nextHighlightedId = nextParentId;
670                    }
671                }
672
673                // Highlighted one will be "selected".
674                nextSelectedId = nextHighlightedId;
675
676            } else { // !mEnableHighlight
677                nextHighlightedId = Mailbox.NO_MAILBOX;
678
679                // Parent will be selected.
680                nextSelectedId = nextParentId;
681            }
682            return new Long[]{nextParentId, nextHighlightedId, nextSelectedId};
683        }
684
685        @Override
686        protected void onPostExecute(Long[] result) {
687            mCallback.onResult(result[0], result[1], result[2]);
688        }
689    }
690
691    /**
692     * Starts the loader.
693     *
694     * @param parentMailboxId Mailbox ID to be used as the "parent" mailbox
695     * @param highlightedMailboxId Mailbox ID that should be highlighted when the data is loaded.
696     */
697    private void startLoading(long parentMailboxId, long highlightedMailboxId
698            ) {
699        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
700            Log.d(Logging.LOG_TAG, this + " startLoading  parent=" + parentMailboxId
701                    + " highlighted=" + highlightedMailboxId);
702        }
703        final LoaderManager lm = getLoaderManager();
704        boolean parentMailboxChanging = false;
705
706        // Parent mailbox changing -- destroy the current loader to force reload.
707        if (mParentMailboxId != parentMailboxId) {
708            lm.destroyLoader(MAILBOX_LOADER_ID);
709            setListShown(false);
710            parentMailboxChanging = true;
711        }
712        mParentMailboxId = parentMailboxId;
713        if (getEnableHighlight()) {
714            mNextHighlightedMailboxId = highlightedMailboxId;
715        }
716
717        lm.initLoader(MAILBOX_LOADER_ID, null, new MailboxListLoaderCallbacks());
718
719        if (parentMailboxChanging) {
720            mCallback.onParentMailboxChanged();
721        }
722    }
723
724    /**
725     * Highlight the given mailbox.
726     *
727     * If data is already loaded, it just sets {@link #mHighlightedMailboxId} and highlight the
728     * corresponding list item.  (And if the corresponding list item is not found,
729     * {@link #mHighlightedMailboxId} is set to {@link Mailbox#NO_MAILBOX})
730     *
731     * If we're still loading data, it sets {@link #mNextHighlightedMailboxId} instead, and then
732     * it'll be set to {@link #mHighlightedMailboxId} in
733     * {@link MailboxListLoaderCallbacks#onLoadFinished}.
734     *
735     * @param mailboxId The ID of the mailbox to highlight.
736     */
737    public void setHighlightedMailbox(long mailboxId) {
738        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
739            Log.d(Logging.LOG_TAG, this + " setHighlightedMailbox  mailbox=" + mailboxId);
740        }
741        if (!getEnableHighlight()) {
742            return;
743        }
744        if (mHighlightedMailboxId == mailboxId) {
745            return; // already highlighted.
746        }
747        if (mListAdapter.getCursor() == null) {
748            // List not loaded yet.  Just remember the ID here and let onLoadFinished() update
749            // mHighlightedMailboxId.
750            mNextHighlightedMailboxId = mailboxId;
751            return;
752        }
753        mHighlightedMailboxId = mailboxId;
754        updateHighlightedMailbox(true);
755    }
756
757    // TODO This class probably should be made static. There are many calls into the enclosing
758    // class and we need to be cautious about what we call while in these callbacks
759    private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> {
760        private boolean mIsFirstLoad;
761
762        @Override
763        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
764            if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
765                Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onCreateLoader");
766            }
767            mIsFirstLoad = true;
768            if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) {
769                return MailboxFragmentAdapter.createCombinedViewLoader(getActivity());
770            } else {
771                return MailboxFragmentAdapter.createMailboxesLoader(getActivity(), getAccountId(),
772                        mParentMailboxId);
773            }
774        }
775
776        @Override
777        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
778            if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
779                Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoadFinished  count="
780                        + cursor.getCount());
781            }
782            // Note in onLoadFinished we can assume the view is created.
783            // The loader manager doesn't deliver results when a fragment is stopped.
784
785            // If we're showing a nested mailboxes, and the current parent mailbox has no children,
786            // go up.
787            if (getAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW) {
788                MailboxFragmentAdapter.CursorWithExtras c =
789                        (MailboxFragmentAdapter.CursorWithExtras) cursor;
790                if ((c.mChildCount == 0) && !isRoot()) {
791                    navigateUp();
792                    return;
793                }
794            }
795
796            // Save list view state (primarily scroll position)
797            final ListView lv = getListView();
798            final Parcelable listState;
799            if (mSavedListState != null) {
800                listState = mSavedListState;
801                mSavedListState = null;
802            } else {
803                listState = lv.onSaveInstanceState();
804            }
805
806            if (cursor.getCount() == 0) {
807                // There's no row -- call setListShown(false) to make ListFragment show progress
808                // icon.
809                mListAdapter.swapCursor(null);
810                setListShown(false);
811            } else {
812                mListAdapter.swapCursor(cursor);
813                setListShown(true);
814
815                // Update the highlighted mailbox
816                if (mNextHighlightedMailboxId != Mailbox.NO_MAILBOX) {
817                    mHighlightedMailboxId = mNextHighlightedMailboxId;
818                    mNextHighlightedMailboxId = Mailbox.NO_MAILBOX;
819                }
820
821                // We want to make visible the selection only for the first load.
822                // Re-load caused by content changed events shouldn't scroll the list.
823                if (!updateHighlightedMailbox(mIsFirstLoad)) {
824
825                    // TODO We should just select the parent mailbox, or Inbox if it's already
826                    // top-level.  Make sure to call onMailboxSelected().
827                    return;
828                }
829            }
830
831            // List has been reloaded; clear any drop target information
832            mDropTargetId = NO_DROP_TARGET;
833            mDropTargetView = null;
834
835            // Restore the list state.
836            lv.onRestoreInstanceState(listState);
837
838            mIsFirstLoad = false;
839        }
840
841        @Override
842        public void onLoaderReset(Loader<Cursor> loader) {
843            if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
844                Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoaderReset");
845            }
846            mListAdapter.swapCursor(null);
847        }
848    }
849
850    /**
851     * {@inheritDoc}
852     * <p>
853     * @param doNotUse <em>IMPORTANT</em>: Do not use this parameter. The ID in the list widget
854     * must be a positive value. However, we rely on negative IDs for special mailboxes. Instead,
855     * we use the ID returned by {@link MailboxFragmentAdapter#getId(int)}.
856     */
857    @Override
858    public void onItemClick(AdapterView<?> parent, View view, int position, long doNotUse) {
859        final long id = mListAdapter.getId(position);
860        if (mListAdapter.isAccountRow(position)) {
861            mCallback.onAccountSelected(id);
862        } else {
863            // Save account-id.  (Need to do this before startLoading() below, which will destroy
864            // the current loader and make the mListAdapter lose the cursor.
865            // Note, don't just use getAccountId().  A mailbox may tied to a different account ID
866            // from getAccountId().  (Currently "Starred" does so.)
867            final long accountId = mListAdapter.getAccountId(position);
868            boolean nestedNavigation = false;
869            if (((MailboxListItem) view).isNavigable() && (id != mParentMailboxId)) {
870                // Drill-in.  Selected one will be the next parent, and it'll also be highlighted.
871                startLoading(id, id);
872                nestedNavigation = true;
873            }
874            mCallback.onMailboxSelected(accountId, id, nestedNavigation);
875        }
876    }
877
878    /**
879     * Really highlight the mailbox for {@link #mHighlightedMailboxId} on the list view.
880     *
881     * Note if a list item for {@link #mHighlightedMailboxId} is not found,
882     * {@link #mHighlightedMailboxId} will be set to {@link Mailbox#NO_MAILBOX}.
883     *
884     * @return false when the highlighted mailbox seems to be gone; i.e. if
885     *         {@link #mHighlightedMailboxId} is set but not found in the list.
886     */
887    private boolean updateHighlightedMailbox(boolean ensureSelectionVisible) {
888        if (!getEnableHighlight() || !isViewCreated()) {
889            return true; // Nothing to highlight
890        }
891        final ListView lv = getListView();
892        boolean found = false;
893        String mailboxName = "";
894        int unreadCount = 0;
895        if (mHighlightedMailboxId == Mailbox.NO_MAILBOX) {
896            // No mailbox selected
897            lv.clearChoices();
898            found = true;
899        } else {
900            // TODO Don't mix list view & list adapter indices. This is a recipe for disaster.
901            final int count = lv.getCount();
902            for (int i = 0; i < count; i++) {
903                if (mListAdapter.getId(i) != mHighlightedMailboxId) {
904                    continue;
905                }
906                found = true;
907                lv.setItemChecked(i, true);
908                if (ensureSelectionVisible) {
909                    Utility.listViewSmoothScrollToPosition(getActivity(), lv, i);
910                }
911                mailboxName = mListAdapter.getDisplayName(mActivity, i);
912                unreadCount = mListAdapter.getUnreadCount(i);
913                break;
914            }
915        }
916        if (found) {
917            mCallback.onCurrentMailboxUpdated(mHighlightedMailboxId, mailboxName, unreadCount);
918        } else {
919            mHighlightedMailboxId = Mailbox.NO_MAILBOX;
920        }
921        return found;
922    }
923
924    // Drag & Drop handling
925
926    /**
927     * Update all of the list's child views with the proper target background (for now, orange if
928     * a valid target, except red if the trash; standard background otherwise)
929     */
930    private void updateChildViews() {
931        final ListView lv = getListView();
932        int itemCount = lv.getChildCount();
933        // Lazily initialize the height of our list items
934        if (itemCount > 0 && mDragItemHeight < 0) {
935            mDragItemHeight = lv.getChildAt(0).getHeight();
936        }
937        for (int i = 0; i < itemCount; i++) {
938            final View child = lv.getChildAt(i);
939            if (!(child instanceof MailboxListItem)) {
940                continue;
941            }
942            MailboxListItem item = (MailboxListItem) child;
943            item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
944        }
945    }
946
947    /**
948     * Called when the user has dragged outside of the mailbox list area.
949     */
950    private void onDragExited() {
951        // Reset the background of the current target
952        if (mDropTargetView != null) {
953            mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId);
954            mDropTargetView = null;
955        }
956        mDropTargetId = NO_DROP_TARGET;
957        stopScrolling();
958    }
959
960    /**
961     * Called while dragging;  highlight possible drop targets, and auto scroll the list.
962     */
963    private void onDragLocation(DragEvent event) {
964        final ListView lv = getListView();
965        // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop
966        // if the list is being updated [i.e. navigated to another mailbox]
967        if (mDragItemHeight <= 0) {
968            // This shouldn't be possible, but avoid NPE
969            Log.w(TAG, "drag item height is not set");
970            return;
971        }
972        // Find out which item we're in and highlight as appropriate
973        final int rawTouchX = (int) event.getX();
974        final int rawTouchY = (int) event.getY();
975        final int viewIndex = pointToIndex(lv, rawTouchX, rawTouchY);
976        int targetId = viewIndex;
977        if (targetId != mDropTargetId) {
978            if (DEBUG_DRAG_DROP) {
979                Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId);
980            }
981            // Remove highlight the current target; if there was one
982            if (mDropTargetView != null) {
983                mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId);
984                mDropTargetView = null;
985            }
986            // Get the new target mailbox view
987            final View childView = lv.getChildAt(viewIndex);
988            final MailboxListItem newTarget;
989            if (childView == null) {
990                // In any event, we're no longer dragging in the list view if newTarget is null
991                if (DEBUG_DRAG_DROP) {
992                    Log.d(TAG, "=== Drag off the list");
993                }
994                newTarget = null;
995                final int childCount = lv.getChildCount();
996                if (viewIndex >= childCount) {
997                    // Touching beyond the end of the list; may happen for small lists
998                    onDragExited();
999                    return;
1000                } else {
1001                    // We should never get here
1002                    Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount);
1003                }
1004            } else if (!(childView instanceof MailboxListItem)) {
1005                // We're over a header suchas "Recent folders".  We shouldn't finish DnD, but
1006                // drop should be disabled.
1007                newTarget = null;
1008                targetId = NO_DROP_TARGET;
1009            } else {
1010                newTarget = (MailboxListItem) childView;
1011                if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) {
1012                    if (DEBUG_DRAG_DROP) {
1013                        Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId);
1014                    }
1015                    newTarget.setDropTrashBackground();
1016                } else if (newTarget.isDropTarget(mDragItemMailboxId)) {
1017                    if (DEBUG_DRAG_DROP) {
1018                        Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId);
1019                    }
1020                    newTarget.setDropActiveBackground();
1021                } else {
1022                    if (DEBUG_DRAG_DROP) {
1023                        Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId);
1024                    }
1025                    newTarget.setDropTargetBackground(true, mDragItemMailboxId);
1026                    targetId = NO_DROP_TARGET;
1027                }
1028            }
1029            // Save away our current position and view
1030            mDropTargetId = targetId;
1031            mDropTargetView = newTarget;
1032        }
1033
1034        // This is a quick-and-dirty implementation of drag-under-scroll; something like this
1035        // should eventually find its way into the framework
1036        int scrollDiff = rawTouchY - (lv.getHeight() - SCROLL_ZONE_SIZE);
1037        boolean scrollDown = (scrollDiff > 0);
1038        boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY);
1039        if (!mTargetScrolling && scrollDown) {
1040            int itemsToScroll = lv.getCount() - lv.getLastVisiblePosition();
1041            int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight;
1042            lv.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
1043            if (DEBUG_DRAG_DROP) {
1044                Log.d(TAG, "=== Start scrolling list down");
1045            }
1046            mTargetScrolling = true;
1047        } else if (!mTargetScrolling && scrollUp) {
1048            int pixelsToScroll = (lv.getFirstVisiblePosition() + 1) * mDragItemHeight;
1049            lv.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED);
1050            if (DEBUG_DRAG_DROP) {
1051                Log.d(TAG, "=== Start scrolling list up");
1052            }
1053            mTargetScrolling = true;
1054        } else if (!scrollUp && !scrollDown) {
1055            stopScrolling();
1056        }
1057    }
1058
1059    /**
1060     * Indicate that scrolling has stopped
1061     */
1062    private void stopScrolling() {
1063        final ListView lv = getListView();
1064        if (mTargetScrolling) {
1065            mTargetScrolling = false;
1066            if (DEBUG_DRAG_DROP) {
1067                Log.d(TAG, "=== Stop scrolling list");
1068            }
1069            // Stop the scrolling
1070            lv.smoothScrollBy(0, 0);
1071        }
1072    }
1073
1074    private void onDragEnded() {
1075        if (mDragInProgress) {
1076            mDragInProgress = false;
1077            // Reenable updates to the view and redraw (in case it changed)
1078            MailboxFragmentAdapter.enableUpdates(true);
1079            mListAdapter.notifyDataSetChanged();
1080            // Stop highlighting targets
1081            updateChildViews();
1082            // Stop any scrolling that was going on
1083            stopScrolling();
1084        }
1085    }
1086
1087    private boolean onDragStarted(DragEvent event) {
1088        // We handle dropping of items with our email mime type
1089        // If the mime type has a mailbox id appended, that is the mailbox of the item
1090        // being draged
1091        ClipDescription description = event.getClipDescription();
1092        int mimeTypeCount = description.getMimeTypeCount();
1093        for (int i = 0; i < mimeTypeCount; i++) {
1094            String mimeType = description.getMimeType(i);
1095            if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) {
1096                if (DEBUG_DRAG_DROP) {
1097                    Log.d(TAG, "=== Drag started");
1098                }
1099                mDragItemMailboxId = -1;
1100                // See if we find a mailbox id here
1101                int dash = mimeType.lastIndexOf('-');
1102                if (dash > 0) {
1103                    try {
1104                        mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1));
1105                    } catch (NumberFormatException e) {
1106                        // Ignore; we just won't know the mailbox
1107                    }
1108                }
1109                mDragInProgress = true;
1110                // Stop the list from updating
1111                MailboxFragmentAdapter.enableUpdates(false);
1112                // Update the backgrounds of our child views to highlight drop targets
1113                updateChildViews();
1114                return true;
1115            }
1116        }
1117        return false;
1118    }
1119
1120    /**
1121     * Perform a "drop" action. If the user is not on top of a valid drop target, no action
1122     * is performed.
1123     * @return {@code true} if the drop action was performed. Otherwise {@code false}.
1124     */
1125    private boolean onDrop(DragEvent event) {
1126        stopScrolling();
1127        // If we're not on a target, we're done
1128        if (mDropTargetId == NO_DROP_TARGET) {
1129            return false;
1130        }
1131        final Controller controller = Controller.getInstance(mActivity);
1132        ClipData clipData = event.getClipData();
1133        int count = clipData.getItemCount();
1134        if (DEBUG_DRAG_DROP) {
1135            Log.d(TAG, "=== Dropping " + count + " items.");
1136        }
1137        // Extract the messageId's to move from the ClipData (set up in MessageListItem)
1138        final long[] messageIds = new long[count];
1139        for (int i = 0; i < count; i++) {
1140            Uri uri = clipData.getItemAt(i).getUri();
1141            String msgNum = uri.getPathSegments().get(1);
1142            long id = Long.parseLong(msgNum);
1143            messageIds[i] = id;
1144        }
1145        // Call either deleteMessage or moveMessage, depending on the target
1146        if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) {
1147            controller.deleteMessages(messageIds);
1148        } else {
1149            controller.moveMessages(messageIds, mDropTargetView.mMailboxId);
1150        }
1151        return true;
1152    }
1153
1154    @Override
1155    public boolean onDrag(View view, DragEvent event) {
1156        boolean result = false;
1157        switch (event.getAction()) {
1158            case DragEvent.ACTION_DRAG_STARTED:
1159                result = onDragStarted(event);
1160                break;
1161            case DragEvent.ACTION_DRAG_ENTERED:
1162                // The drag has entered the ListView window
1163                if (DEBUG_DRAG_DROP) {
1164                    Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId);
1165                }
1166                break;
1167            case DragEvent.ACTION_DRAG_EXITED:
1168                // The drag has left the building
1169                if (DEBUG_DRAG_DROP) {
1170                    Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId);
1171                }
1172                onDragExited();
1173                break;
1174            case DragEvent.ACTION_DRAG_ENDED:
1175                // The drag is over
1176                if (DEBUG_DRAG_DROP) {
1177                    Log.d(TAG, "=== Drag ended");
1178                }
1179                onDragEnded();
1180                break;
1181            case DragEvent.ACTION_DRAG_LOCATION:
1182                // We're moving around within our window; handle scroll, if necessary
1183                onDragLocation(event);
1184                break;
1185            case DragEvent.ACTION_DROP:
1186                // The drag item was dropped
1187                if (DEBUG_DRAG_DROP) {
1188                    Log.d(TAG, "=== Drop");
1189                }
1190                result = onDrop(event);
1191                break;
1192            default:
1193                break;
1194        }
1195        return result;
1196    }
1197}
1198