FolderListFragment.java revision b9c23da3c3d0eebf980552a0f95bbee4e11368af
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.app.Activity;
21import android.app.ListFragment;
22import android.app.LoaderManager;
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Loader;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.net.Uri;
30import android.os.AsyncTask;
31import android.os.Bundle;
32import android.provider.ContactsContract;
33import android.provider.ContactsContract.Data;
34import android.provider.ContactsContract.CommonDataKinds.Email;
35import android.provider.ContactsContract.Contacts.Photo;
36import android.text.TextUtils;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.ArrayAdapter;
41import android.widget.BaseAdapter;
42import android.widget.ImageView;
43import android.widget.ListAdapter;
44import android.widget.ListView;
45
46import com.android.mail.R;
47import com.android.mail.adapter.DrawerItem;
48import com.android.mail.browse.LetterTileUtils;
49import com.android.mail.content.ObjectCursor;
50import com.android.mail.content.ObjectCursorLoader;
51import com.android.mail.photomanager.BitmapUtil;
52import com.android.mail.providers.Account;
53import com.android.mail.providers.AccountObserver;
54import com.android.mail.providers.AllAccountObserver;
55import com.android.mail.providers.Folder;
56import com.android.mail.providers.FolderObserver;
57import com.android.mail.providers.FolderWatcher;
58import com.android.mail.providers.RecentFolderObserver;
59import com.android.mail.providers.UIProvider;
60import com.android.mail.providers.UIProvider.FolderType;
61import com.android.mail.utils.LogTag;
62import com.android.mail.utils.LogUtils;
63import com.google.common.collect.ImmutableSet;
64import com.android.mail.utils.Utils;
65
66import java.util.ArrayList;
67import java.util.Arrays;
68import java.util.HashMap;
69import java.util.HashSet;
70import java.util.Iterator;
71import java.util.List;
72import java.util.Set;
73
74/**
75 * The folder list UI component.
76 */
77public class FolderListFragment extends ListFragment implements
78        LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
79    private static final String LOG_TAG = LogTag.getLogTag();
80    /** The parent activity */
81    private ControllableActivity mActivity;
82    /** The underlying list view */
83    private ListView mListView;
84    /** URI that points to the list of folders for the current account. */
85    private Uri mFolderListUri;
86    /** True if you want a sectioned FolderList, false otherwise. */
87    protected boolean mIsSectioned;
88    /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
89    private ArrayList<Integer> mExcludedFolderTypes;
90    /** Object that changes folders on our behalf. */
91    private FolderListSelectionListener mFolderChanger;
92    /** Object that changes accounts on our behalf */
93    private AccountController mAccountChanger;
94
95    /** The currently selected folder (the folder being viewed).  This is never null. */
96    private Uri mSelectedFolderUri = Uri.EMPTY;
97    /**
98     * The current folder from the controller.  This is meant only to check when the unread count
99     * goes out of sync and fixing it.
100     */
101    private Folder mCurrentFolderForUnreadCheck;
102    /** Parent of the current folder, or null if the current folder is not a child. */
103    private Folder mParentFolder;
104
105    private static final int FOLDER_LOADER_ID = 0;
106    /** Key to store {@link #mParentFolder}. */
107    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
108    /** Key to store {@link #mIsSectioned} */
109    private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
110    /** Key to store {@link #mFolderListUri}. */
111    private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
112    /** Key to store {@link #mExcludedFolderTypes} */
113    private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
114    /** Key to store {@link #mType} */
115    private static final String ARG_TYPE = "arg-flf-type";
116
117    /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */
118    private int mType;
119    /** This fragment is a drawer */
120    private static final int TYPE_DRAWER = 0;
121    /** This fragment is a folder tree */
122    private static final int TYPE_TREE = 1;
123
124    private static final String BUNDLE_LIST_STATE = "flf-list-state";
125    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
126    private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
127
128    private FolderListFragmentCursorAdapter mCursorAdapter;
129    /** Observer to wait for changes to the current folder so we can change the selected folder */
130    private FolderObserver mFolderObserver = null;
131    /** Listen for account changes. */
132    private AccountObserver mAccountObserver = null;
133
134    /** Listen to changes to list of all accounts */
135    private AllAccountObserver mAllAccountsObserver = null;
136    /**
137     * Type of currently selected folder: {@link DrawerItem#FOLDER_SYSTEM},
138     * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_USER}.
139     */
140    // Setting to INERT_HEADER = leaving uninitialized.
141    private int mSelectedFolderType = DrawerItem.UNSET;
142    /** The current account according to the controller */
143    private Account mCurrentAccount;
144
145    /**
146     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
147     */
148    public FolderListFragment() {
149        super();
150    }
151
152    @Override
153    public String toString() {
154        final StringBuilder sb = new StringBuilder(super.toString());
155        sb.setLength(sb.length() - 1);
156        sb.append(" folder=");
157        sb.append(mFolderListUri);
158        sb.append(" parent=");
159        sb.append(mParentFolder);
160        sb.append(" adapterCount=");
161        sb.append(mCursorAdapter != null ? mCursorAdapter.getCount() : -1);
162        sb.append("}");
163        return sb.toString();
164    }
165
166    /**
167     * Creates a new instance of {@link FolderListFragment}. Gets the current account and current
168     * folder through observers.
169     */
170    public static FolderListFragment ofDrawer() {
171        final FolderListFragment fragment = new FolderListFragment();
172        // The drawer is always sectioned
173        final boolean isSectioned = true;
174        fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null));
175        return fragment;
176    }
177
178    /**
179     * Creates a new instance of {@link FolderListFragment}, initialized
180     * to display the folder and its immediate children.
181     * @param folder parent folder whose children are shown
182     *
183     */
184    public static FolderListFragment ofTree(Folder folder) {
185        final FolderListFragment fragment = new FolderListFragment();
186        // Trees are never sectioned.
187        final boolean isSectioned = false;
188        fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri,
189                isSectioned, null));
190        return fragment;
191    }
192
193    /**
194     * Creates a new instance of {@link FolderListFragment}, initialized
195     * to display the folder and its immediate children.
196     * @param folderListUri the URI which contains all the list of folders
197     * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
198     */
199    public static FolderListFragment ofTopLevelTree(Uri folderListUri,
200            final ArrayList<Integer> excludedFolderTypes) {
201        final FolderListFragment fragment = new FolderListFragment();
202        // Trees are never sectioned.
203        final boolean isSectioned = false;
204        fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri,
205                isSectioned, excludedFolderTypes));
206        return fragment;
207    }
208
209    /**
210     * Construct a bundle that represents the state of this fragment.
211     * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE}
212     * @param parentFolder non-null for trees, the parent of this list
213     * @param isSectioned true if this drawer is sectioned, false otherwise
214     * @param folderListUri the URI which contains all the list of folders
215     * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
216     * @return Bundle containing parentFolder, sectioned list boolean and
217     *         excluded folder types
218     */
219    private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri,
220            boolean isSectioned, final ArrayList<Integer> excludedFolderTypes) {
221        final Bundle args = new Bundle();
222        args.putInt(ARG_TYPE, type);
223        if (parentFolder != null) {
224            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
225        }
226        if (folderListUri != null) {
227            args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
228        }
229        args.putBoolean(ARG_IS_SECTIONED, isSectioned);
230        if (excludedFolderTypes != null) {
231            args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
232        }
233        return args;
234    }
235
236    @Override
237    public void onActivityCreated(Bundle savedState) {
238        super.onActivityCreated(savedState);
239        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
240        // only activity creating a ConversationListContext is a MailActivity which is of type
241        // ControllableActivity, so this cast should be safe. If this cast fails, some other
242        // activity is creating ConversationListFragments. This activity must be of type
243        // ControllableActivity.
244        final Activity activity = getActivity();
245        Folder currentFolder = null;
246        if (! (activity instanceof ControllableActivity)){
247            LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
248                    "create it. Cannot proceed.");
249        }
250        mActivity = (ControllableActivity) activity;
251        final FolderController controller = mActivity.getFolderController();
252        // Listen to folder changes in the future
253        mFolderObserver = new FolderObserver() {
254            @Override
255            public void onChanged(Folder newFolder) {
256                setSelectedFolder(newFolder);
257            }
258        };
259        if (controller != null) {
260            // Only register for selected folder updates if we have a controller.
261            currentFolder = mFolderObserver.initialize(controller);
262            mCurrentFolderForUnreadCheck = currentFolder;
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(mIsSectioned);
273            selectedFolder = currentFolder;
274        }
275        // Is the selected folder fresher than the one we have restored from a bundle?
276        if (selectedFolder != null && !selectedFolder.uri.equals(mSelectedFolderUri)) {
277            setSelectedFolder(selectedFolder);
278        }
279
280        // Assign observers for current account & all accounts
281        final AccountController accountController = mActivity.getAccountController();
282        mAccountObserver = new AccountObserver() {
283            @Override
284            public void onChanged(Account newAccount) {
285                setSelectedAccount(newAccount);
286            }
287        };
288        if (accountController != null) {
289            // Current account and its observer.
290            setSelectedAccount(mAccountObserver.initialize(accountController));
291            // List of all accounts and its observer.
292            mAllAccountsObserver = new AllAccountObserver(){
293                @Override
294                public void onChanged(Account[] allAccounts) {
295                    mCursorAdapter.notifyAllAccountsChanged();
296                }
297            };
298            mAllAccountsObserver.initialize(accountController);
299            mAccountChanger = accountController;
300        }
301
302        mFolderChanger = mActivity.getFolderListSelectionListener();
303        if (mActivity.isFinishing()) {
304            // Activity is finishing, just bail.
305            return;
306        }
307
308        setListAdapter(mCursorAdapter);
309    }
310
311    /**
312     * Set the instance variables from the arguments provided here.
313     * @param args
314     */
315    private void setInstanceFromBundle(Bundle args) {
316        if (args == null) {
317            return;
318        }
319        mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
320        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
321        if (folderUri == null) {
322            mFolderListUri = Uri.EMPTY;
323        } else {
324            mFolderListUri = Uri.parse(folderUri);
325        }
326        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
327        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
328        mType = args.getInt(ARG_TYPE);
329    }
330
331    @Override
332    public View onCreateView(LayoutInflater inflater, ViewGroup container,
333            Bundle savedState) {
334        final View rootView = inflater.inflate(R.layout.folder_list, null);
335        mListView = (ListView) rootView.findViewById(android.R.id.list);
336        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
337        mListView.setEmptyView(null);
338        // If we're not using tablet UI, set the background correctly
339        if (!Utils.useTabletUI(getResources())) {
340            mListView.setBackgroundResource(R.color.list_background_color);
341        }
342        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
343            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
344        }
345        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
346            mSelectedFolderUri = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER));
347            mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
348        } else if (mParentFolder != null) {
349            mSelectedFolderUri = mParentFolder.uri;
350            // No selected folder type required for hierarchical lists.
351        }
352
353        return rootView;
354    }
355
356    @Override
357    public void onStart() {
358        super.onStart();
359    }
360
361    @Override
362    public void onStop() {
363        super.onStop();
364    }
365
366    @Override
367    public void onPause() {
368        super.onPause();
369    }
370
371    @Override
372    public void onSaveInstanceState(Bundle outState) {
373        super.onSaveInstanceState(outState);
374        if (mListView != null) {
375            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
376        }
377        if (mSelectedFolderUri != null) {
378            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
379        }
380        outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
381    }
382
383    @Override
384    public void onDestroyView() {
385        if (mCursorAdapter != null) {
386            mCursorAdapter.destroy();
387        }
388        // Clear the adapter.
389        setListAdapter(null);
390        if (mFolderObserver != null) {
391            mFolderObserver.unregisterAndDestroy();
392            mFolderObserver = null;
393        }
394        if (mAccountObserver != null) {
395            mAccountObserver.unregisterAndDestroy();
396            mAccountObserver = null;
397        }
398        if (mAllAccountsObserver != null) {
399            mAllAccountsObserver.unregisterAndDestroy();
400            mAllAccountsObserver = null;
401        }
402        super.onDestroyView();
403    }
404
405    @Override
406    public void onListItemClick(ListView l, View v, int position, long id) {
407        viewFolderOrChangeAccount(position);
408    }
409
410    /**
411     * Display the conversation list from the folder at the position given.
412     * @param position a zero indexed position into the list.
413     */
414    private void viewFolderOrChangeAccount(int position) {
415        final Object item = getListAdapter().getItem(position);
416        LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
417        final Folder folder;
418        if (item instanceof DrawerItem) {
419            final DrawerItem drawerItem = (DrawerItem) item;
420            // Could be a folder or account.
421            final int itemType = mCursorAdapter.getItemType(drawerItem);
422            if (itemType == DrawerItem.VIEW_ACCOUNT) {
423                // Account, so switch.
424                folder = null;
425                final Account account = drawerItem.mAccount;
426                // Switching accounts takes you to the inbox, which is always a system folder.
427                mSelectedFolderType = DrawerItem.FOLDER_SYSTEM;
428                mAccountChanger.changeAccount(account);
429            } else if (itemType == DrawerItem.VIEW_FOLDER) {
430                // Folder type, so change folders only.
431                folder = drawerItem.mFolder;
432                mSelectedFolderType = drawerItem.mFolderType;
433                LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
434                        folder, mSelectedFolderType);
435            } else {
436                // Do nothing.
437                LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
438                        + " Clicked on unset item in drawer. Offending item is " + item);
439                return;
440            }
441        } else if (item instanceof Folder) {
442            folder = (Folder) item;
443        } else if (item instanceof ObjectCursor){
444            folder = ((ObjectCursor<Folder>) item).getModel();
445        } else {
446            // Don't know how we got here.
447            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
448            folder = null;
449        }
450        if (folder != null) {
451            // Since we may be looking at hierarchical views, if we can
452            // determine the parent of the folder we have tapped, set it here.
453            // If we are looking at the folder we are already viewing, don't
454            // update its parent!
455            folder.parent = folder.equals(mParentFolder) ? null : mParentFolder;
456            // Go to the conversation list for this folder.
457            mFolderChanger.onFolderSelected(folder);
458        }
459    }
460
461    @Override
462    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
463        mListView.setEmptyView(null);
464        final Uri folderListUri;
465        if (mType == TYPE_TREE) {
466            // Folder trees, they specify a URI at construction time.
467            folderListUri = mFolderListUri;
468        } else if (mType == TYPE_DRAWER) {
469            // Drawers should have a valid account
470            if (mCurrentAccount != null) {
471                folderListUri = mCurrentAccount.folderListUri;
472            } else {
473                LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
474                return null;
475            }
476        } else {
477            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
478            return null;
479        }
480        return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
481                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
482    }
483
484    @Override
485    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
486        if (mCursorAdapter != null) {
487            mCursorAdapter.setCursor(data);
488        }
489    }
490
491    @Override
492    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
493        if (mCursorAdapter != null) {
494            mCursorAdapter.setCursor(null);
495        }
496    }
497
498    /**
499     *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
500     *  frequency of use.
501     * @return a list of accounts, sorted by frequency of use
502     */
503    private Account[] getAllAccounts() {
504        if (mAllAccountsObserver != null) {
505            return mAllAccountsObserver.getAllAccounts();
506        }
507        return new Account[0];
508    }
509
510    /**
511     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
512     */
513    private interface FolderListFragmentCursorAdapter extends ListAdapter {
514        /** Update the folder list cursor with the cursor given here. */
515        void setCursor(ObjectCursor<Folder> cursor);
516        /**
517         * Given an item, find the type of the item, which should only be {@link
518         * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
519         * @return item the type of the item.
520         */
521        int getItemType(DrawerItem item);
522        /** Get the folder associated with this item. **/
523        Folder getFullFolder(DrawerItem item);
524        /** Notify that the all accounts changed. */
525        void notifyAllAccountsChanged();
526        /** Remove all observers and destroy the object. */
527        void destroy();
528        /** Notifies the adapter that the data has changed. */
529        void notifyDataSetChanged();
530    }
531
532    /**
533     * An adapter for flat folder lists.
534     */
535    private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
536
537        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
538            @Override
539            public void onChanged() {
540                recalculateList();
541            }
542        };
543        /** Database columns for email address -> photo_id query */
544        private final String[] DATA_COLS = new String[] { Email.DATA, Email.PHOTO_ID };
545        /** Database columns for photo_id -> photo query */
546        private final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO };
547        /** No resource used for string header in folder list */
548        private static final int NO_HEADER_RESOURCE = -1;
549        /** Cache of most recently used folders */
550        private final RecentFolderList mRecentFolders;
551        /** True if the list is sectioned, false otherwise */
552        private final boolean mIsSectioned;
553        /** All the items */
554        private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
555        /** Cursor into the folder list. This might be null. */
556        private ObjectCursor<Folder> mCursor = null;
557        /** Watcher for tracking and receiving unread counts for mail */
558        private FolderWatcher mFolderWatcher = null;
559        /**
560         * DO NOT USE off the UI thread. Will cause ConcurrentModificationExceptions otherwise
561         *
562         * Email address -> Bitmap
563         * Caveat: at some point we will want this to be from URI to Bitmap.
564         */
565        private final HashMap<String, Bitmap> mEmailToPhotoMap = new HashMap<String, Bitmap>();
566
567        /**
568         * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
569         *
570         * @param isSectioned true if folder list is flat, false if sectioned by label group
571         */
572        public FolderListAdapter(boolean isSectioned) {
573            super();
574            mIsSectioned = isSectioned;
575            final RecentFolderController controller = mActivity.getRecentFolderController();
576            if (controller != null && mIsSectioned) {
577                mRecentFolders = mRecentFolderObserver.initialize(controller);
578            } else {
579                mRecentFolders = null;
580            }
581            mFolderWatcher = new FolderWatcher(mActivity, this);
582            mFolderWatcher.updateAccountList(getAllAccounts());
583        }
584
585        @Override
586        public void notifyAllAccountsChanged() {
587            mFolderWatcher.updateAccountList(getAllAccounts());
588            retrieveContactPhotos();
589            recalculateList();
590        }
591
592        /**
593         * AsyncTask for loading all photos that populates the email address -> Bitmap Hash Map.
594         * Does the querying and loading of photos in the background along with creating
595         * default images in case contact photos aren't found.
596         *
597         * The task is of type <String, Void, HashMap<String, Bitmap>> which corresponds to
598         * the input being an array of String and the result being a HashMap that will get merged to
599         * {@link FolderListAdapter#mEmailToPhotoMap}.
600         */
601        private class LoadPhotosTask extends AsyncTask<Account, Void, HashMap<String, Bitmap>> {
602            private final ContentResolver mResolver;
603            private final Context mContext;
604            private final int mImageSize;
605
606            /**
607             * Construct the async task for downloading the photos.
608             */
609            public LoadPhotosTask(final Context context, final int imageSize) {
610                mResolver = context.getContentResolver();
611                mContext = context;
612                mImageSize = imageSize;
613            }
614
615            /**
616             * Runs account photo retrieval in the background. Note, mEmailToPhotoMap should NOT be
617             * modified here since this is run on a background thread and not the UI thread.
618             *
619             * The {@link Account#accountFromAddresses} is used for letter tiles and is required
620             * in order to properly assign the tile to the respective account.
621             */
622            @Override
623            protected HashMap<String, Bitmap> doInBackground(final Account... allAccounts) {
624                final HashMap<String, String> addressToDisplayNameMap = new HashMap<
625                        String, String>();
626                for (final Account account : allAccounts) {
627                    addressToDisplayNameMap.put(account.name, account.accountFromAddresses);
628                }
629
630                return getAccountPhoto(addressToDisplayNameMap);
631            }
632
633            @Override
634            protected void onPostExecute(final HashMap<String, Bitmap> accountPhotos) {
635                mEmailToPhotoMap.putAll(accountPhotos);
636                if (!accountPhotos.isEmpty()) {
637                    recalculateList();
638                }
639            }
640
641            /**
642             * Queries the database for the photos. First finds the corresponding photo_id and then
643             * proceeds to find the photo through subsequent queries for {photo_id, bytes}. If the
644             * photo is not found for the address at the end, creates a letter tile using the
645             * display name/email address and then adds that to the finished HashMap
646             *
647             * @param addresses array of email addresses (strings)
648             * @param addressToDisplayNameMap map of email addresses to display names used for
649             *              letter tiles
650             * @return map of email addresses to the corresponding photos
651             */
652            private HashMap<String, Bitmap> getAccountPhoto(
653                    final HashMap<String, String> addressToDisplayNameMap) {
654                // Columns for email address, photo_id
655                final int DATA_EMAIL_COLUMN = 0;
656                final int DATA_PHOTO_COLUMN = 1;
657                final HashMap<String, Bitmap> photoMap = new HashMap<String, Bitmap>();
658                final Set<String> addressSet = addressToDisplayNameMap.keySet();
659
660                String address;
661                long photoId;
662                Cursor photoIdsCursor = null;
663
664
665                try {
666                    // Build query for address -> photo_id
667                    final StringBuilder query = new StringBuilder().append(Data.MIMETYPE)
668                            .append("='").append(Email.CONTENT_ITEM_TYPE).append("' AND ")
669                            .append(Email.DATA).append(" IN (");
670                    appendQuestionMarks(query, addressSet.size());
671                    query.append(')');
672                    photoIdsCursor = mResolver
673                            .query(Data.CONTENT_URI, DATA_COLS, query.toString(),
674                                    addressSet.toArray(new String[addressSet.size()]), null);
675
676                    // Iterate through cursor and attempt to find a matching photo_id
677                    if (photoIdsCursor != null) {
678                        while (photoIdsCursor.moveToNext()) {
679                            // If photo_id is found, query for the encoded bitmap
680                            if (!photoIdsCursor.isNull(DATA_PHOTO_COLUMN)) {
681                                address = photoIdsCursor.getString(DATA_EMAIL_COLUMN);
682                                photoId = photoIdsCursor.getLong(DATA_PHOTO_COLUMN);
683                                final byte[] bitmapBytes = getPhotoForId(photoId);
684                                if (bitmapBytes != null && photoMap.get(address) == null) {
685                                    final Bitmap contactPhoto = BitmapUtil.decodeBitmapFromBytes(
686                                            bitmapBytes, mImageSize, mImageSize);
687                                    photoMap.put(address, contactPhoto);
688                                }
689                            }
690                        }
691                    }
692                } finally {
693                    if(photoIdsCursor != null) {
694                        photoIdsCursor.close();
695                    }
696                }
697
698                // Finally, make sure that for any addresses in the original list for which
699                // we are unable to find contact photos, we're adding the LetterTiles
700                for(final String emailAddress : addressSet) {
701                    if(!photoMap.containsKey(emailAddress)) {
702                        final Bitmap letterTile = LetterTileUtils.generateLetterTile(
703                                addressToDisplayNameMap.get(emailAddress), emailAddress, mContext,
704                                mImageSize, mImageSize);
705                        photoMap.put(emailAddress, letterTile);
706                    }
707                }
708
709                return photoMap;
710            }
711
712            /**
713             * Find the photo by running a query on the photoId provided.
714             *
715             * @param resolver ContentResolver to query on
716             * @param photoId id corresponding to the photo (if found)
717             * @return array containing photo bytes
718             */
719            private byte[] getPhotoForId(final long photoId) {
720                // Column for the photo blob
721                final int DATA_PHOTO_COLUMN = 1;
722
723                byte[] bitmapBytes = null;
724                // First try getting photos from Contacts
725                Cursor contactCursor = null;
726                try {
727                    final String[] selectionArgs = { String.valueOf(photoId) };
728                    contactCursor = mResolver.query(Data.CONTENT_URI, PHOTO_COLS,
729                            Photo._ID + " = ?", selectionArgs, null);
730                    while (contactCursor.moveToNext()) {
731                        if (!contactCursor.isNull(1)) {
732                            bitmapBytes = contactCursor.getBlob(1);
733                            break;
734                        }
735                    }
736                } finally {
737                    if (contactCursor != null) {
738                        contactCursor.close();
739                    }
740                }
741
742                // Photo not found in contacts, try profiles instead
743                if(bitmapBytes == null) {
744                    if (ContactsContract.isProfileId(photoId)) {
745                        Cursor profileCursor = null;
746                        try {
747                            profileCursor = mResolver.query(
748                                    ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
749                                    PHOTO_COLS, null, null, null);
750                            if (profileCursor != null && profileCursor.moveToFirst()) {
751                                bitmapBytes = profileCursor.getBlob(DATA_PHOTO_COLUMN);
752                            }
753                        } finally {
754                            if (profileCursor != null) {
755                                profileCursor.close();
756                            }
757                        }
758                    }
759                }
760                return bitmapBytes;
761            }
762
763            /**
764             * Prepare the Selection clause for the given query by appending question marks
765             * followed by commas (Comma-delimited list of question marks as listed by
766             * the itemCount.
767             *
768             * @param query {@link StringBuilder} representing the query thus far
769             * @param itemCount number of selection arguments to add
770             */
771            private void appendQuestionMarks(final StringBuilder query, final int itemCount) {
772                final String[] questionMarks = new String[itemCount];
773                Arrays.fill(questionMarks, "?");
774                final String selection = TextUtils.join(", ", questionMarks);
775                query.append(selection);
776            }
777        }
778
779        /**
780         * Retrieve photos for accounts that do not yet have a mapping in
781         * {@link FolderListAdapter#mEmailToPhotoMap} by querying over the database. Every account
782         * is guaranteed to have either the account contact photo, letter tile, or a default gray
783         * picture for non-English account names.
784         */
785        public synchronized void retrieveContactPhotos() {
786            final Account[] allAccounts = getAllAccounts();
787            if (allAccounts == null) {
788                return;
789            }
790            /** Fresh accounts that were recently added to the system. */
791            final HashSet<Account> freshAccounts = new HashSet<Account>();
792            /** All current account email addresses. */
793            final HashSet<String> currentEmailList = new HashSet<String>();
794            final Context context = mActivity.getActivityContext();
795            final int imageSize = context.getResources().getDimensionPixelSize(
796                    R.dimen.folder_list_item_minimum_height);
797
798            for (final Account account : allAccounts) {
799                final String email = account.name;
800                if (!mEmailToPhotoMap.containsKey(email)) {
801                    freshAccounts.add(account);
802                    // For multiple tasks running very closely together, make sure we don't end up
803                    // loading pictures for an address more than once
804                    mEmailToPhotoMap.put(email, null);
805                }
806                currentEmailList.add(email);
807            }
808            // Find all the stale accounts in our map, and remove them.
809            final Set<String> emails = ImmutableSet.copyOf(mEmailToPhotoMap.keySet());
810            for (final String email : emails) {
811                if (!currentEmailList.contains(email)) {
812                    mEmailToPhotoMap.remove(email);
813                }
814            }
815            // Fetch contact photos or letter tiles for each fresh account.
816            if (!freshAccounts.isEmpty()) {
817                new LoadPhotosTask(context, imageSize).execute(
818                        freshAccounts.toArray(new Account[freshAccounts.size()]));
819            }
820        }
821
822        @Override
823        public View getView(int position, View convertView, ViewGroup parent) {
824            final DrawerItem item = (DrawerItem) getItem(position);
825            final View view = item.getView(position, convertView, parent);
826            final int type = item.mType;
827            if (mListView != null) {
828                final boolean isSelected =
829                        item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType);
830                if (type == DrawerItem.VIEW_FOLDER) {
831                    mListView.setItemChecked(position, isSelected);
832                }
833                // If this is the current folder, also check to verify that the unread count
834                // matches what the action bar shows.
835                if (type == DrawerItem.VIEW_FOLDER
836                        && isSelected
837                        && (mCurrentFolderForUnreadCheck != null)
838                        && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
839                    ((FolderItemView) view).overrideUnreadCount(
840                            mCurrentFolderForUnreadCheck.unreadCount);
841                }
842            }
843            LogUtils.d(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item);
844            return view;
845        }
846
847        @Override
848        public int getViewTypeCount() {
849            // Accounts, headers, folders (all parts of drawer view types)
850            return DrawerItem.getViewTypes();
851        }
852
853        @Override
854        public int getItemViewType(int position) {
855            return ((DrawerItem) getItem(position)).mType;
856        }
857
858        @Override
859        public int getCount() {
860            return mItemList.size();
861        }
862
863        @Override
864        public boolean isEnabled(int position) {
865            return ((DrawerItem) getItem(position)).isItemEnabled();
866        }
867
868        private Uri getCurrentAccountUri() {
869            return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
870        }
871
872        @Override
873        public boolean areAllItemsEnabled() {
874            // We have headers and thus some items are not enabled.
875            return false;
876        }
877
878        /**
879         * Returns all the recent folders from the list given here. Safe to call with a null list.
880         * @param recentList a list of all recently accessed folders.
881         * @return a valid list of folders, which are all recent folders.
882         */
883        private List<Folder> getRecentFolders(RecentFolderList recentList) {
884            final List<Folder> folderList = new ArrayList<Folder>();
885            if (recentList == null) {
886                return folderList;
887            }
888            // Get all recent folders, after removing system folders.
889            for (final Folder f : recentList.getRecentFolderList(null)) {
890                if (!f.isProviderFolder()) {
891                    folderList.add(f);
892                }
893            }
894            return folderList;
895        }
896
897        /**
898         * Responsible for verifying mCursor, and ensuring any recalculate
899         * conditions are met. Also calls notifyDataSetChanged once it's finished
900         * populating {@link FolderListAdapter#mItemList}
901         */
902        private void recalculateList() {
903            final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>();
904            recalculateListAccounts(newFolderList);
905            recalculateListFolders(newFolderList);
906            mItemList = newFolderList;
907            // Ask the list to invalidate its views.
908            notifyDataSetChanged();
909        }
910
911        /**
912         * Recalculates the accounts if not null and adds them to the list.
913         *
914         * @param itemList List of drawer items to populate
915         */
916        private void recalculateListAccounts(List<DrawerItem> itemList) {
917            final Account[] allAccounts = getAllAccounts();
918            // Add all accounts and then the current account
919            final Uri currentAccountUri = getCurrentAccountUri();
920            for (final Account account : allAccounts) {
921                if (!currentAccountUri.equals(account.uri)) {
922                    final int unreadCount = mFolderWatcher.getUnreadCount(account);
923                    itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, false,
924                            mEmailToPhotoMap.get(account.name)));
925                }
926            }
927            if (mCurrentAccount == null) {
928                LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account.");
929            } else {
930                // We don't show the unread count for the current account, so set this to zero.
931                itemList.add(DrawerItem.ofAccount(mActivity, mCurrentAccount, 0, true,
932                        mEmailToPhotoMap.get(mCurrentAccount.name)));
933            }
934        }
935
936        /**
937         * Recalculates the system, recent and user label lists.
938         * This method modifies all the three lists on every single invocation.
939         *
940         * @param itemList List of drawer items to populate
941         */
942        private void recalculateListFolders(List<DrawerItem> itemList) {
943            // If we are waiting for folder initialization, we don't have any kinds of folders,
944            // just the "Waiting for initialization" item. Note, this should only be done
945            // when we're waiting for account initialization or initial sync.
946            if (isCursorInvalid(mCursor)) {
947                if(!mCurrentAccount.isAccountReady()) {
948                    itemList.add(DrawerItem.forWaitView(mActivity));
949                }
950                return;
951            }
952
953            if (!mIsSectioned) {
954                // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers.
955                do {
956                    final Folder f = mCursor.getModel();
957                    if (!isFolderTypeExcluded(f)) {
958                        itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER,
959                                mCursor.getPosition()));
960                    }
961                } while (mCursor.moveToNext());
962                return;
963            }
964
965            // Otherwise, this is an adapter for a sectioned list.
966            final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
967            final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
968            do {
969                final Folder f = mCursor.getModel();
970                if (!isFolderTypeExcluded(f)) {
971                    if (f.isProviderFolder() && f.isInbox()) {
972                        inboxFolders.add(DrawerItem.ofFolder(
973                                mActivity, f, DrawerItem.FOLDER_SYSTEM, mCursor.getPosition()));
974                    } else {
975                        allFoldersList.add(DrawerItem.ofFolder(
976                                mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition()));
977                    }
978                }
979            } while (mCursor.moveToNext());
980
981            // Add all inboxes (sectioned included) before recents.
982            addFolderSection(itemList, inboxFolders, NO_HEADER_RESOURCE);
983
984            // Add most recently folders (in alphabetical order) next.
985            addRecentsToList(itemList);
986
987            // Add the remaining provider folders followed by all labels.
988            addFolderSection(itemList, allFoldersList,  R.string.all_folders_heading);
989        }
990
991        /**
992         * Given a list of folders as {@link DrawerItem}s, add them to the item
993         * list as needed. Passing in a non-0 integer for the resource will
994         * enable a header
995         *
996         * @param destination List of drawer items to populate
997         * @param source List of drawer items representing folders to add to the drawer
998         * @param headerStringResource
999         *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
1000         *            is required, or res-id otherwise
1001         */
1002        private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source,
1003                int headerStringResource) {
1004            if (source.size() > 0) {
1005                if(headerStringResource != NO_HEADER_RESOURCE) {
1006                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
1007                }
1008                destination.addAll(source);
1009            }
1010        }
1011
1012        /**
1013         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
1014         *
1015         * @param destination List of drawer items to populate
1016         */
1017        private void addRecentsToList(List<DrawerItem> destination) {
1018            // If there are recent folders, add them.
1019            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
1020
1021            // Remove any excluded folder types
1022            if (mExcludedFolderTypes != null) {
1023                final Iterator<Folder> iterator = recentFolderList.iterator();
1024                while (iterator.hasNext()) {
1025                    if (isFolderTypeExcluded(iterator.next())) {
1026                        iterator.remove();
1027                    }
1028                }
1029            }
1030
1031            if (recentFolderList.size() > 0) {
1032                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
1033                // Recent folders are not queried for position.
1034                final int position = -1;
1035                for (Folder f : recentFolderList) {
1036                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
1037                            position));
1038                }
1039            }
1040        }
1041
1042        /**
1043         * Check if the cursor provided is valid.
1044         * @param mCursor
1045         * @return True if cursor is invalid, false otherwise
1046         */
1047        private boolean isCursorInvalid(Cursor mCursor) {
1048            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
1049                    || !mCursor.moveToFirst();
1050        }
1051
1052        @Override
1053        public void setCursor(ObjectCursor<Folder> cursor) {
1054            mCursor = cursor;
1055            recalculateList();
1056        }
1057
1058        @Override
1059        public Object getItem(int position) {
1060            return mItemList.get(position);
1061        }
1062
1063        @Override
1064        public long getItemId(int position) {
1065            return getItem(position).hashCode();
1066        }
1067
1068        @Override
1069        public final void destroy() {
1070            mRecentFolderObserver.unregisterAndDestroy();
1071        }
1072
1073        @Override
1074        public int getItemType(DrawerItem item) {
1075            return item.mType;
1076        }
1077
1078        // TODO(viki): This is strange. We have the full folder and yet we create on from scratch.
1079        @Override
1080        public Folder getFullFolder(DrawerItem folderItem) {
1081            if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
1082                return folderItem.mFolder;
1083            } else {
1084                final int pos = folderItem.mPosition;
1085                if (pos > -1 && mCursor != null && !mCursor.isClosed()
1086                        && mCursor.moveToPosition(folderItem.mPosition)) {
1087                    return mCursor.getModel();
1088                } else {
1089                    return null;
1090                }
1091            }
1092        }
1093    }
1094
1095    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
1096            implements FolderListFragmentCursorAdapter{
1097
1098        private static final int PARENT = 0;
1099        private static final int CHILD = 1;
1100        private final Uri mParentUri;
1101        private final Folder mParent;
1102        private final FolderItemView.DropHandler mDropHandler;
1103        private ObjectCursor<Folder> mCursor;
1104
1105        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
1106            super(mActivity.getActivityContext(), R.layout.folder_item);
1107            mDropHandler = mActivity;
1108            mParent = parentFolder;
1109            mParentUri = parentFolder.uri;
1110            setCursor(c);
1111        }
1112
1113        @Override
1114        public int getViewTypeCount() {
1115            // Child and Parent
1116            return 2;
1117        }
1118
1119        @Override
1120        public int getItemViewType(int position) {
1121            final Folder f = getItem(position);
1122            return f.uri.equals(mParentUri) ? PARENT : CHILD;
1123        }
1124
1125        @Override
1126        public View getView(int position, View convertView, ViewGroup parent) {
1127            final FolderItemView folderItemView;
1128            final Folder folder = getItem(position);
1129            boolean isParent = folder.uri.equals(mParentUri);
1130            if (convertView != null) {
1131                folderItemView = (FolderItemView) convertView;
1132            } else {
1133                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
1134                folderItemView = (FolderItemView) LayoutInflater.from(
1135                        mActivity.getActivityContext()).inflate(resId, null);
1136            }
1137            folderItemView.bind(folder, mDropHandler);
1138            if (folder.uri.equals(mSelectedFolderUri)) {
1139                getListView().setItemChecked(position, true);
1140                // If this is the current folder, also check to verify that the unread count
1141                // matches what the action bar shows.
1142                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1143                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1144                if (unreadCountDiffers) {
1145                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1146                }
1147            }
1148            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1149            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1150            return folderItemView;
1151        }
1152
1153        @Override
1154        public void setCursor(ObjectCursor<Folder> cursor) {
1155            mCursor = cursor;
1156            clear();
1157            if (mParent != null) {
1158                add(mParent);
1159            }
1160            if (cursor != null && cursor.getCount() > 0) {
1161                cursor.moveToFirst();
1162                do {
1163                    Folder f = cursor.getModel();
1164                    f.parent = mParent;
1165                    add(f);
1166                } while (cursor.moveToNext());
1167            }
1168        }
1169
1170        @Override
1171        public void destroy() {
1172            // Do nothing.
1173        }
1174
1175        @Override
1176        public int getItemType(DrawerItem item) {
1177            // Always returns folders for now.
1178            return DrawerItem.VIEW_FOLDER;
1179        }
1180
1181        @Override
1182        public Folder getFullFolder(DrawerItem folderItem) {
1183            final int pos = folderItem.mPosition;
1184            if (mCursor == null || mCursor.isClosed()) {
1185                return null;
1186            }
1187            if (pos > -1 && mCursor != null && !mCursor.isClosed()
1188                    && mCursor.moveToPosition(folderItem.mPosition)) {
1189                return mCursor.getModel();
1190            } else {
1191                return null;
1192            }
1193        }
1194
1195        @Override
1196        public void notifyAllAccountsChanged() {
1197            // Do nothing. We don't care about changes to all accounts.
1198        }
1199    }
1200
1201    public Folder getParentFolder() {
1202        return mParentFolder;
1203    }
1204
1205    /**
1206     * Sets the currently selected folder safely.
1207     * @param folder
1208     */
1209    private void setSelectedFolder(Folder folder) {
1210        if (folder == null) {
1211            mSelectedFolderUri = Uri.EMPTY;
1212            mCurrentFolderForUnreadCheck = null;
1213            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1214            return;
1215        }
1216        final boolean viewChanged =
1217                !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1218        mCurrentFolderForUnreadCheck = folder;
1219        mSelectedFolderUri = folder.uri;
1220        setSelectedFolderType(folder);
1221        if (mCursorAdapter != null && viewChanged) {
1222            mCursorAdapter.notifyDataSetChanged();
1223        }
1224    }
1225
1226    /**
1227     * Sets the selected folder type safely.
1228     * @param folder folder to set to.
1229     */
1230    private void setSelectedFolderType(Folder folder) {
1231        if (mSelectedFolderType == DrawerItem.UNSET) {
1232            mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM
1233                    : DrawerItem.FOLDER_USER;
1234        }
1235    }
1236
1237    /**
1238     * Sets the current account to the one provided here.
1239     * @param account the current account to set to.
1240     */
1241    private void setSelectedAccount(Account account){
1242        final boolean changed = (account != null) && (mCurrentAccount == null
1243                || !mCurrentAccount.uri.equals(account.uri));
1244        mCurrentAccount = account;
1245        if (changed) {
1246            // We no longer have proper folder objects. Let the new ones come in
1247            mCursorAdapter.setCursor(null);
1248            // If currentAccount is different from the one we set, restart the loader. Look at the
1249            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1250            // don't just do restartLoader.
1251            final LoaderManager manager = getLoaderManager();
1252            manager.destroyLoader(FOLDER_LOADER_ID);
1253            manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
1254            // An updated cursor causes the entire list to refresh. No need to refresh the list.
1255        } else if (account == null) {
1256            // This should never happen currently, but is a safeguard against a very incorrect
1257            // non-null account -> null account transition.
1258            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1259            final LoaderManager manager = getLoaderManager();
1260            manager.destroyLoader(FOLDER_LOADER_ID);
1261        }
1262    }
1263
1264    public interface FolderListSelectionListener {
1265        public void onFolderSelected(Folder folder);
1266    }
1267
1268    /**
1269     * Get whether the FolderListFragment is currently showing the hierarchy
1270     * under a single parent.
1271     */
1272    public boolean showingHierarchy() {
1273        return mParentFolder != null;
1274    }
1275
1276    /**
1277     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1278     */
1279    private boolean isFolderTypeExcluded(final Folder folder) {
1280        if (mExcludedFolderTypes == null) {
1281            return false;
1282        }
1283
1284        for (final int excludedType : mExcludedFolderTypes) {
1285            if (folder.isType(excludedType)) {
1286                return true;
1287            }
1288        }
1289
1290        return false;
1291    }
1292}
1293