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