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