FolderListFragment.java revision 61f26c2d1c1d3735cf883b58fe7e45550bb1a54c
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.app.Activity;
23import android.app.ListFragment;
24import android.app.LoaderManager;
25import android.content.Loader;
26import android.net.Uri;
27import android.os.Bundle;
28import android.support.v4.widget.DrawerLayout;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.animation.DecelerateInterpolator;
33import android.view.animation.Interpolator;
34import android.widget.AbsListView;
35import android.widget.ArrayAdapter;
36import android.widget.BaseAdapter;
37import android.widget.ImageView;
38import android.widget.ListAdapter;
39import android.widget.ListView;
40import android.widget.TextView;
41
42import com.android.mail.R;
43import com.android.mail.adapter.DrawerItem;
44import com.android.mail.analytics.Analytics;
45import com.android.mail.browse.MergedAdapter;
46import com.android.mail.browse.ScrollIndicatorsView;
47import com.android.mail.content.ObjectCursor;
48import com.android.mail.content.ObjectCursorLoader;
49import com.android.mail.providers.Account;
50import com.android.mail.providers.AccountObserver;
51import com.android.mail.providers.AllAccountObserver;
52import com.android.mail.providers.DrawerClosedObserver;
53import com.android.mail.providers.Folder;
54import com.android.mail.providers.FolderObserver;
55import com.android.mail.providers.FolderWatcher;
56import com.android.mail.providers.RecentFolderObserver;
57import com.android.mail.providers.UIProvider;
58import com.android.mail.providers.UIProvider.FolderType;
59import com.android.mail.utils.FolderUri;
60import com.android.mail.utils.LogTag;
61import com.android.mail.utils.LogUtils;
62import com.android.mail.utils.Utils;
63import com.google.common.collect.Lists;
64
65import java.util.ArrayList;
66import java.util.Iterator;
67import java.util.List;
68
69/**
70 * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
71 * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
72 * in a drawer along with the list of folders.
73 *
74 * This class has the following use-cases:
75 * <ul>
76 *     <li>
77 *         Show a list of accounts and a divided list of folders. In this case, the list shows
78 *         Accounts, Inboxes, Recent Folders, All folders, Help, and Feedback.
79 *         Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
80 *         folders switches folders. Tapping on Help takes the user to HTML help pages. Tapping on
81 *         Feedback takes the user to a screen for submitting text and a screenshot of the
82 *         application to a feedback system.
83 *         This is created through XML resources as a {@link DrawerFragment}. Since it is created
84 *         through resources, it receives all arguments through callbacks.
85 *     </li>
86 *     <li>
87 *         Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
88 *         Drafts, Starred, and any user-created folders. For providers that allow nested folders,
89 *         this will only show the folders at the top-level.
90 *         <br /> Tapping on a parent folder creates a new fragment with the child folders at
91 *         that level.
92 *     </li>
93 *     <li>
94 *         Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
95 *         {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
96 *         any folder for a given account.
97 *     </li>
98 * </ul>
99 */
100public class FolderListFragment extends ListFragment implements
101        LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
102    private static final String LOG_TAG = LogTag.getLogTag();
103    /** The parent activity */
104    private ControllableActivity mActivity;
105    /** The underlying list view */
106    private ListView mListView;
107    private ViewGroup mFloatyFooter;
108    /** URI that points to the list of folders for the current account. */
109    private Uri mFolderListUri;
110    /**
111     * True if you want a divided FolderList. A divided folder list shows the following groups:
112     * Inboxes, Recent Folders, All folders.
113     *
114     * An undivided FolderList shows all folders without any divisions and without recent folders.
115     * This is true only for the drawer: for all others it is false.
116     */
117    protected boolean mIsDivided = false;
118    /** True if the folder list belongs to a folder selection activity (one account only) */
119    protected boolean mHideAccounts = true;
120    /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
121    private ArrayList<Integer> mExcludedFolderTypes;
122    /** Object that changes folders on our behalf. */
123    private FolderSelector mFolderChanger;
124    /** Object that changes accounts on our behalf */
125    private AccountController mAccountController;
126
127    /** The currently selected folder (the folder being viewed).  This is never null. */
128    private FolderUri mSelectedFolderUri = FolderUri.EMPTY;
129    /**
130     * The current folder from the controller.  This is meant only to check when the unread count
131     * goes out of sync and fixing it.
132     */
133    private Folder mCurrentFolderForUnreadCheck;
134    /** Parent of the current folder, or null if the current folder is not a child. */
135    private Folder mParentFolder;
136
137    private static final int FOLDER_LIST_LOADER_ID = 0;
138    /** Loader id for the list of all folders in the account */
139    private static final int ALL_FOLDER_LIST_LOADER_ID = 1;
140    /** Key to store {@link #mParentFolder}. */
141    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
142    /** Key to store {@link #mFolderListUri}. */
143    private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
144    /** Key to store {@link #mExcludedFolderTypes} */
145    private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
146
147    private static final String BUNDLE_LIST_STATE = "flf-list-state";
148    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
149    private static final String BUNDLE_SELECTED_ITEM_TYPE = "flf-selected-item-type";
150    private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
151
152    /** Adapter used by the list that wraps both the folder adapter and the accounts adapter. */
153    private MergedAdapter<ListAdapter> mMergedAdapter;
154    /** Adapter containing the list of accounts. */
155    protected AccountsAdapter mAccountsAdapter;
156    /** Adapter containing the list of folders and, optionally, headers and the wait view. */
157    private FolderListFragmentCursorAdapter mFolderAdapter;
158    /** Adapter containing the Help and Feedback views */
159    private FooterAdapter mFooterAdapter;
160    /** Observer to wait for changes to the current folder so we can change the selected folder */
161    private FolderObserver mFolderObserver = null;
162    /** Listen for account changes. */
163    private AccountObserver mAccountObserver = null;
164    /** Listen for account changes. */
165    private DrawerClosedObserver mDrawerObserver = null;
166    /** Listen to changes to list of all accounts */
167    private AllAccountObserver mAllAccountsObserver = null;
168    /**
169     * Type of currently selected folder: {@link DrawerItem#FOLDER_INBOX},
170     * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_OTHER}.
171     * Set as {@link DrawerItem#UNSET} to begin with, as there is nothing selected yet.
172     */
173    private int mSelectedDrawerItemType = DrawerItem.UNSET;
174
175    /** The FolderType of the selected folder {@link FolderType} */
176    private int mSelectedFolderType = FolderType.INBOX;
177    /** The current account according to the controller */
178    protected Account mCurrentAccount;
179    /** The account we will change to once the drawer (if any) is closed */
180    private Account mNextAccount = null;
181    /** The folder we will change to once the drawer (if any) is closed */
182    private Folder mNextFolder = null;
183    /** Watcher for tracking and receiving unread counts for mail */
184    private FolderWatcher mFolderWatcher = null;
185    private boolean mRegistered = false;
186
187    private final DrawerStateListener mDrawerListener = new DrawerStateListener();
188
189    private boolean mShowFooter;
190
191    private boolean mFooterIsAnimating = false;
192
193    private static final Interpolator INTERPOLATOR_SHOW_FLOATY = new DecelerateInterpolator(2.0f);
194    private static final Interpolator INTERPOLATOR_HIDE_FLOATY = new DecelerateInterpolator();
195
196    /**
197     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
198     */
199    public FolderListFragment() {
200        super();
201    }
202
203    @Override
204    public String toString() {
205        final StringBuilder sb = new StringBuilder(super.toString());
206        sb.setLength(sb.length() - 1);
207        sb.append(" folder=");
208        sb.append(mFolderListUri);
209        sb.append(" parent=");
210        sb.append(mParentFolder);
211        sb.append(" adapterCount=");
212        sb.append(mMergedAdapter != null ? mMergedAdapter.getCount() : -1);
213        sb.append("}");
214        return sb.toString();
215    }
216
217    /**
218     * Creates a new instance of {@link FolderListFragment}, initialized
219     * to display the folder and its immediate children.
220     * @param folder parent folder whose children are shown
221     *
222     */
223    public static FolderListFragment ofTree(Folder folder) {
224        final FolderListFragment fragment = new FolderListFragment();
225        fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
226        return fragment;
227    }
228
229    /**
230     * Creates a new instance of {@link FolderListFragment}, initialized
231     * to display the top level: where we have no parent folder, but we have a list of folders
232     * from the account.
233     * @param folderListUri the URI which contains all the list of folders
234     * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
235     */
236    public static FolderListFragment ofTopLevelTree(Uri folderListUri,
237            final ArrayList<Integer> excludedFolderTypes) {
238        final FolderListFragment fragment = new FolderListFragment();
239        fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
240        return fragment;
241    }
242
243    /**
244     * Construct a bundle that represents the state of this fragment.
245     *
246     * @param parentFolder non-null for trees, the parent of this list
247     * @param folderListUri the URI which contains all the list of folders
248     * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
249     * @return Bundle containing parentFolder, divided list boolean and
250     *         excluded folder types
251     */
252    private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
253            final ArrayList<Integer> excludedFolderTypes) {
254        final Bundle args = new Bundle(3);
255        if (parentFolder != null) {
256            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
257        }
258        if (folderListUri != null) {
259            args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
260        }
261        if (excludedFolderTypes != null) {
262            args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
263        }
264        return args;
265    }
266
267    @Override
268    public void onActivityCreated(Bundle savedState) {
269        super.onActivityCreated(savedState);
270        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
271        // only activity creating a ConversationListContext is a MailActivity which is of type
272        // ControllableActivity, so this cast should be safe. If this cast fails, some other
273        // activity is creating ConversationListFragments. This activity must be of type
274        // ControllableActivity.
275        final Activity activity = getActivity();
276        if (!(activity instanceof ControllableActivity)) {
277            LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
278                    "create it. Cannot proceed.");
279            return;
280        }
281        mActivity = (ControllableActivity) activity;
282        final FolderController controller = mActivity.getFolderController();
283        // Listen to folder changes in the future
284        mFolderObserver = new FolderObserver() {
285            @Override
286            public void onChanged(Folder newFolder) {
287                setSelectedFolder(newFolder);
288            }
289        };
290        final Folder currentFolder;
291        if (controller != null) {
292            // Only register for selected folder updates if we have a controller.
293            currentFolder = mFolderObserver.initialize(controller);
294            mCurrentFolderForUnreadCheck = currentFolder;
295        } else {
296            currentFolder = null;
297        }
298
299        // Initialize adapter for folder/hierarchical list.  Note this relies on
300        // mActivity being initialized.
301        final Folder selectedFolder;
302        if (mParentFolder != null) {
303            mFolderAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
304            selectedFolder = mActivity.getHierarchyFolder();
305        } else {
306            mFolderAdapter = new FolderAdapter(mIsDivided);
307            selectedFolder = currentFolder;
308        }
309
310        mAccountsAdapter = newAccountsAdapter();
311        mFooterAdapter = new FooterAdapter();
312        updateFloatyFooter();
313
314        // Is the selected folder fresher than the one we have restored from a bundle?
315        if (selectedFolder != null
316                && !selectedFolder.folderUri.equals(mSelectedFolderUri)) {
317            setSelectedFolder(selectedFolder);
318        }
319
320        // Assign observers for current account & all accounts
321        final AccountController accountController = mActivity.getAccountController();
322        mAccountObserver = new AccountObserver() {
323            @Override
324            public void onChanged(Account newAccount) {
325                setSelectedAccount(newAccount);
326            }
327        };
328        mFolderChanger = mActivity.getFolderSelector();
329        if (accountController != null) {
330            // Current account and its observer.
331            setSelectedAccount(mAccountObserver.initialize(accountController));
332            // List of all accounts and its observer.
333            mAllAccountsObserver = new AllAccountObserver(){
334                @Override
335                public void onChanged(Account[] allAccounts) {
336                    if (!mRegistered && mAccountController != null) {
337                        // TODO(viki): Round-about way of setting the watcher. http://b/8750610
338                        mAccountController.setFolderWatcher(mFolderWatcher);
339                        mRegistered = true;
340                    }
341                    mFolderWatcher.updateAccountList(getAllAccounts());
342                    mAccountsAdapter.rebuildAccountList();
343                }
344            };
345            mAllAccountsObserver.initialize(accountController);
346            mAccountController = accountController;
347
348            // Observer for when the drawer is closed
349            mDrawerObserver = new DrawerClosedObserver() {
350                @Override
351                public void onDrawerClosed() {
352                    // First, check if there's a folder to change to
353                    if (mNextFolder != null) {
354                        mFolderChanger.onFolderSelected(mNextFolder);
355                        mNextFolder = null;
356                    }
357                    // Next, check if there's an account to change to
358                    if (mNextAccount != null) {
359                        mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
360                        mNextAccount = null;
361                    }
362                }
363            };
364            mDrawerObserver.initialize(accountController);
365
366            mActivity.getDrawerController().registerDrawerListener(mDrawerListener);
367        }
368
369        if (mActivity.isFinishing()) {
370            // Activity is finishing, just bail.
371            return;
372        }
373
374        mListView.setChoiceMode(getListViewChoiceMode());
375
376        mMergedAdapter = new MergedAdapter<ListAdapter>();
377        mMergedAdapter.setAdapters(mAccountsAdapter, mFolderAdapter, mFooterAdapter);
378
379        mFolderWatcher = new FolderWatcher(mActivity, mAccountsAdapter);
380        mFolderWatcher.updateAccountList(getAllAccounts());
381
382        setListAdapter(mMergedAdapter);
383    }
384
385    /**
386     * Set the instance variables from the arguments provided here.
387     * @param args bundle of arguments with keys named ARG_*
388     */
389    private void setInstanceFromBundle(Bundle args) {
390        if (args == null) {
391            return;
392        }
393        mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
394        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
395        if (folderUri != null) {
396            mFolderListUri = Uri.parse(folderUri);
397        }
398        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
399    }
400
401    @Override
402    public View onCreateView(LayoutInflater inflater, ViewGroup container,
403            Bundle savedState) {
404        setInstanceFromBundle(getArguments());
405
406        final View rootView = inflater.inflate(R.layout.folder_list, null);
407        final ScrollNotifyingListView lv = (ScrollNotifyingListView) rootView.findViewById(
408                android.R.id.list);
409        mListView = lv;
410        mListView.setEmptyView(null);
411        mListView.setDivider(null);
412        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
413            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
414        }
415        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
416            mSelectedFolderUri =
417                    new FolderUri(Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)));
418            mSelectedDrawerItemType = savedState.getInt(BUNDLE_SELECTED_ITEM_TYPE);
419            mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
420        } else if (mParentFolder != null) {
421            mSelectedFolderUri = mParentFolder.folderUri;
422            // No selected folder type required for hierarchical lists.
423        }
424
425        mShowFooter = getResources().getBoolean(R.bool.show_help_and_feedback_in_drawer);
426
427        final boolean floatyFooterEnabled = mShowFooter && getResources().getBoolean(
428                R.bool.show_drawer_floaty_footer);
429        final ViewGroup ff = (ViewGroup) rootView.findViewById(R.id.floaty_footer);
430        ff.setVisibility(floatyFooterEnabled ? View.VISIBLE : View.GONE);
431        if (floatyFooterEnabled) {
432            mFloatyFooter = ff;
433        }
434
435        final ScrollIndicatorsView scrollbars =
436                (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
437        scrollbars.setSourceView(lv);
438
439        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
440
441            private int mLastState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
442
443            @Override
444            public void onScrollStateChanged(AbsListView view, int scrollState) {
445                mLastState = scrollState;
446            }
447
448            @Override
449            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
450                    int totalItemCount) {
451                // have the floaty footer react only after some non-zero scroll movement
452                if (mLastState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
453                    // ignore onScrolls that are generated as the list data is updated
454                    return;
455                }
456
457                if (mListView.canScrollVertically(-1)) {
458                    // typically we want scroll motion to hide the floaty footer
459                    hideFloatyFooter();
460                } else {
461                    // except at the top, when we should show it
462                    showFloatyFooter(false /* onlyWhenClosed */);
463                }
464            }
465
466        });
467
468        return rootView;
469    }
470
471    @Override
472    public void onStart() {
473        super.onStart();
474    }
475
476    @Override
477    public void onStop() {
478        super.onStop();
479    }
480
481    @Override
482    public void onPause() {
483        super.onPause();
484    }
485
486    @Override
487    public void onSaveInstanceState(Bundle outState) {
488        super.onSaveInstanceState(outState);
489        if (mListView != null) {
490            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
491        }
492        if (mSelectedFolderUri != null) {
493            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
494        }
495        outState.putInt(BUNDLE_SELECTED_ITEM_TYPE, mSelectedDrawerItemType);
496        outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
497    }
498
499    @Override
500    public void onDestroyView() {
501        if (mFolderAdapter != null) {
502            mFolderAdapter.destroy();
503        }
504        // Clear the adapter.
505        setListAdapter(null);
506        if (mFolderObserver != null) {
507            mFolderObserver.unregisterAndDestroy();
508            mFolderObserver = null;
509        }
510        if (mAccountObserver != null) {
511            mAccountObserver.unregisterAndDestroy();
512            mAccountObserver = null;
513        }
514        if (mAllAccountsObserver != null) {
515            mAllAccountsObserver.unregisterAndDestroy();
516            mAllAccountsObserver = null;
517        }
518        if (mDrawerObserver != null) {
519            mDrawerObserver.unregisterAndDestroy();
520            mDrawerObserver = null;
521        }
522        super.onDestroyView();
523
524        if (mActivity != null) {
525            mActivity.getDrawerController().unregisterDrawerListener(mDrawerListener);
526        }
527    }
528
529    @Override
530    public void onListItemClick(ListView l, View v, int position, long id) {
531        viewFolderOrChangeAccount(position);
532    }
533
534    private Folder getDefaultInbox(Account account) {
535        if (account == null || mFolderWatcher == null) {
536            return null;
537        }
538        return mFolderWatcher.getDefaultInbox(account);
539    }
540
541    protected int getUnreadCount(Account account) {
542        if (account == null || mFolderWatcher == null) {
543            return 0;
544        }
545        return mFolderWatcher.getUnreadCount(account);
546    }
547
548    protected void changeAccount(final Account account) {
549        // Switching accounts takes you to the default inbox for that account.
550        mSelectedDrawerItemType = DrawerItem.FOLDER_INBOX;
551        mSelectedFolderType = FolderType.INBOX;
552        mNextAccount = account;
553        mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
554        Analytics.getInstance().sendEvent("switch_account", "drawer_account_switch", null, 0);
555    }
556
557    /**
558     * Display the conversation list from the folder at the position given.
559     * @param position a zero indexed position into the list.
560     */
561    protected void viewFolderOrChangeAccount(int position) {
562        final Object item = getListAdapter().getItem(position);
563        LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
564        final Folder folder;
565        int folderType = DrawerItem.UNSET;
566
567        if (item instanceof DrawerItem) {
568            final DrawerItem drawerItem = (DrawerItem) item;
569            // Could be a folder or account.
570            final int itemType = drawerItem.mType;
571            if (itemType == DrawerItem.VIEW_ACCOUNT) {
572                // Account, so switch.
573                folder = null;
574                onAccountSelected(drawerItem.mAccount);
575            } else if (itemType == DrawerItem.VIEW_FOLDER) {
576                // Folder type, so change folders only.
577                folder = drawerItem.mFolder;
578                mSelectedDrawerItemType = folderType = drawerItem.mFolderType;
579                mSelectedFolderType = folder.type;
580                LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
581                        folder, mSelectedDrawerItemType);
582            } else {
583                // Do nothing.
584                LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
585                        + " Clicked on unset item in drawer. Offending item is " + item);
586                return;
587            }
588        } else if (item instanceof Folder) {
589            folder = (Folder) item;
590        } else if (item instanceof FooterItem) {
591            folder = null;
592            ((FooterItem) item).onClick(null /* unused */);
593        } else {
594            // Don't know how we got here.
595            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
596            folder = null;
597        }
598        if (folder != null) {
599            // Go to the conversation list for this folder.
600            if (!folder.folderUri.equals(mSelectedFolderUri)) {
601                mNextFolder = folder;
602                mAccountController.closeDrawer(true /** hasNewFolderOrAccount */,
603                        null /** nextAccount */,
604                        folder /** nextFolder */);
605
606                final String label = (folderType == DrawerItem.FOLDER_RECENT) ? "recent" : "normal";
607                Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(),
608                        label, 0);
609
610            } else {
611                // Clicked on same folder, just close drawer
612                mAccountController.closeDrawer(false /** hasNewFolderOrAccount */,
613                        null /** nextAccount */,
614                        folder /** nextFolder */);
615            }
616        }
617    }
618
619    protected void onAccountSelected(Account account) {
620        if (account != null && mSelectedFolderUri.equals(account.settings.defaultInbox)) {
621            // We're already in the default inbox for account,
622            // just close the drawer (no new target folders/accounts)
623            mAccountController.closeDrawer(false, mNextAccount,
624                    getDefaultInbox(mNextAccount));
625        } else {
626            changeAccount(account);
627        }
628    }
629
630    @Override
631    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
632        mListView.setEmptyView(null);
633        final Uri folderListUri;
634        if (id == FOLDER_LIST_LOADER_ID) {
635            if (mFolderListUri != null) {
636                // Folder trees, they specify a URI at construction time.
637                folderListUri = mFolderListUri;
638            } else {
639                // Drawers get the folder list from the current account.
640                folderListUri = mCurrentAccount.folderListUri;
641            }
642        } else if (id == ALL_FOLDER_LIST_LOADER_ID) {
643            folderListUri = mCurrentAccount.allFolderListUri;
644        } else {
645            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
646            return null;
647        }
648        return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
649                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
650    }
651
652    @Override
653    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
654        if (mFolderAdapter != null) {
655            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
656                mFolderAdapter.setCursor(data);
657            } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
658                mFolderAdapter.setAllFolderListCursor(data);
659            }
660            // new data means drawer list length may have changed, and the floaty footer visibility
661            // may need to be updated
662            showFloatyFooter(true /* onlyWhenClosed */);
663        }
664    }
665
666    @Override
667    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
668        if (mFolderAdapter != null) {
669            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
670                mFolderAdapter.setCursor(null);
671            } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
672                mFolderAdapter.setAllFolderListCursor(null);
673            }
674        }
675    }
676
677    /**
678     *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
679     *  frequency of use.
680     * @return a list of accounts, sorted by frequency of use
681     */
682    protected Account[] getAllAccounts() {
683        if (mAllAccountsObserver != null) {
684            return mAllAccountsObserver.getAllAccounts();
685        }
686        return new Account[0];
687    }
688
689    protected AccountsAdapter newAccountsAdapter() {
690        return new AccountsAdapter();
691    }
692
693    /**
694     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
695     */
696    private interface FolderListFragmentCursorAdapter extends ListAdapter {
697        /** Update the folder list cursor with the cursor given here. */
698        void setCursor(ObjectCursor<Folder> cursor);
699        /** Update the all folder list cursor with the cursor given here. */
700        void setAllFolderListCursor(ObjectCursor<Folder> cursor);
701        /** Remove all observers and destroy the object. */
702        void destroy();
703        /** Notifies the adapter that the data has changed. */
704        void notifyDataSetChanged();
705    }
706
707    /**
708     * An adapter for flat folder lists.
709     */
710    private class FolderAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
711
712        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
713            @Override
714            public void onChanged() {
715                if (!isCursorInvalid()) {
716                    rebuildFolderList();
717                }
718            }
719        };
720        /** No resource used for string header in folder list */
721        private static final int NO_HEADER_RESOURCE = -1;
722        /** Cache of most recently used folders */
723        private final RecentFolderList mRecentFolders;
724        /** True if the list is divided, false otherwise. See the comment on
725         * {@link FolderListFragment#mIsDivided} for more information */
726        private final boolean mIsDivided;
727        /** All the items */
728        private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
729        /** Cursor into the folder list. This might be null. */
730        private ObjectCursor<Folder> mCursor = null;
731        /** Cursor into the all folder list. This might be null. */
732        private ObjectCursor<Folder> mAllFolderListCursor = null;
733
734        /**
735         * Creates a {@link FolderAdapter}. This is a list of all the accounts and folders.
736         *
737         * @param isDivided true if folder list is flat, false if divided by label group. See
738         *                   the comments on {@link #mIsDivided} for more information
739         */
740        public FolderAdapter(boolean isDivided) {
741            super();
742            mIsDivided = isDivided;
743            final RecentFolderController controller = mActivity.getRecentFolderController();
744            if (controller != null && mIsDivided) {
745                mRecentFolders = mRecentFolderObserver.initialize(controller);
746            } else {
747                mRecentFolders = null;
748            }
749        }
750
751        @Override
752        public View getView(int position, View convertView, ViewGroup parent) {
753            final DrawerItem item = (DrawerItem) getItem(position);
754            final View view = item.getView(convertView, parent);
755            final int type = item.mType;
756            final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedDrawerItemType);
757            if (type == DrawerItem.VIEW_FOLDER) {
758                mListView.setItemChecked(mAccountsAdapter.getCount() + position, isSelected);
759            }
760            // If this is the current folder, also check to verify that the unread count
761            // matches what the action bar shows.
762            if (type == DrawerItem.VIEW_FOLDER
763                    && isSelected
764                    && (mCurrentFolderForUnreadCheck != null)
765                    && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
766                ((FolderItemView) view).overrideUnreadCount(
767                        mCurrentFolderForUnreadCheck.unreadCount);
768            }
769            return view;
770        }
771
772        @Override
773        public int getViewTypeCount() {
774            // Accounts, headers, folders (all parts of drawer view types)
775            return DrawerItem.getViewTypes();
776        }
777
778        @Override
779        public int getItemViewType(int position) {
780            return ((DrawerItem) getItem(position)).mType;
781        }
782
783        @Override
784        public int getCount() {
785            return mItemList.size();
786        }
787
788        @Override
789        public boolean isEnabled(int position) {
790            final DrawerItem drawerItem = ((DrawerItem) getItem(position));
791            return drawerItem != null && drawerItem.isItemEnabled();
792        }
793
794        @Override
795        public boolean areAllItemsEnabled() {
796            // We have headers and thus some items are not enabled.
797            return false;
798        }
799
800        /**
801         * Returns all the recent folders from the list given here. Safe to call with a null list.
802         * @param recentList a list of all recently accessed folders.
803         * @return a valid list of folders, which are all recent folders.
804         */
805        private List<Folder> getRecentFolders(RecentFolderList recentList) {
806            final List<Folder> folderList = new ArrayList<Folder>();
807            if (recentList == null) {
808                return folderList;
809            }
810            // Get all recent folders, after removing system folders.
811            for (final Folder f : recentList.getRecentFolderList(null)) {
812                if (!f.isProviderFolder()) {
813                    folderList.add(f);
814                }
815            }
816            return folderList;
817        }
818
819        /**
820         * Responsible for verifying mCursor, and ensuring any recalculate
821         * conditions are met. Also calls notifyDataSetChanged once it's finished
822         * populating {@link com.android.mail.ui.FolderListFragment.FolderAdapter#mItemList}
823         */
824        private void rebuildFolderList() {
825            mItemList = recalculateListFolders();
826            // Ask the list to invalidate its views.
827            notifyDataSetChanged();
828        }
829
830        /**
831         * Recalculates the system, recent and user label lists.
832         * This method modifies all the three lists on every single invocation.
833         */
834        private List<DrawerItem> recalculateListFolders() {
835            final List<DrawerItem> itemList = new ArrayList<DrawerItem>();
836            // If we are waiting for folder initialization, we don't have any kinds of folders,
837            // just the "Waiting for initialization" item. Note, this should only be done
838            // when we're waiting for account initialization or initial sync.
839            if (isCursorInvalid()) {
840                if(!mCurrentAccount.isAccountReady()) {
841                    itemList.add(DrawerItem.ofWaitView(mActivity));
842                }
843                return itemList;
844            }
845
846            if (!mIsDivided) {
847                // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
848                do {
849                    final Folder f = mCursor.getModel();
850                    if (!isFolderTypeExcluded(f)) {
851                        itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER));
852                    }
853                } while (mCursor.moveToNext());
854
855                return itemList;
856            }
857
858            // Otherwise, this is an adapter for a divided list.
859            final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
860            final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
861            do {
862                final Folder f = mCursor.getModel();
863                if (!isFolderTypeExcluded(f)) {
864                    if (f.isInbox()) {
865                        inboxFolders.add(DrawerItem.ofFolder(
866                                mActivity, f, DrawerItem.FOLDER_INBOX));
867                    } else {
868                        allFoldersList.add(DrawerItem.ofFolder(
869                                mActivity, f, DrawerItem.FOLDER_OTHER));
870                    }
871                }
872            } while (mCursor.moveToNext());
873
874            // If we have the all folder list, verify that the current folder exists
875            boolean currentFolderFound = false;
876            if (mAllFolderListCursor != null) {
877                final String folderName = mSelectedFolderUri.toString();
878                LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);
879
880                if (mAllFolderListCursor.moveToFirst()) {
881                    LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
882                    do {
883                        final Folder f = mAllFolderListCursor.getModel();
884                        if (!isFolderTypeExcluded(f)) {
885                            if (f.folderUri.equals(mSelectedFolderUri)) {
886                                LogUtils.d(LOG_TAG, "Found %s !", folderName);
887                                currentFolderFound = true;
888                            }
889                        }
890                    } while (!currentFolderFound && mAllFolderListCursor.moveToNext());
891                }
892
893                // The search folder will not be found here because it is excluded from the drawer.
894                // Don't switch off from the current folder if it's search.
895                if (!currentFolderFound && !Folder.isType(FolderType.SEARCH, mSelectedFolderType)
896                        && mSelectedFolderUri != FolderUri.EMPTY
897                        && mCurrentAccount != null && mAccountController != null
898                        && mAccountController.isDrawerPullEnabled()) {
899                    LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
900                            folderName, mCurrentAccount.getEmailAddress());
901                    changeAccount(mCurrentAccount);
902                }
903            }
904
905            // Add all inboxes (sectioned Inboxes included) before recent folders.
906            addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
907
908            // Add recent folders next.
909            addRecentsToList(itemList);
910
911            // Add the remaining folders.
912            addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
913
914            return itemList;
915        }
916
917        /**
918         * Given a list of folders as {@link DrawerItem}s, add them as a group.
919         * Passing in a non-0 integer for the resource will enable a header.
920         *
921         * @param destination List of drawer items to populate
922         * @param source List of drawer items representing folders to add to the drawer
923         * @param headerStringResource
924         *            {@link FolderAdapter#NO_HEADER_RESOURCE} if no header
925         *            is required, or res-id otherwise. The integer is interpreted as the string
926         *            for the header's title.
927         */
928        private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
929                int headerStringResource) {
930            if (source.size() > 0) {
931                if(headerStringResource != NO_HEADER_RESOURCE) {
932                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
933                }
934                destination.addAll(source);
935            }
936        }
937
938        /**
939         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
940         *
941         * @param destination List of drawer items to populate
942         */
943        private void addRecentsToList(List<DrawerItem> destination) {
944            // If there are recent folders, add them.
945            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
946
947            // Remove any excluded folder types
948            if (mExcludedFolderTypes != null) {
949                final Iterator<Folder> iterator = recentFolderList.iterator();
950                while (iterator.hasNext()) {
951                    if (isFolderTypeExcluded(iterator.next())) {
952                        iterator.remove();
953                    }
954                }
955            }
956
957            if (recentFolderList.size() > 0) {
958                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
959                // Recent folders are not queried for position.
960                for (Folder f : recentFolderList) {
961                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT));
962                }
963            }
964        }
965
966        /**
967         * Check if the cursor provided is valid.
968         * @return True if cursor is invalid, false otherwise
969         */
970        private boolean isCursorInvalid() {
971            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
972                    || !mCursor.moveToFirst();
973        }
974
975        @Override
976        public void setCursor(ObjectCursor<Folder> cursor) {
977            mCursor = cursor;
978            mAccountsAdapter.rebuildAccountList();
979            rebuildFolderList();
980        }
981
982        @Override
983        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
984            mAllFolderListCursor = cursor;
985            mAccountsAdapter.rebuildAccountList();
986            rebuildFolderList();
987        }
988
989        @Override
990        public Object getItem(int position) {
991            // Is there an attempt made to access outside of the drawer item list?
992            if (position >= mItemList.size()) {
993                return null;
994            } else {
995                return mItemList.get(position);
996            }
997        }
998
999        @Override
1000        public long getItemId(int position) {
1001            return getItem(position).hashCode();
1002        }
1003
1004        @Override
1005        public final void destroy() {
1006            mRecentFolderObserver.unregisterAndDestroy();
1007        }
1008    }
1009
1010    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
1011            implements FolderListFragmentCursorAdapter {
1012
1013        private static final int PARENT = 0;
1014        private static final int CHILD = 1;
1015        private final FolderUri mParentUri;
1016        private final Folder mParent;
1017        private final FolderItemView.DropHandler mDropHandler;
1018
1019        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
1020            super(mActivity.getActivityContext(), R.layout.folder_item);
1021            mDropHandler = mActivity;
1022            mParent = parentFolder;
1023            mParentUri = parentFolder.folderUri;
1024            setCursor(c);
1025        }
1026
1027        @Override
1028        public int getViewTypeCount() {
1029            // Child and Parent
1030            return 2;
1031        }
1032
1033        @Override
1034        public int getItemViewType(int position) {
1035            final Folder f = getItem(position);
1036            return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
1037        }
1038
1039        @Override
1040        public View getView(int position, View convertView, ViewGroup parent) {
1041            final FolderItemView folderItemView;
1042            final Folder folder = getItem(position);
1043            boolean isParent = folder.folderUri.equals(mParentUri);
1044            if (convertView != null) {
1045                folderItemView = (FolderItemView) convertView;
1046            } else {
1047                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
1048                folderItemView = (FolderItemView) LayoutInflater.from(
1049                        mActivity.getActivityContext()).inflate(resId, null);
1050            }
1051            folderItemView.bind(folder, mDropHandler);
1052            if (folder.folderUri.equals(mSelectedFolderUri)) {
1053                getListView().setItemChecked(mAccountsAdapter.getCount() + position, true);
1054                // If this is the current folder, also check to verify that the unread count
1055                // matches what the action bar shows.
1056                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1057                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1058                if (unreadCountDiffers) {
1059                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1060                }
1061            }
1062            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1063            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1064            return folderItemView;
1065        }
1066
1067        @Override
1068        public void setCursor(ObjectCursor<Folder> cursor) {
1069            clear();
1070            if (mParent != null) {
1071                add(mParent);
1072            }
1073            if (cursor != null && cursor.getCount() > 0) {
1074                cursor.moveToFirst();
1075                do {
1076                    add(cursor.getModel());
1077                } while (cursor.moveToNext());
1078            }
1079        }
1080
1081        @Override
1082        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1083            // Not necessary in HierarchicalFolderListAdapter
1084        }
1085
1086        @Override
1087        public void destroy() {
1088            // Do nothing.
1089        }
1090    }
1091
1092    protected class AccountsAdapter extends BaseAdapter {
1093
1094        private List<DrawerItem> mAccounts;
1095
1096        public AccountsAdapter() {
1097            mAccounts = new ArrayList<DrawerItem>();
1098        }
1099
1100        public void rebuildAccountList() {
1101            if (!mHideAccounts) {
1102                mAccounts = buildAccountList();
1103                notifyDataSetChanged();
1104            }
1105        }
1106
1107        /**
1108         * Builds the list of accounts.
1109         */
1110        private List<DrawerItem> buildAccountList() {
1111            final Account[] allAccounts = getAllAccounts();
1112            final List<DrawerItem> accountList = new ArrayList<DrawerItem>(allAccounts.length);
1113            // Add all accounts and then the current account
1114            final Uri currentAccountUri = getCurrentAccountUri();
1115            for (final Account account : allAccounts) {
1116                final int unreadCount = getUnreadCount(account);
1117                accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
1118                        currentAccountUri.equals(account.uri)));
1119            }
1120            if (mCurrentAccount == null) {
1121                LogUtils.wtf(LOG_TAG, "buildAccountList() with null current account.");
1122            }
1123            return accountList;
1124        }
1125
1126        protected Uri getCurrentAccountUri() {
1127            return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
1128        }
1129
1130        @Override
1131        public int getCount() {
1132            return mAccounts.size();
1133        }
1134
1135        @Override
1136        public Object getItem(int position) {
1137            // Is there an attempt made to access outside of the drawer item list?
1138            if (position >= mAccounts.size()) {
1139                return null;
1140            } else {
1141                return mAccounts.get(position);
1142            }
1143        }
1144
1145        @Override
1146        public long getItemId(int position) {
1147            return getItem(position).hashCode();
1148        }
1149
1150        @Override
1151        public View getView(int position, View convertView, ViewGroup parent) {
1152            final DrawerItem item = (DrawerItem) getItem(position);
1153            return item.getView(convertView, parent);
1154        }
1155    }
1156
1157    private class FooterAdapter extends BaseAdapter {
1158
1159        private final List<FooterItem> mFooterItems = Lists.newArrayList();
1160
1161        private FooterAdapter() {
1162            update();
1163        }
1164
1165        @Override
1166        public int getCount() {
1167            return mFooterItems.size();
1168        }
1169
1170        @Override
1171        public Object getItem(int position) {
1172            return mFooterItems.get(position);
1173        }
1174
1175        @Override
1176        public long getItemId(int position) {
1177            return position;
1178        }
1179
1180        /**
1181         * @param convertView a view, possibly null, to be recycled.
1182         * @param parent the parent hosting this view.
1183         * @return a view for the footer item displaying the given text and image.
1184         */
1185        @Override
1186        public View getView(int position, View convertView, ViewGroup parent) {
1187            final ViewGroup footerItemView;
1188            if (convertView != null) {
1189                footerItemView = (ViewGroup) convertView;
1190            } else {
1191                footerItemView = (ViewGroup) getActivity().getLayoutInflater().
1192                        inflate(R.layout.drawer_footer_item, parent, false);
1193            }
1194
1195            final FooterItem item = (FooterItem) getItem(position);
1196
1197            footerItemView.findViewById(R.id.top_border).setVisibility(
1198                    item.shouldShowTopBorder() ? View.VISIBLE : View.GONE);
1199
1200            // adjust the text of the footer item
1201            final TextView textView = (TextView) footerItemView.
1202                    findViewById(R.id.drawer_footer_text);
1203            textView.setText(item.getTextResourceID());
1204
1205            // adjust the icon of the footer item
1206            final ImageView imageView = (ImageView) footerItemView.
1207                    findViewById(R.id.drawer_footer_image);
1208            imageView.setImageResource(item.getImageResourceID());
1209            return footerItemView;
1210        }
1211
1212        /**
1213         * Recomputes the footer drawer items depending on whether the current account
1214         * is populated with URIs that navigate to appropriate destinations.
1215         */
1216        private void update() {
1217            // if the parent activity shows a drawer, these items should participate in that drawer
1218            // (if it shows a *pane* they should *not* participate in that pane)
1219            if (!mShowFooter) {
1220                return;
1221            }
1222
1223            mFooterItems.clear();
1224
1225            if (mCurrentAccount != null) {
1226                mFooterItems.add(new SettingsItem());
1227            }
1228
1229            if (mCurrentAccount != null && !Utils.isEmpty(mCurrentAccount.helpIntentUri)) {
1230                mFooterItems.add(new HelpItem());
1231            }
1232
1233            // if a feedback Uri exists, show the Feedback drawer item
1234            if (mCurrentAccount != null &&
1235                    !Utils.isEmpty(mCurrentAccount.sendFeedbackIntentUri)) {
1236                mFooterItems.add(new FeedbackItem());
1237            }
1238
1239            if (!mFooterItems.isEmpty()) {
1240                mFooterItems.get(0).setShowTopBorder(true);
1241            }
1242
1243            notifyDataSetChanged();
1244        }
1245    }
1246
1247    /**
1248     * Sets the currently selected folder safely.
1249     * @param folder the folder to change to. It is an error to pass null here.
1250     */
1251    private void setSelectedFolder(Folder folder) {
1252        if (folder == null) {
1253            mSelectedFolderUri = FolderUri.EMPTY;
1254            mCurrentFolderForUnreadCheck = null;
1255            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1256            return;
1257        }
1258
1259        final boolean viewChanged =
1260                !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1261
1262        // There are two cases in which the folder type is not set by this class.
1263        // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
1264        //    folder but its type was never set.
1265        // 2. The user backs into the default inbox. Going 'back' from the conversation list of
1266        //    any folder will take you to the default inbox for that account. (If you are in the
1267        //    default inbox already, back exits the app.)
1268        // In both these cases, the selected folder type is not set, and must be set.
1269        if (mSelectedDrawerItemType == DrawerItem.UNSET || (mCurrentAccount != null
1270                && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
1271            mSelectedDrawerItemType =
1272                    folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
1273            mSelectedFolderType = folder.type;
1274        }
1275
1276        mCurrentFolderForUnreadCheck = folder;
1277        mSelectedFolderUri = folder.folderUri;
1278        if (mFolderAdapter != null && viewChanged) {
1279            mFolderAdapter.notifyDataSetChanged();
1280        }
1281    }
1282
1283    /**
1284     * Sets the current account to the one provided here.
1285     * @param account the current account to set to.
1286     */
1287    private void setSelectedAccount(Account account) {
1288        final boolean changed = (account != null) && (mCurrentAccount == null
1289                || !mCurrentAccount.uri.equals(account.uri));
1290        mCurrentAccount = account;
1291        if (changed) {
1292            // Verify that the new account supports sending application feedback
1293            updateFooterItems();
1294            // We no longer have proper folder objects. Let the new ones come in
1295            mFolderAdapter.setCursor(null);
1296            // If currentAccount is different from the one we set, restart the loader. Look at the
1297            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1298            // don't just do restartLoader.
1299            final LoaderManager manager = getLoaderManager();
1300            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1301            manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1302            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1303            manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1304            // An updated cursor causes the entire list to refresh. No need to refresh the list.
1305            // But we do need to blank out the current folder, since the account might not be
1306            // synced.
1307            mSelectedFolderUri = FolderUri.EMPTY;
1308            mCurrentFolderForUnreadCheck = null;
1309        } else if (account == null) {
1310            // This should never happen currently, but is a safeguard against a very incorrect
1311            // non-null account -> null account transition.
1312            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1313            final LoaderManager manager = getLoaderManager();
1314            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1315            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1316        }
1317    }
1318
1319    private void updateFooterItems() {
1320        mFooterAdapter.update();
1321        updateFloatyFooter();
1322    }
1323
1324    private void updateFloatyFooter() {
1325        if (mFloatyFooter == null) {
1326            return;
1327        }
1328
1329        // assuming this isn't often (the caller is debounced), just remake the floaty footer
1330        // from scratch
1331        final ViewGroup container = (ViewGroup) mFloatyFooter.findViewById(
1332                R.id.floaty_footer_items);
1333        container.removeAllViews();
1334
1335        for (int i = 0; i < mFooterAdapter.getCount(); i++) {
1336            final View v = mFooterAdapter.getView(i, null /* convertView */, mFloatyFooter);
1337            // the floaty version has its own top border, so remove the list version's border
1338            if (i == 0) {
1339                v.findViewById(R.id.top_border).setVisibility(View.GONE);
1340            }
1341            v.setOnClickListener((FooterItem) mFooterAdapter.getItem(i));
1342            container.addView(v);
1343        }
1344    }
1345
1346    /**
1347     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1348     */
1349    private boolean isFolderTypeExcluded(final Folder folder) {
1350        if (mExcludedFolderTypes == null) {
1351            return false;
1352        }
1353
1354        for (final int excludedType : mExcludedFolderTypes) {
1355            if (folder.isType(excludedType)) {
1356                return true;
1357            }
1358        }
1359
1360        return false;
1361    }
1362
1363    /**
1364     * @return the choice mode to use for the {@link ListView}
1365     */
1366    protected int getListViewChoiceMode() {
1367        return mAccountController.getFolderListViewChoiceMode();
1368    }
1369
1370    private void showFloatyFooter(boolean onlyWhenClosed) {
1371        // don't ever show if the footer is disabled (in onCreateView)
1372        if (mFloatyFooter == null) {
1373            return;
1374        }
1375
1376        // don't show when onLoadFinished is the reason to show it, and the drawer is open
1377        // (minimize user-visible changes; that case is basically only relevant when closed)
1378        final boolean drawerIsOpen = mActivity.getDrawerController().isDrawerOpen();
1379        if (onlyWhenClosed && drawerIsOpen) {
1380            return;
1381        }
1382
1383        // show the footer, unless if we're already at the very bottom
1384        final int vis = getListView().canScrollVertically(+1) ? View.VISIBLE : View.GONE;
1385
1386        mFloatyFooter.animate().cancel();
1387        if (drawerIsOpen && vis == View.VISIBLE) {
1388            mFloatyFooter.animate()
1389                .translationY(0f)
1390                .setDuration(150)
1391                .setInterpolator(INTERPOLATOR_SHOW_FLOATY)
1392                .setListener(new AnimatorListenerAdapter() {
1393                    @Override
1394                    public void onAnimationStart(Animator animation) {
1395                        mFloatyFooter.setVisibility(vis);
1396                    }
1397                });
1398        } else {
1399            mFloatyFooter.setTranslationY(0f);
1400            mFloatyFooter.setVisibility(vis);
1401        }
1402
1403    }
1404
1405    private void hideFloatyFooter() {
1406        if (mFloatyFooter == null || mFloatyFooter.getVisibility() == View.GONE
1407                || mFooterIsAnimating) {
1408            return;
1409        }
1410        mFooterIsAnimating = true;
1411        mFloatyFooter.animate().cancel();
1412        mFloatyFooter.animate()
1413            .translationY(mFloatyFooter.getHeight())
1414            .setDuration(200)
1415            .setInterpolator(INTERPOLATOR_HIDE_FLOATY)
1416            .setListener(new AnimatorListenerAdapter() {
1417                @Override
1418                public void onAnimationEnd(Animator animation) {
1419                    mFooterIsAnimating = false;
1420                    mFloatyFooter.setVisibility(View.GONE);
1421                }
1422            });
1423    }
1424
1425    /**
1426     * The base class of all footer items. Subclasses must fill in the logic of
1427     * {@link #doFooterAction()} which contains the behavior when the item is selected.
1428     */
1429    private abstract class FooterItem implements View.OnClickListener {
1430
1431        private final int mImageResourceID;
1432        private final int mTextResourceID;
1433
1434        private boolean mShowTopBorder;
1435
1436        private FooterItem(final int imageResourceID, final int textResourceID) {
1437            mImageResourceID = imageResourceID;
1438            mTextResourceID = textResourceID;
1439        }
1440
1441        private int getImageResourceID() {
1442            return mImageResourceID;
1443        }
1444
1445        private int getTextResourceID() {
1446            return mTextResourceID;
1447        }
1448
1449        /**
1450         * Executes the behavior associated with this footer item.<br>
1451         * <br>
1452         * WARNING: you probably don't want to call this directly; use
1453         * {@link #onClick(View)} instead. This method actually performs the action, and its
1454         * execution is deferred from when the 'click' happens so we can smoothly close the drawer
1455         * beforehand.
1456         */
1457        abstract void doFooterAction();
1458
1459        @Override
1460        public final void onClick(View v) {
1461            // close the drawer and defer handling the click until onDrawerClosed
1462            mAccountController.closeDrawer(false /* hasNewFolderOrAccount */,
1463                    null /* nextAccount */, null /* nextFolder */);
1464            mDrawerListener.setPendingFooterClick(this);
1465        }
1466
1467        public boolean shouldShowTopBorder() {
1468            return mShowTopBorder;
1469        }
1470
1471        public void setShowTopBorder(boolean show) {
1472            mShowTopBorder = show;
1473        }
1474
1475    }
1476
1477    private class HelpItem extends FooterItem {
1478        protected HelpItem() {
1479            super(R.drawable.ic_menu_help, R.string.help_and_info);
1480        }
1481
1482        @Override
1483        void doFooterAction() {
1484            Utils.showHelp(getActivity(), mCurrentAccount, mActivity.getHelpContext());
1485        }
1486    }
1487
1488    private class FeedbackItem extends FooterItem {
1489        protected FeedbackItem() {
1490            super(R.drawable.ic_menu_feedback, R.string.feedback);
1491        }
1492
1493        @Override
1494        void doFooterAction() {
1495            Utils.sendFeedback(getActivity(), mCurrentAccount, false);
1496        }
1497    }
1498
1499    private class SettingsItem extends FooterItem {
1500        protected SettingsItem() {
1501            super(R.drawable.ic_menu_settings, R.string.menu_settings);
1502        }
1503
1504        @Override
1505        void doFooterAction() {
1506            Utils.showSettings(mActivity.getActivityContext(), mCurrentAccount);
1507        }
1508    }
1509
1510    /**
1511     * Drawer listener for footer functionality to react to drawer state.
1512     */
1513    private class DrawerStateListener implements DrawerLayout.DrawerListener {
1514
1515        private FooterItem mPendingFooterClick;
1516
1517        public void setPendingFooterClick(FooterItem itemClicked) {
1518            mPendingFooterClick = itemClicked;
1519        }
1520
1521        @Override
1522        public void onDrawerSlide(View drawerView, float slideOffset) {}
1523
1524        @Override
1525        public void onDrawerOpened(View drawerView) {}
1526
1527        @Override
1528        public void onDrawerClosed(View drawerView) {
1529            showFloatyFooter(false /* onlyWhenClosed */);
1530            if (mPendingFooterClick != null) {
1531                mPendingFooterClick.doFooterAction();
1532                mPendingFooterClick = null;
1533            }
1534        }
1535
1536        @Override
1537        public void onDrawerStateChanged(int newState) {}
1538
1539    }
1540}
1541