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