com.android.mail.ui.FolderListFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.android.mail.ui.FolderListFragment.java

Source

/*
 * Copyright (C) 2012 Google Inc.
 * Licensed to The Android Open Source Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.mail.ui;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.ListFragment;
import android.app.LoaderManager;
import android.content.Loader;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.widget.DrawerLayout;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;

import com.android.bitmap.BitmapCache;
import com.android.bitmap.UnrefedBitmapCache;
import com.android.mail.R;
import com.android.mail.analytics.Analytics;
import com.android.mail.bitmap.AccountAvatarDrawable;
import com.android.mail.bitmap.ContactResolver;
import com.android.mail.browse.MergedAdapter;
import com.android.mail.content.ObjectCursor;
import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.drawer.DrawerItem;
import com.android.mail.drawer.FooterItem;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.AllAccountObserver;
import com.android.mail.providers.Folder;
import com.android.mail.providers.FolderObserver;
import com.android.mail.providers.FolderWatcher;
import com.android.mail.providers.RecentFolderObserver;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.utils.FolderUri;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
 * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
 * in a drawer along with the list of folders.
 *
 * This class has the following use-cases:
 * <ul>
 *     <li>
 *         Show a list of accounts and a divided list of folders. In this case, the list shows
 *         Accounts, Inboxes, Recent Folders, All folders, Help, and Feedback.
 *         Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
 *         folders switches folders. Tapping on Help takes the user to HTML help pages. Tapping on
 *         Feedback takes the user to a screen for submitting text and a screenshot of the
 *         application to a feedback system.
 *         This is created through XML resources as a {@link DrawerFragment}. Since it is created
 *         through resources, it receives all arguments through callbacks.
 *     </li>
 *     <li>
 *         Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
 *         Drafts, Starred, and any user-created folders. For providers that allow nested folders,
 *         this will only show the folders at the top-level.
 *         <br /> Tapping on a parent folder creates a new fragment with the child folders at
 *         that level.
 *     </li>
 *     <li>
 *         Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
 *         {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
 *         any folder for a given account.
 *     </li>
 * </ul>
 */
public class FolderListFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>>, FolderWatcher.UnreadCountChangedListener {
    private static final String LOG_TAG = LogTag.getLogTag();
    // Duration to fade alpha from 0 to 1 and vice versa.
    private static final long DRAWER_FADE_VELOCITY_MS_PER_ALPHA = TwoPaneLayout.SLIDE_DURATION_MS;

    /** The parent activity */
    protected ControllableActivity mActivity;
    /** The underlying list view */
    private ListView mListView;
    /** URI that points to the list of folders for the current account. */
    private Uri mFolderListUri;
    /**
     * True if you want a divided FolderList. A divided folder list shows the following groups:
     * Inboxes, Recent Folders, All folders.
     *
     * An undivided FolderList shows all folders without any divisions and without recent folders.
     * This is true only for the drawer: for all others it is false.
     */
    protected boolean mIsDivided = false;
    /**
     * True if the folder list belongs to a folder selection activity (one account only)
     * and the footer should not show.
     */
    protected boolean mIsFolderSelectionActivity = true;
    /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
    private ArrayList<Integer> mExcludedFolderTypes;
    /** Object that changes folders on our behalf. */
    private FolderSelector mFolderChanger;
    /** Object that changes accounts on our behalf */
    private AccountController mAccountController;
    private DrawerController mDrawerController;

    /** The currently selected folder (the folder being viewed).  This is never null. */
    private FolderUri mSelectedFolderUri = FolderUri.EMPTY;
    /**
     * The current folder from the controller.  This is meant only to check when the unread count
     * goes out of sync and fixing it.
     */
    private Folder mCurrentFolderForUnreadCheck;
    /** Parent of the current folder, or null if the current folder is not a child. */
    private Folder mParentFolder;

    private static final int FOLDER_LIST_LOADER_ID = 0;
    /** Loader id for the list of all folders in the account */
    private static final int ALL_FOLDER_LIST_LOADER_ID = 1;
    /** Key to store {@link #mParentFolder}. */
    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
    /** Key to store {@link #mFolderListUri}. */
    private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
    /** Key to store {@link #mExcludedFolderTypes} */
    private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";

    private static final String BUNDLE_LIST_STATE = "flf-list-state";
    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
    private static final String BUNDLE_SELECTED_ITEM_TYPE = "flf-selected-item-type";
    private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
    private static final String BUNDLE_INBOX_PRESENT = "flf-inbox-present";

    /** Number of avatars to we whould like to fit in the avatar cache */
    private static final int IMAGE_CACHE_COUNT = 10;
    /**
     * This is the fractional portion of the total cache size above that's dedicated to non-pooled
     * bitmaps. (This is basically the portion of cache dedicated to GIFs.)
     */
    private static final float AVATAR_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f;
    /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */
    private static final int AVATAR_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100;

    /** Adapter used by the list that wraps both the folder adapter and the accounts adapter. */
    private MergedAdapter<ListAdapter> mMergedAdapter;
    /** Adapter containing the list of accounts. */
    private AccountsAdapter mAccountsAdapter;
    /** Adapter containing the list of folders and, optionally, headers and the wait view. */
    private FolderListFragmentCursorAdapter mFolderAdapter;
    /** Adapter containing the Help and Feedback views */
    private FooterAdapter mFooterAdapter;
    /** Observer to wait for changes to the current folder so we can change the selected folder */
    private FolderObserver mFolderObserver = null;
    /** Listen for account changes. */
    private AccountObserver mAccountObserver = null;
    /** Listen to changes to selected folder or account */
    private FolderOrAccountListener mFolderOrAccountListener = null;
    /** Listen to changes to list of all accounts */
    private AllAccountObserver mAllAccountsObserver = null;
    /**
     * Type of currently selected folder: {@link DrawerItem#FOLDER_INBOX},
     * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_OTHER}.
     * Set as {@link DrawerItem#UNSET} to begin with, as there is nothing selected yet.
     */
    private int mSelectedDrawerItemCategory = DrawerItem.UNSET;

    /** The FolderType of the selected folder {@link FolderType} */
    private int mSelectedFolderType = FolderType.INBOX;
    /** The current account according to the controller */
    protected Account mCurrentAccount;
    /** The account we will change to once the drawer (if any) is closed */
    private Account mNextAccount = null;
    /** The folder we will change to once the drawer (if any) is closed */
    private Folder mNextFolder = null;
    /** Watcher for tracking and receiving unread counts for mail */
    private FolderWatcher mFolderWatcher = null;
    private boolean mRegistered = false;

