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