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