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.net.Uri;
     25 import android.os.Bundle;
     26 import android.support.v4.text.BidiFormatter;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.widget.ArrayAdapter;
     31 import android.widget.BaseAdapter;
     32 import android.widget.ImageView;
     33 import android.widget.ListAdapter;
     34 import android.widget.ListView;
     35 
     36 import com.android.mail.R;
     37 import com.android.mail.adapter.DrawerItem;
     38 import com.android.mail.analytics.Analytics;
     39 import com.android.mail.content.ObjectCursor;
     40 import com.android.mail.content.ObjectCursorLoader;
     41 import com.android.mail.providers.Account;
     42 import com.android.mail.providers.AccountObserver;
     43 import com.android.mail.providers.AllAccountObserver;
     44 import com.android.mail.providers.DrawerClosedObserver;
     45 import com.android.mail.providers.Folder;
     46 import com.android.mail.providers.FolderObserver;
     47 import com.android.mail.providers.FolderWatcher;
     48 import com.android.mail.providers.RecentFolderObserver;
     49 import com.android.mail.providers.UIProvider;
     50 import com.android.mail.providers.UIProvider.FolderType;
     51 import com.android.mail.utils.FolderUri;
     52 import com.android.mail.utils.LogTag;
     53 import com.android.mail.utils.LogUtils;
     54 
     55 import java.util.ArrayList;
     56 import java.util.Iterator;
     57 import java.util.List;
     58 
     59 /**
     60  * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
     61  * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
     62  * in a drawer along with the list of folders.
     63  *
     64  * This class has the following use-cases:
     65  * <ul>
     66  *     <li>
     67  *         Show a list of accounts and a divided list of folders. In this case, the list shows
     68  *         Accounts, Inboxes, Recent Folders, All folders.
     69  *         Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
     70  *         folders switches folders.
     71  *         This is created through XML resources as a {@link DrawerFragment}. Since it is created
     72  *         through resources, it receives all arguments through callbacks.
     73  *     </li>
     74  *     <li>
     75  *         Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
     76  *         Drafts, Starred, and any user-created folders. For providers that allow nested folders,
     77  *         this will only show the folders at the top-level.
     78  *         <br /> Tapping on a parent folder creates a new fragment with the child folders at
     79  *         that level.
     80  *     </li>
     81  *     <li>
     82  *         Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
     83  *         {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
     84  *         any folder for a given account.
     85  *     </li>
     86  * </ul>
     87  */
     88 public class FolderListFragment extends ListFragment implements
     89         LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
     90     private static final String LOG_TAG = LogTag.getLogTag();
     91     /** The parent activity */
     92     private ControllableActivity mActivity;
     93     private BidiFormatter mBidiFormatter;
     94     /** The underlying list view */
     95     private ListView mListView;
     96     /** URI that points to the list of folders for the current account. */
     97     private Uri mFolderListUri;
     98     /**
     99      * True if you want a divided FolderList. A divided folder list shows the following groups:
    100      * Inboxes, Recent Folders, All folders.
    101      *
    102      * An undivided FolderList shows all folders without any divisions and without recent folders.
    103      * This is true only for the drawer: for all others it is false.
    104      */
    105     protected boolean mIsDivided = false;
    106     /** True if the folder list belongs to a folder selection activity (one account only) */
    107     protected boolean mHideAccounts = true;
    108     /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
    109     private ArrayList<Integer> mExcludedFolderTypes;
    110     /** Object that changes folders on our behalf. */
    111     private FolderSelector mFolderChanger;
    112     /** Object that changes accounts on our behalf */
    113     private AccountController mAccountController;
    114 
    115     /** The currently selected folder (the folder being viewed).  This is never null. */
    116     private FolderUri mSelectedFolderUri = FolderUri.EMPTY;
    117     /**
    118      * The current folder from the controller.  This is meant only to check when the unread count
    119      * goes out of sync and fixing it.
    120      */
    121     private Folder mCurrentFolderForUnreadCheck;
    122     /** Parent of the current folder, or null if the current folder is not a child. */
    123     private Folder mParentFolder;
    124 
    125     private static final int FOLDER_LIST_LOADER_ID = 0;
    126     /** Loader id for the list of all folders in the account */
    127     private static final int ALL_FOLDER_LIST_LOADER_ID = 1;
    128     /** Key to store {@link #mParentFolder}. */
    129     private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
    130     /** Key to store {@link #mFolderListUri}. */
    131     private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
    132     /** Key to store {@link #mExcludedFolderTypes} */
    133     private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
    134 
    135     private static final String BUNDLE_LIST_STATE = "flf-list-state";
    136     private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
    137     private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
    138 
    139     private FolderListFragmentCursorAdapter mCursorAdapter;
    140     /** Observer to wait for changes to the current folder so we can change the selected folder */
    141     private FolderObserver mFolderObserver = null;
    142     /** Listen for account changes. */
    143     private AccountObserver mAccountObserver = null;
    144     /** Listen for account changes. */
    145     private DrawerClosedObserver mDrawerObserver = null;
    146     /** Listen to changes to list of all accounts */
    147     private AllAccountObserver mAllAccountsObserver = null;
    148     /**
    149      * Type of currently selected folder: {@link DrawerItem#FOLDER_INBOX},
    150      * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_OTHER}.
    151      * Set as {@link DrawerItem#UNSET} to begin with, as there is nothing selected yet.
    152      */
    153     private int mSelectedFolderType = DrawerItem.UNSET;
    154     /** The current account according to the controller */
    155     private Account mCurrentAccount;
    156     /** The account we will change to once the drawer (if any) is closed */
    157     private Account mNextAccount = null;
    158     /** The folder we will change to once the drawer (if any) is closed */
    159     private Folder mNextFolder = null;
    160 
    161     /**
    162      * Constructor needs to be public to handle orientation changes and activity lifecycle events.
    163      */
    164     public FolderListFragment() {
    165         super();
    166     }
    167 
    168     @Override
    169     public String toString() {
    170         final StringBuilder sb = new StringBuilder(super.toString());
    171         sb.setLength(sb.length() - 1);
    172         sb.append(" folder=");
    173         sb.append(mFolderListUri);
    174         sb.append(" parent=");
    175         sb.append(mParentFolder);
    176         sb.append(" adapterCount=");
    177         sb.append(mCursorAdapter != null ? mCursorAdapter.getCount() : -1);
    178         sb.append("}");
    179         return sb.toString();
    180     }
    181 
    182     /**
    183      * Creates a new instance of {@link FolderListFragment}, initialized
    184      * to display the folder and its immediate children.
    185      * @param folder parent folder whose children are shown
    186      *
    187      */
    188     public static FolderListFragment ofTree(Folder folder) {
    189         final FolderListFragment fragment = new FolderListFragment();
    190         fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
    191         return fragment;
    192     }
    193 
    194     /**
    195      * Creates a new instance of {@link FolderListFragment}, initialized
    196      * to display the top level: where we have no parent folder, but we have a list of folders
    197      * from the account.
    198      * @param folderListUri the URI which contains all the list of folders
    199      * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
    200      */
    201     public static FolderListFragment ofTopLevelTree(Uri folderListUri,
    202             final ArrayList<Integer> excludedFolderTypes) {
    203         final FolderListFragment fragment = new FolderListFragment();
    204         fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
    205         return fragment;
    206     }
    207 
    208     /**
    209      * Construct a bundle that represents the state of this fragment.
    210      *
    211      * @param parentFolder non-null for trees, the parent of this list
    212      * @param folderListUri the URI which contains all the list of folders
    213      * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
    214      * @return Bundle containing parentFolder, divided list boolean and
    215      *         excluded folder types
    216      */
    217     private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
    218             final ArrayList<Integer> excludedFolderTypes) {
    219         final Bundle args = new Bundle(3);
    220         if (parentFolder != null) {
    221             args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
    222         }
    223         if (folderListUri != null) {
    224             args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
    225         }
    226         if (excludedFolderTypes != null) {
    227             args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
    228         }
    229         return args;
    230     }
    231 
    232     @Override
    233     public void onActivityCreated(Bundle savedState) {
    234         super.onActivityCreated(savedState);
    235         // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
    236         // only activity creating a ConversationListContext is a MailActivity which is of type
    237         // ControllableActivity, so this cast should be safe. If this cast fails, some other
    238         // activity is creating ConversationListFragments. This activity must be of type
    239         // ControllableActivity.
    240         final Activity activity = getActivity();
    241         if (! (activity instanceof ControllableActivity)){
    242             LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
    243                     "create it. Cannot proceed.");
    244             return;
    245         }
    246         mActivity = (ControllableActivity) activity;
    247         mBidiFormatter = BidiFormatter.getInstance();
    248         final FolderController controller = mActivity.getFolderController();
    249         // Listen to folder changes in the future
    250         mFolderObserver = new FolderObserver() {
    251             @Override
    252             public void onChanged(Folder newFolder) {
    253                 setSelectedFolder(newFolder);
    254             }
    255         };
    256         final Folder currentFolder;
    257         if (controller != null) {
    258             // Only register for selected folder updates if we have a controller.
    259             currentFolder = mFolderObserver.initialize(controller);
    260             mCurrentFolderForUnreadCheck = currentFolder;
    261         } else {
    262             currentFolder = null;
    263         }
    264 
    265         // Initialize adapter for folder/heirarchical list.  Note this relies on
    266         // mActivity being initialized.
    267         final Folder selectedFolder;
    268         if (mParentFolder != null) {
    269             mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
    270             selectedFolder = mActivity.getHierarchyFolder();
    271         } else {
    272             mCursorAdapter = new FolderListAdapter(mIsDivided);
    273             selectedFolder = currentFolder;
    274         }
    275         // Is the selected folder fresher than the one we have restored from a bundle?
    276         if (selectedFolder != null
    277                 && !selectedFolder.folderUri.equals(mSelectedFolderUri)) {
    278             setSelectedFolder(selectedFolder);
    279         }
    280 
    281         // Assign observers for current account & all accounts
    282         final AccountController accountController = mActivity.getAccountController();
    283         mAccountObserver = new AccountObserver() {
    284             @Override
    285             public void onChanged(Account newAccount) {
    286                 setSelectedAccount(newAccount);
    287             }
    288         };
    289         mFolderChanger = mActivity.getFolderSelector();
    290         if (accountController != null) {
    291             // Current account and its observer.
    292             setSelectedAccount(mAccountObserver.initialize(accountController));
    293             // List of all accounts and its observer.
    294             mAllAccountsObserver = new AllAccountObserver(){
    295                 @Override
    296                 public void onChanged(Account[] allAccounts) {
    297                     mCursorAdapter.notifyAllAccountsChanged();
    298                 }
    299             };
    300             mAllAccountsObserver.initialize(accountController);
    301             mAccountController = accountController;
    302 
    303             // Observer for when the drawer is closed
    304             mDrawerObserver = new DrawerClosedObserver() {
    305                 @Override
    306                 public void onDrawerClosed() {
    307                     // First, check if there's a folder to change to
    308                     if (mNextFolder != null) {
    309                         mFolderChanger.onFolderSelected(mNextFolder);
    310                         mNextFolder = null;
    311                     }
    312                     // Next, check if there's an account to change to
    313                     if (mNextAccount != null) {
    314                         mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
    315                         mNextAccount = null;
    316                     }
    317                 }
    318             };
    319             mDrawerObserver.initialize(accountController);
    320         }
    321 
    322         if (mActivity.isFinishing()) {
    323             // Activity is finishing, just bail.
    324             return;
    325         }
    326 
    327         mListView.setChoiceMode(getListViewChoiceMode());
    328 
    329         setListAdapter(mCursorAdapter);
    330     }
    331 
    332     /**
    333      * Set the instance variables from the arguments provided here.
    334      * @param args bundle of arguments with keys named ARG_*
    335      */
    336     private void setInstanceFromBundle(Bundle args) {
    337         if (args == null) {
    338             return;
    339         }
    340         mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
    341         final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
    342         if (folderUri != null) {
    343             mFolderListUri = Uri.parse(folderUri);
    344         }
    345         mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
    346     }
    347 
    348     @Override
    349     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    350             Bundle savedState) {
    351         setInstanceFromBundle(getArguments());
    352 
    353         final View rootView = inflater.inflate(R.layout.folder_list, null);
    354         mListView = (ListView) rootView.findViewById(android.R.id.list);
    355         mListView.setEmptyView(null);
    356         mListView.setDivider(null);
    357         if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
    358             mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
    359         }
    360         if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
    361             mSelectedFolderUri =
    362                     new FolderUri(Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)));
    363             mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
    364         } else if (mParentFolder != null) {
    365             mSelectedFolderUri = mParentFolder.folderUri;
    366             // No selected folder type required for hierarchical lists.
    367         }
    368 
    369         return rootView;
    370     }
    371 
    372     @Override
    373     public void onStart() {
    374         super.onStart();
    375     }
    376 
    377     @Override
    378     public void onStop() {
    379         super.onStop();
    380     }
    381 
    382     @Override
    383     public void onPause() {
    384         super.onPause();
    385     }
    386 
    387     @Override
    388     public void onSaveInstanceState(Bundle outState) {
    389         super.onSaveInstanceState(outState);
    390         if (mListView != null) {
    391             outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
    392         }
    393         if (mSelectedFolderUri != null) {
    394             outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
    395         }
    396         outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
    397     }
    398 
    399     @Override
    400     public void onDestroyView() {
    401         if (mCursorAdapter != null) {
    402             mCursorAdapter.destroy();
    403         }
    404         // Clear the adapter.
    405         setListAdapter(null);
    406         if (mFolderObserver != null) {
    407             mFolderObserver.unregisterAndDestroy();
    408             mFolderObserver = null;
    409         }
    410         if (mAccountObserver != null) {
    411             mAccountObserver.unregisterAndDestroy();
    412             mAccountObserver = null;
    413         }
    414         if (mAllAccountsObserver != null) {
    415             mAllAccountsObserver.unregisterAndDestroy();
    416             mAllAccountsObserver = null;
    417         }
    418         if (mDrawerObserver != null) {
    419             mDrawerObserver.unregisterAndDestroy();
    420             mDrawerObserver = null;
    421         }
    422         super.onDestroyView();
    423     }
    424 
    425     @Override
    426     public void onListItemClick(ListView l, View v, int position, long id) {
    427         viewFolderOrChangeAccount(position);
    428     }
    429 
    430     private Folder getDefaultInbox(Account account) {
    431         if (account == null || mCursorAdapter == null) {
    432             return null;
    433         }
    434         return mCursorAdapter.getDefaultInbox(account);
    435     }
    436 
    437     private void changeAccount(final Account account) {
    438         // Switching accounts takes you to the default inbox for that account.
    439         mSelectedFolderType = DrawerItem.FOLDER_INBOX;
    440         mNextAccount = account;
    441         mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
    442         Analytics.getInstance().sendEvent("switch_account", "drawer_account_switch", null, 0);
    443     }
    444 
    445     /**
    446      * Display the conversation list from the folder at the position given.
    447      * @param position a zero indexed position into the list.
    448      */
    449     private void viewFolderOrChangeAccount(int position) {
    450         final Object item = getListAdapter().getItem(position);
    451         LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
    452         final Folder folder;
    453         int folderType = DrawerItem.UNSET;
    454 
    455         if (item instanceof DrawerItem) {
    456             final DrawerItem drawerItem = (DrawerItem) item;
    457             // Could be a folder or account.
    458             final int itemType = mCursorAdapter.getItemType(drawerItem);
    459             if (itemType == DrawerItem.VIEW_ACCOUNT) {
    460                 // Account, so switch.
    461                 folder = null;
    462                 final Account account = drawerItem.mAccount;
    463 
    464                 if (account != null && account.settings.defaultInbox.equals(mSelectedFolderUri)) {
    465                     // We're already in the default inbox for account, just re-check item ...
    466                     final int defaultInboxPosition = position + 1;
    467                     if (mListView.getChildAt(defaultInboxPosition) != null) {
    468                         mListView.setItemChecked(defaultInboxPosition, true);
    469                     }
    470                     // ... and close the drawer (no new target folders/accounts)
    471                     mAccountController.closeDrawer(false, mNextAccount,
    472                             getDefaultInbox(mNextAccount));
    473                 } else {
    474                     changeAccount(account);
    475                 }
    476             } else if (itemType == DrawerItem.VIEW_FOLDER) {
    477                 // Folder type, so change folders only.
    478                 folder = drawerItem.mFolder;
    479                 mSelectedFolderType = folderType = drawerItem.mFolderType;
    480                 LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
    481                         folder, mSelectedFolderType);
    482             } else {
    483                 // Do nothing.
    484                 LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
    485                         + " Clicked on unset item in drawer. Offending item is " + item);
    486                 return;
    487             }
    488         } else if (item instanceof Folder) {
    489             folder = (Folder) item;
    490         } else {
    491             // Don't know how we got here.
    492             LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
    493             folder = null;
    494         }
    495         if (folder != null) {
    496             // Not changing the account.
    497             final Account nextAccount = null;
    498             // Go to the conversation list for this folder.
    499             if (!folder.folderUri.equals(mSelectedFolderUri)) {
    500                 mNextFolder = folder;
    501                 mAccountController.closeDrawer(true, nextAccount, folder);
    502 
    503                 final String label = (folderType == DrawerItem.FOLDER_RECENT) ? "recent" : "normal";
    504                 Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(),
    505                         label, 0);
    506 
    507             } else {
    508                 // Clicked on same folder, just close drawer
    509                 mAccountController.closeDrawer(false, nextAccount, folder);
    510             }
    511         }
    512     }
    513 
    514     @Override
    515     public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
    516         mListView.setEmptyView(null);
    517         final Uri folderListUri;
    518         if (id == FOLDER_LIST_LOADER_ID) {
    519             if (mFolderListUri != null) {
    520                 // Folder trees, they specify a URI at construction time.
    521                 folderListUri = mFolderListUri;
    522             } else {
    523                 // Drawers get the folder list from the current account.
    524                 folderListUri = mCurrentAccount.folderListUri;
    525             }
    526         } else if (id == ALL_FOLDER_LIST_LOADER_ID) {
    527             folderListUri = mCurrentAccount.allFolderListUri;
    528         } else {
    529             LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
    530             return null;
    531         }
    532         return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
    533                 UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
    534     }
    535 
    536     @Override
    537     public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
    538         if (mCursorAdapter != null) {
    539             if (loader.getId() == FOLDER_LIST_LOADER_ID) {
    540                 mCursorAdapter.setCursor(data);
    541             } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
    542                 mCursorAdapter.setAllFolderListCursor(data);
    543             }
    544         }
    545     }
    546 
    547     @Override
    548     public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
    549         if (mCursorAdapter != null) {
    550             if (loader.getId() == FOLDER_LIST_LOADER_ID) {
    551                 mCursorAdapter.setCursor(null);
    552             } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
    553                 mCursorAdapter.setAllFolderListCursor(null);
    554             }
    555         }
    556     }
    557 
    558     /**
    559      *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
    560      *  frequency of use.
    561      * @return a list of accounts, sorted by frequency of use
    562      */
    563     private Account[] getAllAccounts() {
    564         if (mAllAccountsObserver != null) {
    565             return mAllAccountsObserver.getAllAccounts();
    566         }
    567         return new Account[0];
    568     }
    569 
    570     /**
    571      * Interface for all cursor adapters that allow setting a cursor and being destroyed.
    572      */
    573     private interface FolderListFragmentCursorAdapter extends ListAdapter {
    574         /** Update the folder list cursor with the cursor given here. */
    575         void setCursor(ObjectCursor<Folder> cursor);
    576         /** Update the all folder list cursor with the cursor given here. */
    577         void setAllFolderListCursor(ObjectCursor<Folder> cursor);
    578         /**
    579          * Given an item, find the type of the item, which should only be {@link
    580          * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
    581          * @return item the type of the item.
    582          */
    583         int getItemType(DrawerItem item);
    584         /** Notify that the all accounts changed. */
    585         void notifyAllAccountsChanged();
    586         /** Remove all observers and destroy the object. */
    587         void destroy();
    588         /** Notifies the adapter that the data has changed. */
    589         void notifyDataSetChanged();
    590         /** Returns default inbox for this account. */
    591         Folder getDefaultInbox(Account account);
    592         /** Returns the index of the first selected item, or -1 if no selection */
    593         int getSelectedPosition();
    594     }
    595 
    596     /**
    597      * An adapter for flat folder lists.
    598      */
    599     private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
    600 
    601         private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
    602             @Override
    603             public void onChanged() {
    604                 if (!isCursorInvalid()) {
    605                     recalculateList();
    606                 }
    607             }
    608         };
    609         /** No resource used for string header in folder list */
    610         private static final int NO_HEADER_RESOURCE = -1;
    611         /** Cache of most recently used folders */
    612         private final RecentFolderList mRecentFolders;
    613         /** True if the list is divided, false otherwise. See the comment on
    614          * {@link FolderListFragment#mIsDivided} for more information */
    615         private final boolean mIsDivided;
    616         /** All the items */
    617         private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
    618         /** Cursor into the folder list. This might be null. */
    619         private ObjectCursor<Folder> mCursor = null;
    620         /** Cursor into the all folder list. This might be null. */
    621         private ObjectCursor<Folder> mAllFolderListCursor = null;
    622         /** Watcher for tracking and receiving unread counts for mail */
    623         private FolderWatcher mFolderWatcher = null;
    624         private boolean mRegistered = false;
    625 
    626         /**
    627          * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
    628          *
    629          * @param isDivided true if folder list is flat, false if divided by label group. See
    630          *                   the comments on {@link #mIsDivided} for more information
    631          */
    632         public FolderListAdapter(boolean isDivided) {
    633             super();
    634             mIsDivided = isDivided;
    635             final RecentFolderController controller = mActivity.getRecentFolderController();
    636             if (controller != null && mIsDivided) {
    637                 mRecentFolders = mRecentFolderObserver.initialize(controller);
    638             } else {
    639                 mRecentFolders = null;
    640             }
    641             mFolderWatcher = new FolderWatcher(mActivity, this);
    642             mFolderWatcher.updateAccountList(getAllAccounts());
    643         }
    644 
    645         @Override
    646         public void notifyAllAccountsChanged() {
    647             if (!mRegistered && mAccountController != null) {
    648                 // TODO(viki): Round-about way of setting the watcher. http://b/8750610
    649                 mAccountController.setFolderWatcher(mFolderWatcher);
    650                 mRegistered = true;
    651             }
    652             mFolderWatcher.updateAccountList(getAllAccounts());
    653             recalculateList();
    654         }
    655 
    656         @Override
    657         public View getView(int position, View convertView, ViewGroup parent) {
    658             final DrawerItem item = (DrawerItem) getItem(position);
    659             final View view = item.getView(convertView, parent);
    660             final int type = item.mType;
    661             final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedFolderType);
    662             if (type == DrawerItem.VIEW_FOLDER) {
    663                 mListView.setItemChecked(position, isSelected);
    664             }
    665             // If this is the current folder, also check to verify that the unread count
    666             // matches what the action bar shows.
    667             if (type == DrawerItem.VIEW_FOLDER
    668                     && isSelected
    669                     && (mCurrentFolderForUnreadCheck != null)
    670                     && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
    671                 ((FolderItemView) view).overrideUnreadCount(
    672                         mCurrentFolderForUnreadCheck.unreadCount);
    673             }
    674             return view;
    675         }
    676 
    677         @Override
    678         public int getViewTypeCount() {
    679             // Accounts, headers, folders (all parts of drawer view types)
    680             return DrawerItem.getViewTypes();
    681         }
    682 
    683         @Override
    684         public int getItemViewType(int position) {
    685             return ((DrawerItem) getItem(position)).mType;
    686         }
    687 
    688         @Override
    689         public int getCount() {
    690             return mItemList.size();
    691         }
    692 
    693         @Override
    694         public boolean isEnabled(int position) {
    695             final DrawerItem drawerItem = ((DrawerItem) getItem(position));
    696             return drawerItem != null && drawerItem.isItemEnabled();
    697         }
    698 
    699         private Uri getCurrentAccountUri() {
    700             return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
    701         }
    702 
    703         @Override
    704         public boolean areAllItemsEnabled() {
    705             // We have headers and thus some items are not enabled.
    706             return false;
    707         }
    708 
    709         /**
    710          * Returns all the recent folders from the list given here. Safe to call with a null list.
    711          * @param recentList a list of all recently accessed folders.
    712          * @return a valid list of folders, which are all recent folders.
    713          */
    714         private List<Folder> getRecentFolders(RecentFolderList recentList) {
    715             final List<Folder> folderList = new ArrayList<Folder>();
    716             if (recentList == null) {
    717                 return folderList;
    718             }
    719             // Get all recent folders, after removing system folders.
    720             for (final Folder f : recentList.getRecentFolderList(null)) {
    721                 if (!f.isProviderFolder()) {
    722                     folderList.add(f);
    723                 }
    724             }
    725             return folderList;
    726         }
    727 
    728         /**
    729          * Responsible for verifying mCursor, and ensuring any recalculate
    730          * conditions are met. Also calls notifyDataSetChanged once it's finished
    731          * populating {@link FolderListAdapter#mItemList}
    732          */
    733         private void recalculateList() {
    734             final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>();
    735             // Don't show accounts for single-account-based folder selection (i.e. widgets)
    736             if (!mHideAccounts) {
    737                 recalculateListAccounts(newFolderList);
    738             }
    739             recalculateListFolders(newFolderList);
    740             mItemList = newFolderList;
    741             // Ask the list to invalidate its views.
    742             notifyDataSetChanged();
    743         }
    744 
    745         /**
    746          * Recalculates the accounts if not null and adds them to the list.
    747          *
    748          * @param itemList List of drawer items to populate
    749          */
    750         private void recalculateListAccounts(List<DrawerItem> itemList) {
    751             final Account[] allAccounts = getAllAccounts();
    752             // Add all accounts and then the current account
    753             final Uri currentAccountUri = getCurrentAccountUri();
    754             for (final Account account : allAccounts) {
    755                 final int unreadCount = mFolderWatcher.getUnreadCount(account);
    756                 itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
    757                         currentAccountUri.equals(account.uri), mBidiFormatter));
    758             }
    759             if (mCurrentAccount == null) {
    760                 LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account.");
    761             }
    762         }
    763 
    764         /**
    765          * Recalculates the system, recent and user label lists.
    766          * This method modifies all the three lists on every single invocation.
    767          *
    768          * @param itemList List of drawer items to populate
    769          */
    770         private void recalculateListFolders(List<DrawerItem> itemList) {
    771             // If we are waiting for folder initialization, we don't have any kinds of folders,
    772             // just the "Waiting for initialization" item. Note, this should only be done
    773             // when we're waiting for account initialization or initial sync.
    774             if (isCursorInvalid()) {
    775                 if(!mCurrentAccount.isAccountReady()) {
    776                     itemList.add(DrawerItem.ofWaitView(mActivity, mBidiFormatter));
    777                 }
    778                 return;
    779             }
    780 
    781             if (!mIsDivided) {
    782                 // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
    783                 do {
    784                     final Folder f = mCursor.getModel();
    785                     if (!isFolderTypeExcluded(f)) {
    786                         itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER,
    787                                 mBidiFormatter));
    788                     }
    789                 } while (mCursor.moveToNext());
    790 
    791                 return;
    792             }
    793 
    794             // Otherwise, this is an adapter for a divided list.
    795             final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
    796             final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
    797             do {
    798                 final Folder f = mCursor.getModel();
    799                 if (!isFolderTypeExcluded(f)) {
    800                     if (f.isInbox()) {
    801                         inboxFolders.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_INBOX,
    802                                 mBidiFormatter));
    803                     } else {
    804                         allFoldersList.add(DrawerItem.ofFolder(mActivity, f,
    805                                 DrawerItem.FOLDER_OTHER, mBidiFormatter));
    806                     }
    807                 }
    808             } while (mCursor.moveToNext());
    809 
    810             // If we have the all folder list, verify that the current folder exists
    811             boolean currentFolderFound = false;
    812             if (mAllFolderListCursor != null) {
    813                 final String folderName = mSelectedFolderUri.toString();
    814                 LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);
    815 
    816                 if (mAllFolderListCursor.moveToFirst()) {
    817                     LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
    818                     do {
    819                         final Folder f = mAllFolderListCursor.getModel();
    820                         if (!isFolderTypeExcluded(f)) {
    821                             if (f.folderUri.equals(mSelectedFolderUri)) {
    822                                 LogUtils.d(LOG_TAG, "Found %s !", folderName);
    823                                 currentFolderFound = true;
    824                             }
    825                         }
    826                     } while (!currentFolderFound && mAllFolderListCursor.moveToNext());
    827                 }
    828 
    829                 if (!currentFolderFound && mSelectedFolderUri != FolderUri.EMPTY
    830                         && mCurrentAccount != null && mAccountController != null
    831                         && mAccountController.isDrawerPullEnabled()) {
    832                     LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
    833                             folderName, mCurrentAccount.name);
    834                     changeAccount(mCurrentAccount);
    835                 }
    836             }
    837 
    838             // Add all inboxes (sectioned Inboxes included) before recent folders.
    839             addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
    840 
    841             // Add recent folders next.
    842             addRecentsToList(itemList);
    843 
    844             // Add the remaining folders.
    845             addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
    846         }
    847 
    848         /**
    849          * Given a list of folders as {@link DrawerItem}s, add them as a group.
    850          * Passing in a non-0 integer for the resource will enable a header.
    851          *
    852          * @param destination List of drawer items to populate
    853          * @param source List of drawer items representing folders to add to the drawer
    854          * @param headerStringResource
    855          *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
    856          *            is required, or res-id otherwise. The integer is interpreted as the string
    857          *            for the header's title.
    858          */
    859         private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
    860                 int headerStringResource) {
    861             if (source.size() > 0) {
    862                 if(headerStringResource != NO_HEADER_RESOURCE) {
    863                     destination.add(DrawerItem.ofHeader(mActivity, headerStringResource,
    864                             mBidiFormatter));
    865                 }
    866                 destination.addAll(source);
    867             }
    868         }
    869 
    870         /**
    871          * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
    872          *
    873          * @param destination List of drawer items to populate
    874          */
    875         private void addRecentsToList(List<DrawerItem> destination) {
    876             // If there are recent folders, add them.
    877             final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
    878 
    879             // Remove any excluded folder types
    880             if (mExcludedFolderTypes != null) {
    881                 final Iterator<Folder> iterator = recentFolderList.iterator();
    882                 while (iterator.hasNext()) {
    883                     if (isFolderTypeExcluded(iterator.next())) {
    884                         iterator.remove();
    885                     }
    886                 }
    887             }
    888 
    889             if (recentFolderList.size() > 0) {
    890                 destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading,
    891                         mBidiFormatter));
    892                 // Recent folders are not queried for position.
    893                 for (Folder f : recentFolderList) {
    894                     destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
    895                             mBidiFormatter));
    896                 }
    897             }
    898         }
    899 
    900         /**
    901          * Check if the cursor provided is valid.
    902          * @return True if cursor is invalid, false otherwise
    903          */
    904         private boolean isCursorInvalid() {
    905             return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
    906                     || !mCursor.moveToFirst();
    907         }
    908 
    909         @Override
    910         public void setCursor(ObjectCursor<Folder> cursor) {
    911             mCursor = cursor;
    912             recalculateList();
    913         }
    914 
    915         @Override
    916         public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
    917             mAllFolderListCursor = cursor;
    918             recalculateList();
    919         }
    920 
    921         @Override
    922         public Object getItem(int position) {
    923             // Is there an attempt made to access outside of the drawer item list?
    924             if (position >= mItemList.size()) {
    925                 return null;
    926             } else {
    927                 return mItemList.get(position);
    928             }
    929         }
    930 
    931         @Override
    932         public long getItemId(int position) {
    933             return getItem(position).hashCode();
    934         }
    935 
    936         @Override
    937         public final void destroy() {
    938             mRecentFolderObserver.unregisterAndDestroy();
    939         }
    940 
    941         @Override
    942         public Folder getDefaultInbox(Account account) {
    943             if (mFolderWatcher != null) {
    944                 return mFolderWatcher.getDefaultInbox(account);
    945             }
    946             return null;
    947         }
    948 
    949         @Override
    950         public int getItemType(DrawerItem item) {
    951             return item.mType;
    952         }
    953 
    954         @Override
    955         public int getSelectedPosition() {
    956             for (int i = 0; i < mItemList.size(); i++) {
    957                 final DrawerItem item = (DrawerItem) getItem(i);
    958                 final boolean isSelected =
    959                         item.isHighlighted(mSelectedFolderUri, mSelectedFolderType);
    960                 if (isSelected) {
    961                     return i;
    962                 }
    963             }
    964 
    965             return -1;
    966         }
    967     }
    968 
    969     private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
    970             implements FolderListFragmentCursorAdapter {
    971 
    972         private static final int PARENT = 0;
    973         private static final int CHILD = 1;
    974         private final FolderUri mParentUri;
    975         private final Folder mParent;
    976         private final FolderItemView.DropHandler mDropHandler;
    977 
    978         public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
    979             super(mActivity.getActivityContext(), R.layout.folder_item);
    980             mDropHandler = mActivity;
    981             mParent = parentFolder;
    982             mParentUri = parentFolder.folderUri;
    983             setCursor(c);
    984         }
    985 
    986         @Override
    987         public int getViewTypeCount() {
    988             // Child and Parent
    989             return 2;
    990         }
    991 
    992         @Override
    993         public int getItemViewType(int position) {
    994             final Folder f = getItem(position);
    995             return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
    996         }
    997 
    998         @Override
    999         public View getView(int position, View convertView, ViewGroup parent) {
   1000             final FolderItemView folderItemView;
   1001             final Folder folder = getItem(position);
   1002             boolean isParent = folder.folderUri.equals(mParentUri);
   1003             if (convertView != null) {
   1004                 folderItemView = (FolderItemView) convertView;
   1005             } else {
   1006                 int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
   1007                 folderItemView = (FolderItemView) LayoutInflater.from(
   1008                         mActivity.getActivityContext()).inflate(resId, null);
   1009             }
   1010             folderItemView.bind(folder, mDropHandler, mBidiFormatter);
   1011             if (folder.folderUri.equals(mSelectedFolderUri)) {
   1012                 getListView().setItemChecked(position, true);
   1013                 // If this is the current folder, also check to verify that the unread count
   1014                 // matches what the action bar shows.
   1015                 final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
   1016                         && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
   1017                 if (unreadCountDiffers) {
   1018                     folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
   1019                 }
   1020             }
   1021             Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
   1022             Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
   1023             return folderItemView;
   1024         }
   1025 
   1026         @Override
   1027         public void setCursor(ObjectCursor<Folder> cursor) {
   1028             clear();
   1029             if (mParent != null) {
   1030                 add(mParent);
   1031             }
   1032             if (cursor != null && cursor.getCount() > 0) {
   1033                 cursor.moveToFirst();
   1034                 do {
   1035                     add(cursor.getModel());
   1036                 } while (cursor.moveToNext());
   1037             }
   1038         }
   1039 
   1040         @Override
   1041         public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
   1042             // Not necessary in HierarchicalFolderListAdapter
   1043         }
   1044 
   1045         @Override
   1046         public void destroy() {
   1047             // Do nothing.
   1048         }
   1049 
   1050         @Override
   1051         public Folder getDefaultInbox(Account account) {
   1052             return null;
   1053         }
   1054 
   1055         @Override
   1056         public int getItemType(DrawerItem item) {
   1057             // Always returns folders for now.
   1058             return DrawerItem.VIEW_FOLDER;
   1059         }
   1060 
   1061         @Override
   1062         public void notifyAllAccountsChanged() {
   1063             // Do nothing. We don't care about changes to all accounts.
   1064         }
   1065 
   1066         @Override
   1067         public int getSelectedPosition() {
   1068             final int count = getCount();
   1069             for (int i = 0; i < count; i++) {
   1070                 final Folder folder = getItem(i);
   1071                 final boolean isSelected = folder.folderUri.equals(mSelectedFolderUri);
   1072                 if (isSelected) {
   1073                     return i;
   1074                 }
   1075             }
   1076             return -1;
   1077         }
   1078     }
   1079 
   1080     /**
   1081      * Sets the currently selected folder safely.
   1082      * @param folder the folder to change to. It is an error to pass null here.
   1083      */
   1084     private void setSelectedFolder(Folder folder) {
   1085         if (folder == null) {
   1086             mSelectedFolderUri = FolderUri.EMPTY;
   1087             mCurrentFolderForUnreadCheck = null;
   1088             LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
   1089             return;
   1090         }
   1091 
   1092         final boolean viewChanged =
   1093                 !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
   1094 
   1095         // There are two cases in which the folder type is not set by this class.
   1096         // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
   1097         //    folder but its type was never set.
   1098         // 2. The user backs into the default inbox. Going 'back' from the conversation list of
   1099         //    any folder will take you to the default inbox for that account. (If you are in the
   1100         //    default inbox already, back exits the app.)
   1101         // In both these cases, the selected folder type is not set, and must be set.
   1102         if (mSelectedFolderType == DrawerItem.UNSET || (mCurrentAccount != null
   1103                 && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
   1104             mSelectedFolderType =
   1105                     folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
   1106         }
   1107 
   1108         mCurrentFolderForUnreadCheck = folder;
   1109         mSelectedFolderUri = folder.folderUri;
   1110         if (mCursorAdapter != null && viewChanged) {
   1111             mCursorAdapter.notifyDataSetChanged();
   1112         }
   1113     }
   1114 
   1115     public void updateScroll() {
   1116         final int selectedPosition = mCursorAdapter.getSelectedPosition();
   1117         if (selectedPosition >= 0) {
   1118             // TODO: setSelection() jumps the item to the top of the list "hiding" the accounts
   1119             // TODO: and smoothScrollToPosition() is too slow for lots of labels/folders
   1120             // It's called "setSelection" but it's really more like "jumpScrollToPosition"
   1121             // mListView.setSelection(selectedPosition);
   1122         }
   1123     }
   1124 
   1125     /**
   1126      * Sets the current account to the one provided here.
   1127      * @param account the current account to set to.
   1128      */
   1129     private void setSelectedAccount(Account account){
   1130         final boolean changed = (account != null) && (mCurrentAccount == null
   1131                 || !mCurrentAccount.uri.equals(account.uri));
   1132         mCurrentAccount = account;
   1133         if (changed) {
   1134             // We no longer have proper folder objects. Let the new ones come in
   1135             mCursorAdapter.setCursor(null);
   1136             // If currentAccount is different from the one we set, restart the loader. Look at the
   1137             // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
   1138             // don't just do restartLoader.
   1139             final LoaderManager manager = getLoaderManager();
   1140             manager.destroyLoader(FOLDER_LIST_LOADER_ID);
   1141             manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
   1142             manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
   1143             manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
   1144             // An updated cursor causes the entire list to refresh. No need to refresh the list.
   1145             // But we do need to blank out the current folder, since the account might not be
   1146             // synced.
   1147             mSelectedFolderUri = FolderUri.EMPTY;
   1148             mCurrentFolderForUnreadCheck = null;
   1149         } else if (account == null) {
   1150             // This should never happen currently, but is a safeguard against a very incorrect
   1151             // non-null account -> null account transition.
   1152             LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
   1153             final LoaderManager manager = getLoaderManager();
   1154             manager.destroyLoader(FOLDER_LIST_LOADER_ID);
   1155             manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
   1156         }
   1157     }
   1158 
   1159     /**
   1160      * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
   1161      */
   1162     private boolean isFolderTypeExcluded(final Folder folder) {
   1163         if (mExcludedFolderTypes == null) {
   1164             return false;
   1165         }
   1166 
   1167         for (final int excludedType : mExcludedFolderTypes) {
   1168             if (folder.isType(excludedType)) {
   1169                 return true;
   1170             }
   1171         }
   1172 
   1173         return false;
   1174     }
   1175 
   1176     /**
   1177      * @return the choice mode to use for the {@link ListView}
   1178      */
   1179     protected int getListViewChoiceMode() {
   1180         return mAccountController.getFolderListViewChoiceMode();
   1181     }
   1182 }
   1183