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