    private final DrawerStateListener mDrawerListener = new DrawerStateListener();

    private BitmapCache mImagesCache;
    private ContactResolver mContactResolver;

    private boolean mInboxPresent;

    private boolean mMiniDrawerEnabled;
    private boolean mIsMinimized;
    protected MiniDrawerView mMiniDrawerView;
    private MiniDrawerAccountsAdapter mMiniDrawerAccountsAdapter;
    // use the same dimen as AccountItemView to participate in recycling
    // TODO: but Material account switcher doesn't recycle...
    private int mMiniDrawerAvatarDecodeSize;

    private AnimatorListenerAdapter mMiniDrawerFadeOutListener;
    private AnimatorListenerAdapter mListViewFadeOutListener;
    private AnimatorListenerAdapter mMiniDrawerFadeInListener;
    private AnimatorListenerAdapter mListViewFadeInListener;

    /**
     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
     */
    public FolderListFragment() {
        super();
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder(super.toString());
        sb.setLength(sb.length() - 1);
        sb.append(" folder=");
        sb.append(mFolderListUri);
        sb.append(" parent=");
        sb.append(mParentFolder);
        sb.append(" adapterCount=");
        sb.append(mMergedAdapter != null ? mMergedAdapter.getCount() : -1);
        sb.append("}");
        return sb.toString();
    }

    /**
     * Creates a new instance of {@link FolderListFragment}, initialized
     * to display the folder and its immediate children.
     * @param folder parent folder whose children are shown
     *
     */
    public static FolderListFragment ofTree(Folder folder) {
        final FolderListFragment fragment = new FolderListFragment();
        fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
        return fragment;
    }

    /**
     * Creates a new instance of {@link FolderListFragment}, initialized
     * to display the top level: where we have no parent folder, but we have a list of folders
     * from the account.
     * @param folderListUri the URI which contains all the list of folders
     * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
     */
    public static FolderListFragment ofTopLevelTree(Uri folderListUri,
            final ArrayList<Integer> excludedFolderTypes) {
        final FolderListFragment fragment = new FolderListFragment();
        fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
        return fragment;
    }

