FolderListFragment.java revision e06c114b3ca18ae26bcb08fa46717f44d621d8ef
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
266        final Folder selectedFolder;
267        if (mParentFolder != null) {
268            mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
269            selectedFolder = mActivity.getHierarchyFolder();
270        } else {
271            mCursorAdapter = new FolderListAdapter(mIsSectioned);
272            selectedFolder = currentFolder;
273        }
274        // Is the selected folder fresher than the one we have restored from a bundle?
275        if (selectedFolder != null && !selectedFolder.uri.equals(mSelectedFolderUri)) {
276            setSelectedFolder(selectedFolder);
277        }
278
279        // Assign observers for current account & all accounts
280        final AccountController accountController = mActivity.getAccountController();
281        mAccountObserver = new AccountObserver() {
282            @Override
283            public void onChanged(Account newAccount) {
284                setSelectedAccount(newAccount);
285            }
286        };
287        if (accountController != null) {
288            // Current account and its observer.
289            setSelectedAccount(mAccountObserver.initialize(accountController));
290            // List of all accounts and its observer.
291            mAllAccountsObserver = new AllAccountObserver(){
292                @Override
293                public void onChanged(Account[] allAccounts) {
294                    mCursorAdapter.notifyAllAccountsChanged();
295                }
296            };
297            mAllAccountsObserver.initialize(accountController);
298            mAccountChanger = accountController;
299        }
300
301        mFolderChanger = mActivity.getFolderListSelectionListener();
302        if (mActivity.isFinishing()) {
303            // Activity is finishing, just bail.
304            return;
305        }
306
307        setListAdapter(mCursorAdapter);
308    }
309
310    /**
311     * Set the instance variables from the arguments provided here.
312     * @param args
313     */
314    private void setInstanceFromBundle(Bundle args) {
315        if (args == null) {
316            return;
317        }
318        mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
319        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
320        if (folderUri == null) {
321            mFolderListUri = Uri.EMPTY;
322        } else {
323            mFolderListUri = Uri.parse(folderUri);
324        }
325        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
326        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
327        mType = args.getInt(ARG_TYPE);
328    }
329
330    @Override
331    public View onCreateView(LayoutInflater inflater, ViewGroup container,
332            Bundle savedState) {
333        setInstanceFromBundle(getArguments());
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.i(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                mAccountChanger.changeAccount(account);
427            } else if (itemType == DrawerItem.VIEW_FOLDER) {
428                // Folder type, so change folders only.
429                folder = drawerItem.mFolder;
430                mSelectedFolderType = drawerItem.mFolderType;
431                LogUtils.i(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
432                        folder, mSelectedFolderType);
433            } else {
434                // Do nothing.
435                LogUtils.i(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
436                        + " Clicked on unset item in drawer. Offending item is " + item);
437                return;
438            }
439        } else if (item instanceof Folder) {
440            folder = (Folder) item;
441        } else if (item instanceof ObjectCursor){
442            folder = ((ObjectCursor<Folder>) item).getModel();
443        } else {
444            // Don't know how we got here.
445            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
446            folder = null;
447        }
448        if (folder != null) {
449            // Since we may be looking at hierarchical views, if we can
450            // determine the parent of the folder we have tapped, set it here.
451            // If we are looking at the folder we are already viewing, don't
452            // update its parent!
453            folder.parent = folder.equals(mParentFolder) ? null : mParentFolder;
454            // Go to the conversation list for this folder.
455            mFolderChanger.onFolderSelected(folder);
456        }
457    }
458
459    @Override
460    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
461        mListView.setEmptyView(null);
462        final Uri folderListUri;
463        if (mType == TYPE_TREE) {
464            // Folder trees, they specify a URI at construction time.
465            folderListUri = mFolderListUri;
466        } else if (mType == TYPE_DRAWER) {
467            // Drawers should have a valid account
468            if (mCurrentAccount != null) {
469                folderListUri = mCurrentAccount.folderListUri;
470            } else {
471                LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
472                return null;
473            }
474        } else {
475            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
476            return null;
477        }
478        return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
479                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
480    }
481
482    @Override
483    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
484        if (mCursorAdapter != null) {
485            mCursorAdapter.setCursor(data);
486        }
487    }
488
489    @Override
490    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
491        if (mCursorAdapter != null) {
492            mCursorAdapter.setCursor(null);
493        }
494    }
495
496    /**
497     *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
498     *  frequency of use.
499     * @return a list of accounts, sorted by frequency of use
500     */
501    private Account[] getAllAccounts() {
502        if (mAllAccountsObserver != null) {
503            return mAllAccountsObserver.getAllAccounts();
504        }
505        return new Account[0];
506    }
507
508    /**
509     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
510     */
511    private interface FolderListFragmentCursorAdapter extends ListAdapter {
512        /** Update the folder list cursor with the cursor given here. */
513        void setCursor(ObjectCursor<Folder> cursor);
514        /**
515         * Given an item, find the type of the item, which should only be {@link
516         * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
517         * @return item the type of the item.
518         */
519        int getItemType(DrawerItem item);
520        /** Get the folder associated with this item. **/
521        Folder getFullFolder(DrawerItem item);
522        /** Notify that the all accounts changed. */
523        void notifyAllAccountsChanged();
524        /** Remove all observers and destroy the object. */
525        void destroy();
526        /** Notifies the adapter that the data has changed. */
527        void notifyDataSetChanged();
528    }
529
530    /**
531     * An adapter for flat folder lists.
532     */
533    private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
534
535        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
536            @Override
537            public void onChanged() {
538                recalculateList();
539            }
540        };
541        /** Database columns for email address -> photo_id query */
542        private final String[] DATA_COLS = new String[] { Email.DATA, Email.PHOTO_ID };
543        /** Database columns for photo_id -> photo query */
544        private final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO };
545        /** No resource used for string header in folder list */
546        private static final int NO_HEADER_RESOURCE = -1;
547        /** Cache of most recently used folders */
548        private final RecentFolderList mRecentFolders;
549        /** True if the list is sectioned, false otherwise */
550        private final boolean mIsSectioned;
551        /** All the items */
552        private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
553        /** Cursor into the folder list. This might be null. */
554        private ObjectCursor<Folder> mCursor = null;
555        /** Watcher for tracking and receiving unread counts for mail */
556        private FolderWatcher mFolderWatcher = null;
557        /**
558         * DO NOT USE off the UI thread. Will cause ConcurrentModificationExceptions otherwise
559         *
560         * Email address -> Bitmap
561         * Caveat: at some point we will want this to be from URI to Bitmap.
562         */
563        private final HashMap<String, Bitmap> mEmailToPhotoMap = new HashMap<String, Bitmap>();
564
565        /**
566         * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
567         *
568         * @param isSectioned true if folder list is flat, false if sectioned by label group
569         */
570        public FolderListAdapter(boolean isSectioned) {
571            super();
572            mIsSectioned = isSectioned;
573            final RecentFolderController controller = mActivity.getRecentFolderController();
574            if (controller != null && mIsSectioned) {
575                mRecentFolders = mRecentFolderObserver.initialize(controller);
576            } else {
577                mRecentFolders = null;
578            }
579            mFolderWatcher = new FolderWatcher(mActivity, this);
580            mFolderWatcher.updateAccountList(getAllAccounts());
581        }
582
583        @Override
584        public void notifyAllAccountsChanged() {
585            mFolderWatcher.updateAccountList(getAllAccounts());
586            retrieveContactPhotos();
587            recalculateList();
588        }
589
590        /**
591         * AsyncTask for loading all photos that populates the email address -> Bitmap Hash Map.
592         * Does the querying and loading of photos in the background along with creating
593         * default images in case contact photos aren't found.
594         *
595         * The task is of type <String, Void, HashMap<String, Bitmap>> which corresponds to
596         * the input being an array of String and the result being a HashMap that will get merged to
597         * {@link FolderListAdapter#mEmailToPhotoMap}.
598         */
599        private class LoadPhotosTask extends AsyncTask<Account, Void, HashMap<String, Bitmap>> {
600            private final ContentResolver mResolver;
601            private final Context mContext;
602            private final int mImageSize;
603
604            /**
605             * Construct the async task for downloading the photos.
606             */
607            public LoadPhotosTask(final Context context, final int imageSize) {
608                mResolver = context.getContentResolver();
609                mContext = context;
610                mImageSize = imageSize;
611            }
612
613            /**
614             * Runs account photo retrieval in the background. Note, mEmailToPhotoMap should NOT be
615             * modified here since this is run on a background thread and not the UI thread.
616             *
617             * The {@link Account#accountFromAddresses} is used for letter tiles and is required
618             * in order to properly assign the tile to the respective account.
619             */
620            @Override
621            protected HashMap<String, Bitmap> doInBackground(final Account... allAccounts) {
622                final HashMap<String, String> addressToDisplayNameMap = new HashMap<
623                        String, String>();
624                for (final Account account : allAccounts) {
625                    addressToDisplayNameMap.put(account.name, account.accountFromAddresses);
626                }
627
628                return getAccountPhoto(addressToDisplayNameMap);
629            }
630
631            @Override
632            protected void onPostExecute(final HashMap<String, Bitmap> accountPhotos) {
633                mEmailToPhotoMap.putAll(accountPhotos);
634            }
635
636            /**
637             * Queries the database for the photos. First finds the corresponding photo_id and then
638             * proceeds to find the photo through subsequent queries for {photo_id, bytes}. If the
639             * photo is not found for the address at the end, creates a letter tile using the
640             * display name/email address and then adds that to the finished HashMap
641             *
642             * @param addresses array of email addresses (strings)
643             * @param addressToDisplayNameMap map of email addresses to display names used for
644             *              letter tiles
645             * @return map of email addresses to the corresponding photos
646             */
647            private HashMap<String, Bitmap> getAccountPhoto(
648                    final HashMap<String, String> addressToDisplayNameMap) {
649                // Columns for email address, photo_id
650                final int DATA_EMAIL_COLUMN = 0;
651                final int DATA_PHOTO_COLUMN = 1;
652                final HashMap<String, Bitmap> photoMap = new HashMap<String, Bitmap>();
653                final Set<String> addressSet = addressToDisplayNameMap.keySet();
654
655                String address;
656                long photoId;
657                Cursor photoIdsCursor = null;
658
659
660                try {
661                    // Build query for address -> photo_id
662                    final StringBuilder query = new StringBuilder().append(Data.MIMETYPE)
663                            .append("='").append(Email.CONTENT_ITEM_TYPE).append("' AND ")
664                            .append(Email.DATA).append(" IN (");
665                    appendQuestionMarks(query, addressSet.size());
666                    query.append(')');
667                    photoIdsCursor = mResolver
668                            .query(Data.CONTENT_URI, DATA_COLS, query.toString(),
669                                    addressSet.toArray(new String[addressSet.size()]), null);
670
671                    // Iterate through cursor and attempt to find a matching photo_id
672                    if (photoIdsCursor != null) {
673                        while (photoIdsCursor.moveToNext()) {
674                            // If photo_id is found, query for the encoded bitmap
675                            if (!photoIdsCursor.isNull(DATA_PHOTO_COLUMN)) {
676                                address = photoIdsCursor.getString(DATA_EMAIL_COLUMN);
677                                photoId = photoIdsCursor.getLong(DATA_PHOTO_COLUMN);
678                                final byte[] bitmapBytes = getPhotoForId(photoId);
679                                if (bitmapBytes != null && photoMap.get(address) == null) {
680                                    final Bitmap contactPhoto = BitmapUtil.decodeBitmapFromBytes(
681                                            bitmapBytes, mImageSize, mImageSize);
682                                    photoMap.put(address, contactPhoto);
683                                }
684                            }
685                        }
686                    }
687                } finally {
688                    if(photoIdsCursor != null) {
689                        photoIdsCursor.close();
690                    }
691                }
692
693                // Finally, make sure that for any addresses in the original list for which
694                // we are unable to find contact photos, we're adding the LetterTiles
695                for(final String emailAddress : addressSet) {
696                    if(!photoMap.containsKey(emailAddress)) {
697                        final Bitmap letterTile = LetterTileUtils.generateLetterTile(
698                                addressToDisplayNameMap.get(emailAddress), emailAddress, mContext,
699                                mImageSize, mImageSize);
700                        photoMap.put(emailAddress, letterTile);
701                    }
702                }
703
704                return photoMap;
705            }
706
707            /**
708             * Find the photo by running a query on the photoId provided.
709             *
710             * @param resolver ContentResolver to query on
711             * @param photoId id corresponding to the photo (if found)
712             * @return array containing photo bytes
713             */
714            private byte[] getPhotoForId(final long photoId) {
715                // Column for the photo blob
716                final int DATA_PHOTO_COLUMN = 1;
717
718                byte[] bitmapBytes = null;
719                // First try getting photos from Contacts
720                Cursor contactCursor = null;
721                try {
722                    final String[] selectionArgs = { String.valueOf(photoId) };
723                    contactCursor = mResolver.query(Data.CONTENT_URI, PHOTO_COLS,
724                            Photo._ID + " = ?", selectionArgs, null);
725                    while (contactCursor.moveToNext()) {
726                        if (!contactCursor.isNull(1)) {
727                            bitmapBytes = contactCursor.getBlob(1);
728                            break;
729                        }
730                    }
731                } finally {
732                    if (contactCursor != null) {
733                        contactCursor.close();
734                    }
735                }
736
737                // Photo not found in contacts, try profiles instead
738                if(bitmapBytes == null) {
739                    if (ContactsContract.isProfileId(photoId)) {
740                        Cursor profileCursor = null;
741                        try {
742                            profileCursor = mResolver.query(
743                                    ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
744                                    PHOTO_COLS, null, null, null);
745                            if (profileCursor != null && profileCursor.moveToFirst()) {
746                                bitmapBytes = profileCursor.getBlob(DATA_PHOTO_COLUMN);
747                            }
748                        } finally {
749                            if (profileCursor != null) {
750                                profileCursor.close();
751                            }
752                        }
753                    }
754                }
755                return bitmapBytes;
756            }
757
758            /**
759             * Prepare the Selection clause for the given query by appending question marks
760             * followed by commas (Comma-delimited list of question marks as listed by
761             * the itemCount.
762             *
763             * @param query {@link StringBuilder} representing the query thus far
764             * @param itemCount number of selection arguments to add
765             */
766            private void appendQuestionMarks(final StringBuilder query, final int itemCount) {
767                final String[] questionMarks = new String[itemCount];
768                Arrays.fill(questionMarks, "?");
769                final String selection = TextUtils.join(", ", questionMarks);
770                query.append(selection);
771            }
772        }
773
774        /**
775         * Retrieve photos for accounts that do not yet have a mapping in
776         * {@link FolderListAdapter#mEmailToPhotoMap} by querying over the database. Every account
777         * is guaranteed to have either the account contact photo, letter tile, or a default gray
778         * picture for non-English account names.
779         */
780        public synchronized void retrieveContactPhotos() {
781            final Account[] allAccounts = getAllAccounts();
782            if (allAccounts == null) {
783                return;
784            }
785            /** Fresh accounts that were recently added to the system. */
786            final HashSet<Account> freshAccounts = new HashSet<Account>();
787            /** All current account email addresses. */
788            final HashSet<String> currentEmailList = new HashSet<String>();
789            final Context context = mActivity.getActivityContext();
790            final int imageSize = context.getResources().getDimensionPixelSize(
791                    R.dimen.folder_list_item_minimum_height);
792
793            for (final Account account : allAccounts) {
794                final String email = account.name;
795                if (!mEmailToPhotoMap.containsKey(email)) {
796                    freshAccounts.add(account);
797                    // For multiple tasks running very closely together, make sure we don't end up
798                    // loading pictures for an address more than once
799                    mEmailToPhotoMap.put(email, null);
800                }
801                currentEmailList.add(email);
802            }
803            // Find all the stale accounts in our map, and remove them.
804            final Set<String> emails = ImmutableSet.copyOf(mEmailToPhotoMap.keySet());
805            for (final String email : emails) {
806                if (!currentEmailList.contains(email)) {
807                    mEmailToPhotoMap.remove(email);
808                }
809            }
810            // Fetch contact photos or letter tiles for each fresh account.
811            if (!freshAccounts.isEmpty()) {
812                new LoadPhotosTask(context, imageSize).execute(
813                        freshAccounts.toArray(new Account[freshAccounts.size()]));
814            }
815        }
816
817        @Override
818        public View getView(int position, View convertView, ViewGroup parent) {
819            final DrawerItem item = (DrawerItem) getItem(position);
820            final View view = item.getView(position, convertView, parent);
821            final int type = item.mType;
822            if (mListView != null) {
823                final boolean isSelected =
824                        item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType);
825                if (type == DrawerItem.VIEW_FOLDER) {
826                    mListView.setItemChecked(position, isSelected);
827                }
828                // If this is the current folder, also check to verify that the unread count
829                // matches what the action bar shows.
830                if (type == DrawerItem.VIEW_FOLDER
831                        && isSelected
832                        && (mCurrentFolderForUnreadCheck != null)
833                        && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
834                    ((FolderItemView) view).overrideUnreadCount(
835                            mCurrentFolderForUnreadCheck.unreadCount);
836                }
837            }
838            LogUtils.i(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item);
839            return view;
840        }
841
842        @Override
843        public int getViewTypeCount() {
844            // Accounts, headers, folders (all parts of drawer view types)
845            return DrawerItem.getViewTypes();
846        }
847
848        @Override
849        public int getItemViewType(int position) {
850            return ((DrawerItem) getItem(position)).mType;
851        }
852
853        @Override
854        public int getCount() {
855            return mItemList.size();
856        }
857
858        @Override
859        public boolean isEnabled(int position) {
860            return ((DrawerItem) getItem(position)).isItemEnabled();
861        }
862
863        private Uri getCurrentAccountUri() {
864            return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
865        }
866
867        @Override
868        public boolean areAllItemsEnabled() {
869            // We have headers and thus some items are not enabled.
870            return false;
871        }
872
873        /**
874         * Returns all the recent folders from the list given here. Safe to call with a null list.
875         * @param recentList a list of all recently accessed folders.
876         * @return a valid list of folders, which are all recent folders.
877         */
878        private List<Folder> getRecentFolders(RecentFolderList recentList) {
879            final List<Folder> folderList = new ArrayList<Folder>();
880            if (recentList == null) {
881                return folderList;
882            }
883            // Get all recent folders, after removing system folders.
884            for (final Folder f : recentList.getRecentFolderList(null)) {
885                if (!f.isProviderFolder()) {
886                    folderList.add(f);
887                }
888            }
889            return folderList;
890        }
891
892        /**
893         * Responsible for verifying mCursor, and ensuring any recalculate
894         * conditions are met. Also calls notifyDataSetChanged once it's finished
895         * populating {@link FolderListAdapter#mItemList}
896         */
897        private void recalculateList() {
898            final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>();
899            recalculateListAccounts(newFolderList);
900            recalculateListFolders(newFolderList);
901            mItemList = newFolderList;
902            // Ask the list to invalidate its views.
903            notifyDataSetChanged();
904        }
905
906        /**
907         * Recalculates the accounts if not null and adds them to the list.
908         *
909         * @param itemList List of drawer items to populate
910         */
911        private void recalculateListAccounts(List<DrawerItem> itemList) {
912            final Account[] allAccounts = getAllAccounts();
913            // Add all accounts and then the current account
914            final Uri currentAccountUri = getCurrentAccountUri();
915            for (final Account account : allAccounts) {
916                if (!currentAccountUri.equals(account.uri)) {
917                    final int unreadCount = mFolderWatcher.getUnreadCount(account);
918                    itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, false,
919                            mEmailToPhotoMap.get(account.name)));
920                }
921            }
922            if (mCurrentAccount == null) {
923                LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account.");
924            } else {
925                // We don't show the unread count for the current account, so set this to zero.
926                itemList.add(DrawerItem.ofAccount(mActivity, mCurrentAccount, 0, true,
927                        mEmailToPhotoMap.get(mCurrentAccount.name)));
928            }
929        }
930
931        /**
932         * Recalculates the system, recent and user label lists.
933         * This method modifies all the three lists on every single invocation.
934         *
935         * @param itemList List of drawer items to populate
936         */
937        private void recalculateListFolders(List<DrawerItem> itemList) {
938            // If we are waiting for folder initialization, we don't have any kinds of folders,
939            // just the "Waiting for initialization" item. Note, this should only be done
940            // when we're waiting for account initialization or initial sync.
941            if (isCursorInvalid(mCursor)) {
942                if(!mCurrentAccount.isAccountReady()) {
943                    itemList.add(DrawerItem.forWaitView(mActivity));
944                }
945                return;
946            }
947
948            if (!mIsSectioned) {
949                // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers.
950                do {
951                    final Folder f = mCursor.getModel();
952                    if (!isFolderTypeExcluded(f)) {
953                        itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER,
954                                mCursor.getPosition()));
955                    }
956                } while (mCursor.moveToNext());
957                return;
958            }
959
960            // Otherwise, this is an adapter for a sectioned list.
961            final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
962            final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
963            do {
964                final Folder f = mCursor.getModel();
965                if (!isFolderTypeExcluded(f)) {
966                    if (f.isProviderFolder() && f.isInbox()) {
967                        inboxFolders.add(DrawerItem.ofFolder(
968                                mActivity, f, DrawerItem.FOLDER_SYSTEM, mCursor.getPosition()));
969                    } else {
970                        allFoldersList.add(DrawerItem.ofFolder(
971                                mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition()));
972                    }
973                }
974            } while (mCursor.moveToNext());
975
976            // Add all inboxes (sectioned included) before recents.
977            addFolderSection(itemList, inboxFolders, NO_HEADER_RESOURCE);
978
979            // Add most recently folders (in alphabetical order) next.
980            addRecentsToList(itemList);
981
982            // Add the remaining provider folders followed by all labels.
983            addFolderSection(itemList, allFoldersList,  R.string.all_folders_heading);
984        }
985
986        /**
987         * Given a list of folders as {@link DrawerItem}s, add them to the item
988         * list as needed. Passing in a non-0 integer for the resource will
989         * enable a header
990         *
991         * @param destination List of drawer items to populate
992         * @param source List of drawer items representing folders to add to the drawer
993         * @param headerStringResource
994         *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
995         *            is required, or res-id otherwise
996         */
997        private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source,
998                int headerStringResource) {
999            if (source.size() > 0) {
1000                if(headerStringResource != NO_HEADER_RESOURCE) {
1001                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
1002                }
1003                destination.addAll(source);
1004            }
1005        }
1006
1007        /**
1008         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
1009         *
1010         * @param destination List of drawer items to populate
1011         */
1012        private void addRecentsToList(List<DrawerItem> destination) {
1013            // If there are recent folders, add them.
1014            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
1015
1016            // Remove any excluded folder types
1017            if (mExcludedFolderTypes != null) {
1018                final Iterator<Folder> iterator = recentFolderList.iterator();
1019                while (iterator.hasNext()) {
1020                    if (isFolderTypeExcluded(iterator.next())) {
1021                        iterator.remove();
1022                    }
1023                }
1024            }
1025
1026            if (recentFolderList.size() > 0) {
1027                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
1028                // Recent folders are not queried for position.
1029                final int position = -1;
1030                for (Folder f : recentFolderList) {
1031                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
1032                            position));
1033                }
1034            }
1035        }
1036
1037        /**
1038         * Check if the cursor provided is valid.
1039         * @param mCursor
1040         * @return True if cursor is invalid, false otherwise
1041         */
1042        private boolean isCursorInvalid(Cursor mCursor) {
1043            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
1044                    || !mCursor.moveToFirst();
1045        }
1046
1047        @Override
1048        public void setCursor(ObjectCursor<Folder> cursor) {
1049            mCursor = cursor;
1050            recalculateList();
1051        }
1052
1053        @Override
1054        public Object getItem(int position) {
1055            return mItemList.get(position);
1056        }
1057
1058        @Override
1059        public long getItemId(int position) {
1060            return getItem(position).hashCode();
1061        }
1062
1063        @Override
1064        public final void destroy() {
1065            mRecentFolderObserver.unregisterAndDestroy();
1066        }
1067
1068        @Override
1069        public int getItemType(DrawerItem item) {
1070            return item.mType;
1071        }
1072
1073        // TODO(viki): This is strange. We have the full folder and yet we create on from scratch.
1074        @Override
1075        public Folder getFullFolder(DrawerItem folderItem) {
1076            if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
1077                return folderItem.mFolder;
1078            } else {
1079                final int pos = folderItem.mPosition;
1080                if (pos > -1 && mCursor != null && !mCursor.isClosed()
1081                        && mCursor.moveToPosition(folderItem.mPosition)) {
1082                    return mCursor.getModel();
1083                } else {
1084                    return null;
1085                }
1086            }
1087        }
1088    }
1089
1090    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
1091            implements FolderListFragmentCursorAdapter{
1092
1093        private static final int PARENT = 0;
1094        private static final int CHILD = 1;
1095        private final Uri mParentUri;
1096        private final Folder mParent;
1097        private final FolderItemView.DropHandler mDropHandler;
1098        private ObjectCursor<Folder> mCursor;
1099
1100        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
1101            super(mActivity.getActivityContext(), R.layout.folder_item);
1102            mDropHandler = mActivity;
1103            mParent = parentFolder;
1104            mParentUri = parentFolder.uri;
1105            setCursor(c);
1106        }
1107
1108        @Override
1109        public int getViewTypeCount() {
1110            // Child and Parent
1111            return 2;
1112        }
1113
1114        @Override
1115        public int getItemViewType(int position) {
1116            final Folder f = getItem(position);
1117            return f.uri.equals(mParentUri) ? PARENT : CHILD;
1118        }
1119
1120        @Override
1121        public View getView(int position, View convertView, ViewGroup parent) {
1122            final FolderItemView folderItemView;
1123            final Folder folder = getItem(position);
1124            boolean isParent = folder.uri.equals(mParentUri);
1125            if (convertView != null) {
1126                folderItemView = (FolderItemView) convertView;
1127            } else {
1128                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
1129                folderItemView = (FolderItemView) LayoutInflater.from(
1130                        mActivity.getActivityContext()).inflate(resId, null);
1131            }
1132            folderItemView.bind(folder, mDropHandler);
1133            if (folder.uri.equals(mSelectedFolderUri)) {
1134                getListView().setItemChecked(position, true);
1135                // If this is the current folder, also check to verify that the unread count
1136                // matches what the action bar shows.
1137                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1138                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1139                if (unreadCountDiffers) {
1140                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1141                }
1142            }
1143            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1144            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1145            return folderItemView;
1146        }
1147
1148        @Override
1149        public void setCursor(ObjectCursor<Folder> cursor) {
1150            mCursor = cursor;
1151            clear();
1152            if (mParent != null) {
1153                add(mParent);
1154            }
1155            if (cursor != null && cursor.getCount() > 0) {
1156                cursor.moveToFirst();
1157                do {
1158                    Folder f = cursor.getModel();
1159                    f.parent = mParent;
1160                    add(f);
1161                } while (cursor.moveToNext());
1162            }
1163        }
1164
1165        @Override
1166        public void destroy() {
1167            // Do nothing.
1168        }
1169
1170        @Override
1171        public int getItemType(DrawerItem item) {
1172            // Always returns folders for now.
1173            return DrawerItem.VIEW_FOLDER;
1174        }
1175
1176        @Override
1177        public Folder getFullFolder(DrawerItem folderItem) {
1178            final int pos = folderItem.mPosition;
1179            if (mCursor == null || mCursor.isClosed()) {
1180                return null;
1181            }
1182            if (pos > -1 && mCursor != null && !mCursor.isClosed()
1183                    && mCursor.moveToPosition(folderItem.mPosition)) {
1184                return mCursor.getModel();
1185            } else {
1186                return null;
1187            }
1188        }
1189
1190        @Override
1191        public void notifyAllAccountsChanged() {
1192            // Do nothing. We don't care about changes to all accounts.
1193        }
1194    }
1195
1196    public Folder getParentFolder() {
1197        return mParentFolder;
1198    }
1199
1200    /**
1201     * Sets the currently selected folder safely.
1202     * @param folder
1203     */
1204    private void setSelectedFolder(Folder folder) {
1205        if (folder == null) {
1206            mSelectedFolderUri = Uri.EMPTY;
1207            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1208            return;
1209        }
1210        mCurrentFolderForUnreadCheck = folder;
1211        mSelectedFolderUri = folder.uri;
1212        setSelectedFolderType(folder);
1213        final boolean viewChanged =
1214                !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1215        if (mCursorAdapter != null && viewChanged) {
1216            mCursorAdapter.notifyDataSetChanged();
1217        }
1218    }
1219
1220    /**
1221     * Sets the selected folder type safely.
1222     * @param folder folder to set to.
1223     */
1224    private void setSelectedFolderType(Folder folder) {
1225        if (mSelectedFolderType == DrawerItem.UNSET) {
1226            mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM
1227                    : DrawerItem.FOLDER_USER;
1228        }
1229    }
1230
1231    /**
1232     * Sets the current account to the one provided here.
1233     * @param account the current account to set to.
1234     */
1235    private void setSelectedAccount(Account account){
1236        final boolean changed = (account != null) && (mCurrentAccount == null
1237                || !mCurrentAccount.uri.equals(account.uri));
1238        mCurrentAccount = account;
1239        if (changed) {
1240            // We no longer have proper folder objects. Let the new ones come in
1241            mCursorAdapter.setCursor(null);
1242            // If currentAccount is different from the one we set, restart the loader. Look at the
1243            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1244            // don't just do restartLoader.
1245            final LoaderManager manager = getLoaderManager();
1246            manager.destroyLoader(FOLDER_LOADER_ID);
1247            manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
1248            // An updated cursor causes the entire list to refresh. No need to refresh the list.
1249        } else if (account == null) {
1250            // This should never happen currently, but is a safeguard against a very incorrect
1251            // non-null account -> null account transition.
1252            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1253            final LoaderManager manager = getLoaderManager();
1254            manager.destroyLoader(FOLDER_LOADER_ID);
1255        }
1256    }
1257
1258    public interface FolderListSelectionListener {
1259        public void onFolderSelected(Folder folder);
1260    }
1261
1262    /**
1263     * Get whether the FolderListFragment is currently showing the hierarchy
1264     * under a single parent.
1265     */
1266    public boolean showingHierarchy() {
1267        return mParentFolder != null;
1268    }
1269
1270    /**
1271     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1272     */
1273    private boolean isFolderTypeExcluded(final Folder folder) {
1274        if (mExcludedFolderTypes == null) {
1275            return false;
1276        }
1277
1278        for (final int excludedType : mExcludedFolderTypes) {
1279            if (folder.isType(excludedType)) {
1280                return true;
1281            }
1282        }
1283
1284        return false;
1285    }
1286}
1287