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