    /**
     * Construct a bundle that represents the state of this fragment.
     *
     * @param parentFolder non-null for trees, the parent of this list
     * @param folderListUri the URI which contains all the list of folders
     * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
     * @return Bundle containing parentFolder, divided list boolean and
     *         excluded folder types
     */
    private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
            final ArrayList<Integer> excludedFolderTypes) {
        final Bundle args = new Bundle(3);
        if (parentFolder != null) {
            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
        }
        if (folderListUri != null) {
            args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
        }
        if (excludedFolderTypes != null) {
            args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
        }
        return args;
    }

    @Override
    public void onActivityCreated(Bundle savedState) {
        super.onActivityCreated(savedState);
        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
        // only activity creating a ConversationListContext is a MailActivity which is of type
        // ControllableActivity, so this cast should be safe. If this cast fails, some other
        // activity is creating ConversationListFragments. This activity must be of type
        // ControllableActivity.
        final Activity activity = getActivity();
        if (!(activity instanceof ControllableActivity)) {
            LogUtils.wtf(LOG_TAG,
                    "FolderListFragment expects only a ControllableActivity to" + "create it. Cannot proceed.");
            return;
        }
        mActivity = (ControllableActivity) activity;

        mMiniDrawerAvatarDecodeSize = getResources().getDimensionPixelSize(R.dimen.account_avatar_dimension);

        final int avatarSize = getActivity().getResources().getDimensionPixelSize(R.dimen.account_avatar_dimension);

        mImagesCache = new UnrefedBitmapCache(
                Utils.isLowRamDevice(getActivity()) ? 0 : avatarSize * avatarSize * IMAGE_CACHE_COUNT,
                AVATAR_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION, AVATAR_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY);
        mContactResolver = new ContactResolver(getActivity().getContentResolver(), mImagesCache);

        if (mMiniDrawerEnabled) {
            setupMiniDrawerAccountsAdapter();
            mMiniDrawerView.setController(this);
            // set up initial state
            setMinimized(isMinimized());
        } else {
            mMiniDrawerView.setVisibility(View.GONE);
        }

        final FolderController controller = mActivity.getFolderController();
        // Listen to folder changes in the future
        mFolderObserver = new FolderObserver() {
            @Override
            public void onChanged(Folder newFolder) {
                setSelectedFolder(newFolder);
            }
        };
        final Folder currentFolder;
        if (controller != null) {
            // Only register for selected folder updates if we have a controller.
            currentFolder = mFolderObserver.initialize(controller);
            mCurrentFolderForUnreadCheck = currentFolder;
        } else {
            currentFolder = null;
        }

        // Initialize adapter for folder/hierarchical list.  Note this relies on
        // mActivity being initialized.
        final Folder selectedFolder;
        if (mParentFolder != null) {
            mFolderAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
            selectedFolder = mActivity.getHierarchyFolder();
        } else {
            mFolderAdapter = new FolderAdapter(mIsDivided);
            selectedFolder = currentFolder;
        }

        mAccountsAdapter = newAccountsAdapter();
        mFooterAdapter = new FooterAdapter();

        // Is the selected folder fresher than the one we have restored from a bundle?
        if (selectedFolder != null && !selectedFolder.folderUri.equals(mSelectedFolderUri)) {
            setSelectedFolder(selectedFolder);
        }

        // Assign observers for current account & all accounts
        final AccountController accountController = mActivity.getAccountController();
        mAccountObserver = new AccountObserver() {
            @Override
            public void onChanged(Account newAccount) {
                setSelectedAccount(newAccount);
            }
        };
        mFolderChanger = mActivity.getFolderSelector();
        if (accountController != null) {
            mAccountController = accountController;
            // Current account and its observer.
            setSelectedAccount(mAccountObserver.initialize(accountController));
            // List of all accounts and its observer.
            mAllAccountsObserver = new AllAccountObserver() {
                @Override
                public void onChanged(Account[] allAccounts) {
                    if (!mRegistered && mAccountController != null) {
                        // TODO(viki): Round-about way of setting the watcher. http://b/8750610
                        mAccountController.setFolderWatcher(mFolderWatcher);
                        mRegistered = true;
                    }
                    mFolderWatcher.updateAccountList(getAllAccounts());
                    rebuildAccountList();
                }
            };
            mAllAccountsObserver.initialize(accountController);

            mFolderOrAccountListener = new FolderOrAccountListener();
            mAccountController.registerFolderOrAccountChangedObserver(mFolderOrAccountListener);

            final DrawerController dc = mActivity.getDrawerController();
            if (dc != null) {
                dc.registerDrawerListener(mDrawerListener);
            }
        }

        mDrawerController = mActivity.getDrawerController();

        if (mActivity.isFinishing()) {
            // Activity is finishing, just bail.
            return;
        }

        mListView.setChoiceMode(getListViewChoiceMode());

        mMergedAdapter = new MergedAdapter<>();
        if (mAccountsAdapter != null) {
            mMergedAdapter.setAdapters(mAccountsAdapter, mFolderAdapter, mFooterAdapter);
        } else {
            mMergedAdapter.setAdapters(mFolderAdapter, mFooterAdapter);
        }

        mFolderWatcher = new FolderWatcher(mActivity, this);
        mFolderWatcher.updateAccountList(getAllAccounts());

        setListAdapter(mMergedAdapter);
    }

    public BitmapCache getBitmapCache() {
        return mImagesCache;
    }

    public ContactResolver getContactResolver() {
        return mContactResolver;
    }

    public void toggleDrawerState() {
        if (mDrawerController != null) {
            mDrawerController.toggleDrawerState();
        }
    }

    /**
     * Set the instance variables from the arguments provided here.
     * @param args bundle of arguments with keys named ARG_*
     */
    private void setInstanceFromBundle(Bundle args) {
        if (args == null) {
            return;
        }
        mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
        if (folderUri != null) {
            mFolderListUri = Uri.parse(folderUri);
        }
        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
        setInstanceFromBundle(getArguments());

        final View rootView = inflater.inflate(R.layout.folder_list, container, false);
        mListView = (ListView) rootView.findViewById(android.R.id.list);
        mListView.setEmptyView(null);
        mListView.setDivider(null);
        addListHeader(inflater, rootView, mListView);
        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
        }
        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
            mSelectedFolderUri = new FolderUri(Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)));
            mSelectedDrawerItemCategory = savedState.getInt(BUNDLE_SELECTED_ITEM_TYPE);
            mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
        } else if (mParentFolder != null) {
            mSelectedFolderUri = mParentFolder.folderUri;
            // No selected folder type required for hierarchical lists.
        }
        if (savedState != null) {
            mInboxPresent = savedState.getBoolean(BUNDLE_INBOX_PRESENT, true);
        } else {
            mInboxPresent = true;
        }

        mMiniDrawerView = (MiniDrawerView) rootView.findViewById(R.id.mini_drawer);

        // Create default animator listeners
        mMiniDrawerFadeOutListener = new FadeAnimatorListener(mMiniDrawerView, true /* fadeOut */);
        mListViewFadeOutListener = new FadeAnimatorListener(mListView, true /* fadeOut */);
        mMiniDrawerFadeInListener = new FadeAnimatorListener(mMiniDrawerView, false /* fadeOut */);
        mListViewFadeInListener = new FadeAnimatorListener(mListView, false /* fadeOut */);

        return rootView;
    }

    protected void addListHeader(LayoutInflater inflater, View rootView, ListView list) {
        // Default impl does nothing
    }

    @Override
    public void onStart() {
        super.onStart();
    }

    @Override
    public void onStop() {
        super.onStop();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mListView != null) {
            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
        }
        if (mSelectedFolderUri != null) {
            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
        }
        outState.putInt(BUNDLE_SELECTED_ITEM_TYPE, mSelectedDrawerItemCategory);
        outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
        outState.putBoolean(BUNDLE_INBOX_PRESENT, mInboxPresent);
    }

    @Override
    public void onDestroyView() {
        if (mFolderAdapter != null) {
            mFolderAdapter.destroy();
        }
        // Clear the adapter.
        setListAdapter(null);
        if (mFolderObserver != null) {
            mFolderObserver.unregisterAndDestroy();
            mFolderObserver = null;
        }
        if (mAccountObserver != null) {
            mAccountObserver.unregisterAndDestroy();
            mAccountObserver = null;
        }
        if (mAllAccountsObserver != null) {
            mAllAccountsObserver.unregisterAndDestroy();
            mAllAccountsObserver = null;
        }
        if (mFolderOrAccountListener != null && mAccountController != null) {
            mAccountController.unregisterFolderOrAccountChangedObserver(mFolderOrAccountListener);
            mFolderOrAccountListener = null;
        }
        super.onDestroyView();

        if (mActivity != null) {
            final DrawerController dc = mActivity.getDrawerController();
            if (dc != null) {
                dc.unregisterDrawerListener(mDrawerListener);
            }
        }
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        viewFolderOrChangeAccount(position);
    }

    private Folder getDefaultInbox(Account account) {
        if (account == null || mFolderWatcher == null) {
            return null;
        }
        return mFolderWatcher.getDefaultInbox(account);
    }

    protected int getUnreadCount(Account account) {
        if (account == null || mFolderWatcher == null) {
            return 0;
        }
        return mFolderWatcher.getUnreadCount(account);
    }

    protected void changeAccount(final Account account) {
        // Switching accounts takes you to the default inbox for that account.
        mSelectedDrawerItemCategory = DrawerItem.FOLDER_INBOX;
        mSelectedFolderType = FolderType.INBOX;
        mNextAccount = account;
        mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
        Analytics.getInstance().sendEvent("switch_account", "drawer_account_switch", null, 0);
    }

    /**
     * Display the conversation list from the folder at the position given.
     * @param position a zero indexed position into the list.
     */
    protected void viewFolderOrChangeAccount(int position) {
        // Get the ListView's adapter
        final Object item = getListView().getAdapter().getItem(position);
        LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
        final Folder folder;
        @DrawerItem.DrawerItemCategory
        int itemCategory = DrawerItem.UNSET;

        if (item instanceof DrawerItem) {
            final DrawerItem drawerItem = (DrawerItem) item;
            // Could be a folder or account or footer
            final @DrawerItem.DrawerItemType int itemType = drawerItem.getType();
            if (itemType == DrawerItem.VIEW_ACCOUNT) {
                // Account, so switch.
                folder = null;
                onAccountSelected(drawerItem.mAccount);
            } else if (itemType == DrawerItem.VIEW_FOLDER) {
                // Folder type, so change folders only.
                folder = drawerItem.mFolder;
                mSelectedDrawerItemCategory = itemCategory = drawerItem.mItemCategory;
                mSelectedFolderType = folder.type;
                LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d", folder,
                        mSelectedDrawerItemCategory);
            } else if (itemType == DrawerItem.VIEW_FOOTER_HELP || itemType == DrawerItem.VIEW_FOOTER_SETTINGS) {
                folder = null;
                drawerItem.onClick(null /* unused */);
            } else {
                // Do nothing.
                LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
                        + " Clicked on unset item in drawer. Offending item is " + item);
                return;
            }
        } else if (item instanceof Folder) {
            folder = (Folder) item;
        } else {
            // Don't know how we got here.
            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
            folder = null;
        }
        if (folder != null) {
            final String label = (itemCategory == DrawerItem.FOLDER_RECENT) ? "recent" : "normal";
            onFolderSelected(folder, label);
        }
    }

    public void onFolderSelected(Folder folder, String analyticsLabel) {
        // Go to the conversation list for this folder.
        if (!folder.folderUri.equals(mSelectedFolderUri)) {
            mNextFolder = folder;
            mAccountController.closeDrawer(true /** hasNewFolderOrAccount */
                    , null /** nextAccount */
                    , folder /** nextFolder */
            );

            Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(), analyticsLabel, 0);

        } else {
            // Clicked on same folder, just close drawer
            mAccountController.closeDrawer(false /** hasNewFolderOrAccount */
                    , null /** nextAccount */
                    , folder /** nextFolder */
            );
        }
    }

    public void onAccountSelected(Account account) {
        // Only reset the cache if the account has changed.
        if (mCurrentAccount == null || account == null
                || !mCurrentAccount.getEmailAddress().equals(account.getEmailAddress())) {
            mActivity.resetSenderImageCache();
        }

        if (account != null && mSelectedFolderUri.equals(account.settings.defaultInbox)) {
            // We're already in the default inbox for account,
            // just close the drawer (no new target folders/accounts)
            mAccountController.closeDrawer(false, mNextAccount, getDefaultInbox(mNextAccount));
        } else {
            changeAccount(account);
        }
    }

    @Override
    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
        final Uri folderListUri;
        if (id == FOLDER_LIST_LOADER_ID) {
            if (mFolderListUri != null) {
                // Folder trees, they specify a URI at construction time.
                folderListUri = mFolderListUri;
            } else {
                // Drawers get the folder list from the current account.
                folderListUri = mCurrentAccount.folderListUri;
            }
        } else if (id == ALL_FOLDER_LIST_LOADER_ID) {
            folderListUri = mCurrentAccount.allFolderListUri;
        } else {
            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
            return null;
        }
        return new ObjectCursorLoader<>(mActivity.getActivityContext(), folderListUri,
                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
    }

    @Override
    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
        if (mFolderAdapter != null) {
            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
                mFolderAdapter.setCursor(data);

                if (mMiniDrawerEnabled) {
                    mMiniDrawerView.refresh();
                }

            } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
                mFolderAdapter.setAllFolderListCursor(data);
            }
        }
    }

    @Override
    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
        if (mFolderAdapter != null) {
            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
                mFolderAdapter.setCursor(null);
            } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
                mFolderAdapter.setAllFolderListCursor(null);
            }
        }
    }

    /**
     *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
     *  frequency of use.
     * @return a list of accounts, sorted by frequency of use
     */
    public Account[] getAllAccounts() {
        if (mAllAccountsObserver != null) {
            return mAllAccountsObserver.getAllAccounts();
        }
        return new Account[0];
    }

    protected AccountsAdapter newAccountsAdapter() {
        return new AccountsAdapter();
    }

    @Override
    public void onUnreadCountChange() {
        if (mAccountsAdapter != null) {
            mAccountsAdapter.notifyDataSetChanged();
        }
    }

    public boolean isMiniDrawerEnabled() {
        return mMiniDrawerEnabled;
    }

    public void setMiniDrawerEnabled(boolean enabled) {
        mMiniDrawerEnabled = enabled;
        setMinimized(isMinimized()); // init visual state
    }

    public boolean isMinimized() {
        return mMiniDrawerEnabled && mIsMinimized;
    }

    public void setMinimized(boolean minimized) {
        if (!mMiniDrawerEnabled) {
            return;
        }

        mIsMinimized = minimized;

        if (isMinimized()) {
            mMiniDrawerView.setVisibility(View.VISIBLE);
            mMiniDrawerView.setAlpha(1f);
            mListView.setVisibility(View.INVISIBLE);
            mListView.setAlpha(0f);
        } else {
            mMiniDrawerView.setVisibility(View.INVISIBLE);
            mMiniDrawerView.setAlpha(0f);
            mListView.setVisibility(View.VISIBLE);
            mListView.setAlpha(1f);
        }
    }

    public void animateMinimized(boolean minimized) {
        if (!mMiniDrawerEnabled) {
            return;
        }

        mIsMinimized = minimized;

        Utils.enableHardwareLayer(mMiniDrawerView);
        Utils.enableHardwareLayer(mListView);
        if (mIsMinimized) {
            // From the current state (either maximized or partially dragged) to minimized.
            final float startAlpha = mListView.getAlpha();
            final long duration = (long) (startAlpha * DRAWER_FADE_VELOCITY_MS_PER_ALPHA);
            mMiniDrawerView.setVisibility(View.VISIBLE);

            // Animate the mini-drawer to fade in.
            mMiniDrawerView.animate().alpha(1f).setDuration(duration).setListener(mMiniDrawerFadeInListener);
            // Animate the list view to fade out.
            mListView.animate().alpha(0f).setDuration(duration).setListener(mListViewFadeOutListener);
        } else {
            // From the current state (either minimized or partially dragged) to maximized.
            final float startAlpha = mMiniDrawerView.getAlpha();
            final long duration = (long) (startAlpha * DRAWER_FADE_VELOCITY_MS_PER_ALPHA);
            mListView.setVisibility(View.VISIBLE);
            mListView.requestFocus();

            // Animate the mini-drawer to fade out.
            mMiniDrawerView.animate().alpha(0f).setDuration(duration).setListener(mMiniDrawerFadeOutListener);
            // Animate the list view to fade in.
            mListView.animate().alpha(1f).setDuration(duration).setListener(mListViewFadeInListener);
        }
    }

    public void onDrawerDragStarted() {
        Utils.enableHardwareLayer(mMiniDrawerView);
        Utils.enableHardwareLayer(mListView);
        // The drawer drag will always end with animating the drawers to their final states, so
        // the animation will remove the hardware layer upon completion.
    }

    public void onDrawerDrag(float percent) {
        mMiniDrawerView.setAlpha(1f - percent);
        mListView.setAlpha(percent);
        mMiniDrawerView.setVisibility(View.VISIBLE);
        mListView.setVisibility(View.VISIBLE);
    }

    /**
     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
     */
    private interface FolderListFragmentCursorAdapter extends ListAdapter {
        /** Update the folder list cursor with the cursor given here. */
        void setCursor(ObjectCursor<Folder> cursor);

        ObjectCursor<Folder> getCursor();

        /** Update the all folder list cursor with the cursor given here. */
        void setAllFolderListCursor(ObjectCursor<Folder> cursor);

        /** Remove all observers and destroy the object. */
        void destroy();

        /** Notifies the adapter that the data has changed. */
        void notifyDataSetChanged();
    }

    /**
     * An adapter for flat folder lists.
     */
    private class FolderAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {

        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
            @Override
            public void onChanged() {
                if (!isCursorInvalid()) {
                    rebuildFolderList();
                }
            }
        };
        /** No resource used for string header in folder list */
        private static final int BLANK_HEADER_RESOURCE = -1;
        /** Cache of most recently used folders */
        private final RecentFolderList mRecentFolders;
        /** True if the list is divided, false otherwise. See the comment on
         * {@link FolderListFragment#mIsDivided} for more information */
        private final boolean mIsDivided;
        /** All the items */
        private List<DrawerItem> mItemList = new ArrayList<>();
        /** Cursor into the folder list. This might be null. */
        private ObjectCursor<Folder> mCursor = null;
        /** Cursor into the all folder list. This might be null. */
        private ObjectCursor<Folder> mAllFolderListCursor = null;

        /**
         * Creates a {@link FolderAdapter}. This is a list of all the accounts and folders.
         *
         * @param isDivided true if folder list is flat, false if divided by label group. See
         *                   the comments on {@link #mIsDivided} for more information
         */
        public FolderAdapter(boolean isDivided) {
            super();
            mIsDivided = isDivided;
            final RecentFolderController controller = mActivity.getRecentFolderController();
            if (controller != null && mIsDivided) {
                mRecentFolders = mRecentFolderObserver.initialize(controller);
            } else {
                mRecentFolders = null;
            }
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final DrawerItem item = (DrawerItem) getItem(position);
            final View view = item.getView(convertView, parent);
            final @DrawerItem.DrawerItemType int type = item.getType();
            final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedDrawerItemCategory);
            if (type == DrawerItem.VIEW_FOLDER) {
                mListView.setItemChecked((mAccountsAdapter != null ? mAccountsAdapter.getCount() : 0) + position
                        + mListView.getHeaderViewsCount(), isSelected);
            }
            // If this is the current folder, also check to verify that the unread count
            // matches what the action bar shows.
            if (type == DrawerItem.VIEW_FOLDER && isSelected && (mCurrentFolderForUnreadCheck != null)
                    && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
                ((FolderItemView) view).overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
            }
            return view;
        }

        @Override
        public int getViewTypeCount() {
            // Accounts, headers, folders (all parts of drawer view types)
            return DrawerItem.getViewTypeCount();
        }

        @Override
        public int getItemViewType(int position) {
            return ((DrawerItem) getItem(position)).getType();
        }

        @Override
        public int getCount() {
            return mItemList.size();
        }

        @Override
        public boolean isEnabled(int position) {
            final DrawerItem drawerItem = ((DrawerItem) getItem(position));
            return drawerItem != null && drawerItem.isItemEnabled();
        }

        @Override
        public boolean areAllItemsEnabled() {
            // We have headers and thus some items are not enabled.
            return false;
        }

        /**
         * Returns all the recent folders from the list given here. Safe to call with a null list.
         * @param recentList a list of all recently accessed folders.
         * @return a valid list of folders, which are all recent folders.
         */
        private List<Folder> getRecentFolders(RecentFolderList recentList) {
            final List<Folder> folderList = new ArrayList<>();
            if (recentList == null) {
                return folderList;
            }
            // Get all recent folders, after removing system folders.
            for (final Folder f : recentList.getRecentFolderList(null)) {
                if (!f.isProviderFolder()) {
                    folderList.add(f);
                }
            }
            return folderList;
        }

        /**
         * Responsible for verifying mCursor, and ensuring any recalculate
         * conditions are met. Also calls notifyDataSetChanged once it's finished
         * populating {@link com.android.mail.ui.FolderListFragment.FolderAdapter#mItemList}
         */
        private void rebuildFolderList() {
            final boolean oldInboxPresent = mInboxPresent;
            mItemList = recalculateListFolders();
            if (mAccountController != null && mInboxPresent && !oldInboxPresent) {
                // We didn't have an inbox folder before, but now we do. This can occur when
                // setting up a new account. We automatically create the "starred" virtual
                // virtual folder, but we won't create the inbox until it gets synced.
                // This means that we'll start out looking at the "starred" folder, and the
                // user will need to manually switch to the inbox. See b/13793316
                mAccountController.switchToDefaultInboxOrChangeAccount(mCurrentAccount);
            }
            // Ask the list to invalidate its views.
            notifyDataSetChanged();
        }

        /**
         * Recalculates the system, recent and user label lists.
         * This method modifies all the three lists on every single invocation.
         */
        private List<DrawerItem> recalculateListFolders() {
            final List<DrawerItem> itemList = new ArrayList<>();
            // If we are waiting for folder initialization, we don't have any kinds of folders,
            // just the "Waiting for initialization" item. Note, this should only be done
            // when we're waiting for account initialization or initial sync.
            if (isCursorInvalid()) {
                if (!mCurrentAccount.isAccountReady()) {
                    itemList.add(DrawerItem.ofWaitView(mActivity));
                }
                return itemList;
            }
            if (mIsDivided) {
                //Choose an adapter for a divided list with sections
                return recalculateDividedListFolders(itemList);
            } else {
                // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
                return recalculateFlatListFolders(itemList);
            }
        }

        // Recalculate folder list intended to be flat (no hearders or sections shown).
        // This is commonly used for the widget or other simple folder selections
        private List<DrawerItem> recalculateFlatListFolders(List<DrawerItem> itemList) {
            final List<DrawerItem> inboxFolders = new ArrayList<>();
            final List<DrawerItem> allFoldersList = new ArrayList<>();
            do {
                final Folder f = mCursor.getModel();
                if (!isFolderTypeExcluded(f)) {
                    // Prioritize inboxes
                    if (f.isInbox()) {
                        inboxFolders.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER));
                    } else {
                        allFoldersList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER));
                    }
                }
            } while (mCursor.moveToNext());
            itemList.addAll(inboxFolders);
            itemList.addAll(allFoldersList);
            return itemList;
        }

        // Recalculate folder list divided by sections (inboxes, recents, all, etc...)
        // This is primarily used by the drawer
        private List<DrawerItem> recalculateDividedListFolders(List<DrawerItem> itemList) {
            final List<DrawerItem> allFoldersList = new ArrayList<>();
            final List<DrawerItem> inboxFolders = new ArrayList<>();
            do {
                final Folder f = mCursor.getModel();
                if (!isFolderTypeExcluded(f)) {
                    if (f.isInbox()) {
                        inboxFolders.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_INBOX));
                    } else {
                        allFoldersList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER));
                    }
                }
            } while (mCursor.moveToNext());

            // If we have the all folder list, verify that the current folder exists
            boolean currentFolderFound = false;
            if (mAllFolderListCursor != null) {
                final String folderName = mSelectedFolderUri.toString();
                LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);

                if (mAllFolderListCursor.moveToFirst()) {
                    LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
                    do {
                        final Folder f = mAllFolderListCursor.getModel();
                        if (!isFolderTypeExcluded(f)) {
                            if (f.folderUri.equals(mSelectedFolderUri)) {
                                LogUtils.d(LOG_TAG, "Found %s !", folderName);
                                currentFolderFound = true;
                            }
                        }
                    } while (!currentFolderFound && mAllFolderListCursor.moveToNext());
                }

                // The search folder will not be found here because it is excluded from the drawer.
                // Don't switch off from the current folder if it's search.
                if (!currentFolderFound && !Folder.isType(FolderType.SEARCH, mSelectedFolderType)
                        && mSelectedFolderUri != FolderUri.EMPTY && mCurrentAccount != null
                        && mAccountController != null && mAccountController.isDrawerPullEnabled()) {
                    LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s", folderName,
                            mCurrentAccount.getEmailAddress());
                    changeAccount(mCurrentAccount);
                }
            }

            mInboxPresent = (inboxFolders.size() > 0);

            // Add all inboxes (sectioned Inboxes included) before recent folders.
            addFolderDivision(itemList, inboxFolders, BLANK_HEADER_RESOURCE);

            // Add recent folders next.
            addRecentsToList(itemList);

            // Add the remaining folders.
            addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);

            return itemList;
        }

        /**
         * Given a list of folders as {@link DrawerItem}s, add them as a group.
         * Passing in a non-0 integer for the resource will enable a header.
         *
         * @param destination List of drawer items to populate
         * @param source List of drawer items representing folders to add to the drawer
         * @param headerStringResource
         *            {@link FolderAdapter#BLANK_HEADER_RESOURCE} if no header text
         *            is required, or res-id otherwise. The integer is interpreted as the string
         *            for the header's title.
         */
        private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
                int headerStringResource) {
            if (source.size() > 0) {
                if (headerStringResource != BLANK_HEADER_RESOURCE) {
                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
                } else {
                    destination.add(DrawerItem.ofBlankHeader(mActivity));
                }
                destination.addAll(source);
            }
        }

        /**
         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
         *
         * @param destination List of drawer items to populate
         */
        private void addRecentsToList(List<DrawerItem> destination) {
            // If there are recent folders, add them.
            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);

            // Remove any excluded folder types
            if (mExcludedFolderTypes != null) {
                final Iterator<Folder> iterator = recentFolderList.iterator();
                while (iterator.hasNext()) {
                    if (isFolderTypeExcluded(iterator.next())) {
                        iterator.remove();
                    }
                }
            }

            if (recentFolderList.size() > 0) {
                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
                // Recent folders are not queried for position.
                for (Folder f : recentFolderList) {
                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT));
                }
            }
        }

        /**
         * Check if the cursor provided is valid.
         * @return True if cursor is invalid, false otherwise
         */
        private boolean isCursorInvalid() {
            return mCursor == null || mCursor.isClosed() || mCursor.getCount() <= 0 || !mCursor.moveToFirst();
        }

        @Override
        public void setCursor(ObjectCursor<Folder> cursor) {
            mCursor = cursor;
            rebuildAccountList();
            rebuildFolderList();
        }

        @Override
        public ObjectCursor<Folder> getCursor() {
            return mCursor;
        }

        @Override
        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
            mAllFolderListCursor = cursor;
            rebuildAccountList();
            rebuildFolderList();
        }

        @Override
        public Object getItem(int position) {
            // Is there an attempt made to access outside of the drawer item list?
            if (position >= mItemList.size()) {
                return null;
            } else {
                return mItemList.get(position);
            }
        }

        @Override
        public long getItemId(int position) {
            return getItem(position).hashCode();
        }

        @Override
        public final void destroy() {
            mRecentFolderObserver.unregisterAndDestroy();
        }
    }

    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
            implements FolderListFragmentCursorAdapter {

        private static final int PARENT = 0;
        private static final int CHILD = 1;
        private final FolderUri mParentUri;
        private final Folder mParent;

        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
            super(mActivity.getActivityContext(), R.layout.folder_item);
            mParent = parentFolder;
            mParentUri = parentFolder.folderUri;
            setCursor(c);
        }

        @Override
        public int getViewTypeCount() {
            // Child and Parent
            return 2;
        }

        @Override
        public int getItemViewType(int position) {
            final Folder f = getItem(position);
            return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final FolderItemView folderItemView;
            final Folder folder = getItem(position);

            if (convertView != null) {
                folderItemView = (FolderItemView) convertView;
            } else {
                folderItemView = (FolderItemView) LayoutInflater.from(mActivity.getActivityContext())
                        .inflate(R.layout.folder_item, null);
            }
            folderItemView.bind(folder, mParentUri);

            if (folder.folderUri.equals(mSelectedFolderUri)) {
                final ListView listView = getListView();
                listView.setItemChecked((mAccountsAdapter != null ? mAccountsAdapter.getCount() : 0) + position
                        + listView.getHeaderViewsCount(), true);
                // If this is the current folder, also check to verify that the unread count
                // matches what the action bar shows.
                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
                if (unreadCountDiffers) {
                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
                }
            }
            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
            return folderItemView;
        }

        @Override
        public void setCursor(ObjectCursor<Folder> cursor) {
            clear();
            if (mParent != null) {
                add(mParent);
            }
            if (cursor != null && cursor.getCount() > 0) {
                cursor.moveToFirst();
                do {
                    add(cursor.getModel());
                } while (cursor.moveToNext());
            }
        }

        @Override
        public ObjectCursor<Folder> getCursor() {
            throw new UnsupportedOperationException("drawers don't have hierarchical folders");
        }

        @Override
        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
            // Not necessary in HierarchicalFolderListAdapter
        }

        @Override
        public void destroy() {
            // Do nothing.
        }
    }

    public void rebuildAccountList() {
        if (!mIsFolderSelectionActivity) {
            if (mAccountsAdapter != null) {
                mAccountsAdapter.setAccounts(buildAccountListDrawerItems());
            }
            if (mMiniDrawerAccountsAdapter != null) {
                mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount);
            }
        }
    }

    protected static class AccountsAdapter extends BaseAdapter {

        private List<DrawerItem> mAccounts;

        public AccountsAdapter() {
            mAccounts = new ArrayList<>();
        }

        public void setAccounts(List<DrawerItem> accounts) {
            mAccounts = accounts;
            notifyDataSetChanged();
        }

        @Override
        public int getCount() {
            return mAccounts.size();
        }

        @Override
        public Object getItem(int position) {
            // Is there an attempt made to access outside of the drawer item list?
            if (position >= mAccounts.size()) {
                return null;
            } else {
                return mAccounts.get(position);
            }
        }

        @Override
        public long getItemId(int position) {
            return getItem(position).hashCode();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final DrawerItem item = (DrawerItem) getItem(position);
            return item.getView(convertView, parent);
        }
    }

    /**
     * Builds the drawer items for the list of accounts.
     */
    private List<DrawerItem> buildAccountListDrawerItems() {
        final Account[] allAccounts = getAllAccounts();
        final List<DrawerItem> accountList = new ArrayList<>(allAccounts.length);
        // Add all accounts and then the current account
        final Uri currentAccountUri = getCurrentAccountUri();
        for (final Account account : allAccounts) {
            final int unreadCount = getUnreadCount(account);
            accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
                    currentAccountUri.equals(account.uri), mImagesCache, mContactResolver));
        }
        if (mCurrentAccount == null) {
            LogUtils.wtf(LOG_TAG, "buildAccountListDrawerItems() with null current account.");
        }
        return accountList;
    }

    private Uri getCurrentAccountUri() {
        return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
    }

    protected String getCurrentAccountEmailAddress() {
        return mCurrentAccount == null ? "" : mCurrentAccount.getEmailAddress();
    }

    protected MergedAdapter<ListAdapter> getMergedAdapter() {
        return mMergedAdapter;
    }

    public ObjectCursor<Folder> getFoldersCursor() {
        return (mFolderAdapter != null) ? mFolderAdapter.getCursor() : null;
    }

    private class FooterAdapter extends BaseAdapter {

        private final List<DrawerItem> mFooterItems = Lists.newArrayList();

        private FooterAdapter() {
            update();
        }

        @Override
        public int getCount() {
            return mFooterItems.size();
        }

        @Override
        public DrawerItem getItem(int position) {
            return mFooterItems.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public int getViewTypeCount() {
            // Accounts, headers, folders (all parts of drawer view types)
            return DrawerItem.getViewTypeCount();
        }

        @Override
        public int getItemViewType(int position) {
            return getItem(position).getType();
        }

        /**
         * @param convertView a view, possibly null, to be recycled.
         * @param parent the parent hosting this view.
         * @return a view for the footer item displaying the given text and image.
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return getItem(position).getView(convertView, parent);
        }

        /**
         * Recomputes the footer drawer items depending on whether the current account
         * is populated with URIs that navigate to appropriate destinations.
         */
        private void update() {
            // if the parent activity shows a drawer, these items should participate in that drawer
            // (if it shows a *pane* they should *not* participate in that pane)
            if (mIsFolderSelectionActivity) {
                return;
            }

            mFooterItems.clear();

            if (mCurrentAccount != null) {
                mFooterItems.add(DrawerItem.ofSettingsItem(mActivity, mCurrentAccount, mDrawerListener));
            }

            if (mCurrentAccount != null && !Utils.isEmpty(mCurrentAccount.helpIntentUri)) {
                mFooterItems.add(DrawerItem.ofHelpItem(mActivity, mCurrentAccount, mDrawerListener));
            }

            if (!mFooterItems.isEmpty()) {
                mFooterItems.add(0, DrawerItem.ofBlankHeader(mActivity));
                mFooterItems.add(DrawerItem.ofBottomSpace(mActivity));
            }

            notifyDataSetChanged();
        }
    }

    /**
     * Sets the currently selected folder safely.
     * @param folder the folder to change to. It is an error to pass null here.
     */
    private void setSelectedFolder(Folder folder) {
        if (folder == null) {
            mSelectedFolderUri = FolderUri.EMPTY;
            mCurrentFolderForUnreadCheck = null;
            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
            return;
        }

        final boolean viewChanged = !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);

        // There are two cases in which the folder type is not set by this class.
        // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
        //    folder but its type was never set.
        // 2. The user backs into the default inbox. Going 'back' from the conversation list of
        //    any folder will take you to the default inbox for that account. (If you are in the
        //    default inbox already, back exits the app.)
        // In both these cases, the selected folder type is not set, and must be set.
        if (mSelectedDrawerItemCategory == DrawerItem.UNSET
                || (mCurrentAccount != null && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
            mSelectedDrawerItemCategory = folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
            mSelectedFolderType = folder.type;
        }

        mCurrentFolderForUnreadCheck = folder;
        mSelectedFolderUri = folder.folderUri;
        if (viewChanged) {
            if (mFolderAdapter != null) {
                mFolderAdapter.notifyDataSetChanged();
            }
            if (mMiniDrawerView != null) {
                mMiniDrawerView.refresh();
            }
        }
    }

    public boolean isSelectedFolder(@NonNull Folder folder) {
        return folder.folderUri.equals(mSelectedFolderUri);
    }

    /**
     * Sets the current account to the one provided here.
     * @param account the current account to set to.
     */
    private void setSelectedAccount(Account account) {
        final boolean changed = (account != null)
                && (mCurrentAccount == null || !mCurrentAccount.uri.equals(account.uri));
        mCurrentAccount = account;
        if (changed) {
            // Verify that the new account supports sending application feedback
            updateFooterItems();
            // We no longer have proper folder objects. Let the new ones come in
            mFolderAdapter.setCursor(null);
            // If currentAccount is different from the one we set, restart the loader. Look at the
            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
            // don't just do restartLoader.
            final LoaderManager manager = getLoaderManager();
            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
            manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
            manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
            // An updated cursor causes the entire list to refresh. No need to refresh the list.
            // But we do need to blank out the current folder, since the account might not be
            // synced.
            mSelectedFolderUri = FolderUri.EMPTY;
            mCurrentFolderForUnreadCheck = null;

            // also set/update the mini-drawer
            if (mMiniDrawerAccountsAdapter != null) {
                mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount);
            }

        } else if (account == null) {
            // This should never happen currently, but is a safeguard against a very incorrect
            // non-null account -> null account transition.
            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
            final LoaderManager manager = getLoaderManager();
            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
        }
    }

    private void updateFooterItems() {
        mFooterAdapter.update();
    }

    /**
     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
     */
    private boolean isFolderTypeExcluded(final Folder folder) {
        if (mExcludedFolderTypes == null) {
            return false;
        }

        for (final int excludedType : mExcludedFolderTypes) {
            if (folder.isType(excludedType)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return the choice mode to use for the {@link ListView}
     */
    protected int getListViewChoiceMode() {
        return mAccountController.getFolderListViewChoiceMode();
    }

    /**
     * Drawer listener for footer functionality to react to drawer state.
     */
    public class DrawerStateListener implements DrawerLayout.DrawerListener {

        private FooterItem mPendingFooterClick;

        public void setPendingFooterClick(FooterItem itemClicked) {
            mPendingFooterClick = itemClicked;
        }

        @Override
        public void onDrawerSlide(View drawerView, float slideOffset) {
        }

        @Override
        public void onDrawerOpened(View drawerView) {
        }

        @Override
        public void onDrawerClosed(View drawerView) {
            if (mPendingFooterClick != null) {
                mPendingFooterClick.onFooterClicked();
                mPendingFooterClick = null;
            }
        }

        @Override
        public void onDrawerStateChanged(int newState) {
        }

    }

    private class FolderOrAccountListener extends DataSetObserver {

        @Override
        public void onChanged() {
            // First, check if there's a folder to change to
            if (mNextFolder != null) {
                mFolderChanger.onFolderSelected(mNextFolder);
                mNextFolder = null;
            }
            // Next, check if there's an account to change to
            if (mNextAccount != null) {
                mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
                mNextAccount = null;
            }
        }
    }

    @Override
    public ListAdapter getListAdapter() {
        // Ensures that we get the adapter with the header views.
        throw new UnsupportedOperationException(
                "Use getListView().getAdapter() instead " + "which accounts for any header or footer views.");
    }

    protected class MiniDrawerAccountsAdapter extends BaseAdapter {

        private List<Account> mAccounts = new ArrayList<>();

        public void setAccounts(Account[] accounts, Account currentAccount) {
            mAccounts.clear();
            if (currentAccount == null) {
                notifyDataSetChanged();
                return;
            }
            mAccounts.add(currentAccount);
            // TODO: sort by most recent accounts
            for (final Account account : accounts) {
                if (!account.getEmailAddress().equals(currentAccount.getEmailAddress())) {
                    mAccounts.add(account);
                }
            }
            notifyDataSetChanged();
        }

        @Override
        public int getCount() {
            return mAccounts.size();
        }

        @Override
        public Object getItem(int position) {
            // Is there an attempt made to access outside of the drawer item list?
            if (position >= mAccounts.size()) {
                return null;
            } else {
                return mAccounts.get(position);
            }
        }

        @Override
        public long getItemId(int position) {
            return getItem(position).hashCode();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final ImageView iv = convertView != null ? (ImageView) convertView
                    : (ImageView) LayoutInflater.from(getActivity())
                            .inflate(R.layout.mini_drawer_recent_account_item, parent, false /* attachToRoot */);
            final MiniDrawerAccountItem item = new MiniDrawerAccountItem(iv);
            item.setupDrawable();
            item.setAccount(mAccounts.get(position));
            iv.setTag(item);
            return iv;
        }

        private class MiniDrawerAccountItem implements View.OnClickListener {
            private Account mAccount;
            private AccountAvatarDrawable mDrawable;
            public final ImageView view;

            public MiniDrawerAccountItem(ImageView iv) {
                view = iv;
                view.setOnClickListener(this);
            }

            public void setupDrawable() {
                mDrawable = new AccountAvatarDrawable(getResources(), getBitmapCache(), getContactResolver());
                mDrawable.setDecodeDimensions(mMiniDrawerAvatarDecodeSize, mMiniDrawerAvatarDecodeSize);
                view.setImageDrawable(mDrawable);
            }

            public void setAccount(Account acct) {
                mAccount = acct;
                mDrawable.bind(mAccount.getSenderName(), mAccount.getEmailAddress());
                String contentDescription = mAccount.getDisplayName();
                if (TextUtils.isEmpty(contentDescription)) {
                    contentDescription = mAccount.getEmailAddress();
                }
                view.setContentDescription(contentDescription);
            }

            @Override
            public void onClick(View v) {
                onAccountSelected(mAccount);
            }
        }
    }

    protected void setupMiniDrawerAccountsAdapter() {
        mMiniDrawerAccountsAdapter = new MiniDrawerAccountsAdapter();
    }

    protected ListAdapter getMiniDrawerAccountsAdapter() {
        return mMiniDrawerAccountsAdapter;
    }

    private static class FadeAnimatorListener extends AnimatorListenerAdapter {
        private boolean mCanceled;
        private final View mView;
        private final boolean mFadeOut;

        FadeAnimatorListener(View v, boolean fadeOut) {
            mView = v;
            mFadeOut = fadeOut;
        }

        @Override
        public void onAnimationStart(Animator animation) {
            if (!mFadeOut) {
                mView.setVisibility(View.VISIBLE);
            }
            mCanceled = false;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            mCanceled = true;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (!mCanceled) {
                // Only need to set visibility to INVISIBLE for fade-out and not fade-in.
                if (mFadeOut) {
                    mView.setVisibility(View.INVISIBLE);
                }
                // If the animation is canceled, then the next animation onAnimationEnd will disable
                // the hardware layer.
                mView.setLayerType(View.LAYER_TYPE_NONE, null);
            }
        }
    }

}