FolderListFragment.java revision cb7dd16f919e6d0a8cf4acf1b1d9e6554aa9b627
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.Loader;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.Bundle;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30import android.widget.ArrayAdapter;
31import android.widget.BaseAdapter;
32import android.widget.ImageView;
33import android.widget.ListAdapter;
34import android.widget.ListView;
35
36import com.android.mail.R;
37import com.android.mail.adapter.DrawerItem;
38import com.android.mail.content.ObjectCursor;
39import com.android.mail.content.ObjectCursorLoader;
40import com.android.mail.providers.Account;
41import com.android.mail.providers.AccountObserver;
42import com.android.mail.providers.AllAccountObserver;
43import com.android.mail.providers.Folder;
44import com.android.mail.providers.FolderObserver;
45import com.android.mail.providers.FolderWatcher;
46import com.android.mail.providers.RecentFolderObserver;
47import com.android.mail.providers.UIProvider;
48import com.android.mail.providers.UIProvider.FolderType;
49import com.android.mail.utils.LogTag;
50import com.android.mail.utils.LogUtils;
51import java.util.ArrayList;
52import java.util.Iterator;
53import java.util.List;
54
55/**
56 * The folder list UI component.
57 */
58public class FolderListFragment extends ListFragment implements
59        LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
60    private static final String LOG_TAG = LogTag.getLogTag();
61    /** The parent activity */
62    private ControllableActivity mActivity;
63    /** The underlying list view */
64    private ListView mListView;
65    /** URI that points to the list of folders for the current account. */
66    private Uri mFolderListUri;
67    /** True if you want a sectioned FolderList, false otherwise. */
68    protected boolean mIsSectioned;
69    /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
70    private ArrayList<Integer> mExcludedFolderTypes;
71    /** Object that changes folders on our behalf. */
72    private FolderListSelectionListener mFolderChanger;
73    /** Object that changes accounts on our behalf */
74    private AccountController mAccountChanger;
75
76    /** The currently selected folder (the folder being viewed).  This is never null. */
77    private Uri mSelectedFolderUri = Uri.EMPTY;
78    /**
79     * The current folder from the controller.  This is meant only to check when the unread count
80     * goes out of sync and fixing it.
81     */
82    private Folder mCurrentFolderForUnreadCheck;
83    /** Parent of the current folder, or null if the current folder is not a child. */
84    private Folder mParentFolder;
85
86    private static final int FOLDER_LOADER_ID = 0;
87    /** Key to store {@link #mParentFolder}. */
88    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
89    /** Key to store {@link #mIsSectioned} */
90    private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
91    /** Key to store {@link #mFolderListUri}. */
92    private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
93    /** Key to store {@link #mExcludedFolderTypes} */
94    private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
95    /** Key to store {@link #mType} */
96    private static final String ARG_TYPE = "arg-flf-type";
97
98    /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */
99    private int mType;
100    /** This fragment is a drawer */
101    private static final int TYPE_DRAWER = 0;
102    /** This fragment is a folder tree */
103    private static final int TYPE_TREE = 1;
104
105    private static final String BUNDLE_LIST_STATE = "flf-list-state";
106    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
107    private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
108
109    private FolderListFragmentCursorAdapter mCursorAdapter;
110    /** Observer to wait for changes to the current folder so we can change the selected folder */
111    private FolderObserver mFolderObserver = null;
112    /** Listen for account changes. */
113    private AccountObserver mAccountObserver = null;
114
115    /** Listen to changes to list of all accounts */
116    private AllAccountObserver mAllAccountsObserver = null;
117    /**
118     * Type of currently selected folder: {@link DrawerItem#FOLDER_SYSTEM},
119     * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_USER}.
120     */
121    // Setting to INERT_HEADER = leaving uninitialized.
122    private int mSelectedFolderType = DrawerItem.UNSET;
123    /** The current account according to the controller */
124    private Account mCurrentAccount;
125
126    /** List of all accounts currently known */
127    private Account[] mAllAccounts;
128
129    /**
130     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
131     */
132    public FolderListFragment() {
133        super();
134    }
135
136    @Override
137    public String toString() {
138        final StringBuilder sb = new StringBuilder(super.toString());
139        sb.setLength(sb.length() - 1);
140        sb.append(" folder=");
141        sb.append(mFolderListUri);
142        sb.append(" parent=");
143        sb.append(mParentFolder);
144        sb.append(" adapterCount=");
145        sb.append(mCursorAdapter != null ? mCursorAdapter.getCount() : -1);
146        sb.append("}");
147        return sb.toString();
148    }
149
150    /**
151     * Creates a new instance of {@link FolderListFragment}. Gets the current account and current
152     * folder through observers.
153     */
154    public static FolderListFragment ofDrawer() {
155        final FolderListFragment fragment = new FolderListFragment();
156        // The drawer is always sectioned
157        final boolean isSectioned = true;
158        fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null));
159        return fragment;
160    }
161
162    /**
163     * Creates a new instance of {@link FolderListFragment}, initialized
164     * to display the folder and its immediate children.
165     * @param folder parent folder whose children are shown
166     *
167     */
168    public static FolderListFragment ofTree(Folder folder) {
169        final FolderListFragment fragment = new FolderListFragment();
170        // Trees are never sectioned.
171        final boolean isSectioned = false;
172        fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri,
173                isSectioned, null));
174        return fragment;
175    }
176
177    /**
178     * Creates a new instance of {@link FolderListFragment}, initialized
179     * to display the folder and its immediate children.
180     * @param folderListUri the URI which contains all the list of folders
181     * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
182     */
183    public static FolderListFragment ofTopLevelTree(Uri folderListUri,
184            final ArrayList<Integer> excludedFolderTypes) {
185        final FolderListFragment fragment = new FolderListFragment();
186        // Trees are never sectioned.
187        final boolean isSectioned = false;
188        fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri,
189                isSectioned, excludedFolderTypes));
190        return fragment;
191    }
192
193    /**
194     * Construct a bundle that represents the state of this fragment.
195     * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE}
196     * @param parentFolder non-null for trees, the parent of this list
197     * @param isSectioned true if this drawer is sectioned, false otherwise
198     * @param folderListUri the URI which contains all the list of folders
199     * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
200     * @return Bundle containing parentFolder, sectioned list boolean and
201     *         excluded folder types
202     */
203    private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri,
204            boolean isSectioned, final ArrayList<Integer> excludedFolderTypes) {
205        final Bundle args = new Bundle();
206        args.putInt(ARG_TYPE, type);
207        if (parentFolder != null) {
208            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
209        }
210        if (folderListUri != null) {
211            args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
212        }
213        args.putBoolean(ARG_IS_SECTIONED, isSectioned);
214        if (excludedFolderTypes != null) {
215            args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
216        }
217        return args;
218    }
219
220    @Override
221    public void onActivityCreated(Bundle savedState) {
222        super.onActivityCreated(savedState);
223        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
224        // only activity creating a ConversationListContext is a MailActivity which is of type
225        // ControllableActivity, so this cast should be safe. If this cast fails, some other
226        // activity is creating ConversationListFragments. This activity must be of type
227        // ControllableActivity.
228        final Activity activity = getActivity();
229        Folder currentFolder = null;
230        if (! (activity instanceof ControllableActivity)){
231            LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
232                    "create it. Cannot proceed.");
233        }
234        mActivity = (ControllableActivity) activity;
235        final FolderController controller = mActivity.getFolderController();
236        // Listen to folder changes in the future
237        mFolderObserver = new FolderObserver() {
238            @Override
239            public void onChanged(Folder newFolder) {
240                setSelectedFolder(newFolder);
241            }
242        };
243        if (controller != null) {
244            // Only register for selected folder updates if we have a controller.
245            currentFolder = mFolderObserver.initialize(controller);
246            mCurrentFolderForUnreadCheck = currentFolder;
247        }
248
249        // Initialize adapter for folder/heirarchical list
250        final Folder selectedFolder;
251        if (mParentFolder != null) {
252            mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
253            selectedFolder = mActivity.getHierarchyFolder();
254        } else {
255            mCursorAdapter = new FolderListAdapter(mIsSectioned);
256            selectedFolder = currentFolder;
257        }
258        // Is the selected folder fresher than the one we have restored from a bundle?
259        if (selectedFolder != null && !selectedFolder.uri.equals(mSelectedFolderUri)) {
260            setSelectedFolder(selectedFolder);
261        }
262
263        // Assign observers for current account & all accounts
264        final AccountController accountController = mActivity.getAccountController();
265        mAccountObserver = new AccountObserver() {
266            @Override
267            public void onChanged(Account newAccount) {
268                setSelectedAccount(newAccount);
269            }
270        };
271        if (accountController != null) {
272            // Current account and its observer.
273            setSelectedAccount(mAccountObserver.initialize(accountController));
274            // List of all accounts and its observer.
275            mAllAccountsObserver = new AllAccountObserver(){
276                @Override
277                public void onChanged(Account[] allAccounts) {
278                    mAllAccounts = allAccounts;
279                    mCursorAdapter.notifyAllAccountsChanged();
280                }
281            };
282            mAllAccounts = mAllAccountsObserver.initialize(accountController);
283            mAccountChanger = accountController;
284        }
285
286        mFolderChanger = mActivity.getFolderListSelectionListener();
287        if (mActivity.isFinishing()) {
288            // Activity is finishing, just bail.
289            return;
290        }
291
292        setListAdapter(mCursorAdapter);
293    }
294
295    /**
296     * Set the instance variables from the arguments provided here.
297     * @param args
298     */
299    private void setInstanceFromBundle(Bundle args) {
300        if (args == null) {
301            return;
302        }
303        mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
304        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
305        if (folderUri == null) {
306            mFolderListUri = Uri.EMPTY;
307        } else {
308            mFolderListUri = Uri.parse(folderUri);
309        }
310        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
311        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
312        mType = args.getInt(ARG_TYPE);
313    }
314
315    @Override
316    public View onCreateView(LayoutInflater inflater, ViewGroup container,
317            Bundle savedState) {
318        setInstanceFromBundle(getArguments());
319        final View rootView = inflater.inflate(R.layout.folder_list, null);
320        mListView = (ListView) rootView.findViewById(android.R.id.list);
321        mListView.setHeaderDividersEnabled(false);
322        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
323        mListView.setEmptyView(null);
324        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
325            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
326        }
327        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
328            mSelectedFolderUri = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER));
329            mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
330        } else if (mParentFolder != null) {
331            mSelectedFolderUri = mParentFolder.uri;
332            // No selected folder type required for hierarchical lists.
333        }
334
335        return rootView;
336    }
337
338    @Override
339    public void onStart() {
340        super.onStart();
341    }
342
343    @Override
344    public void onStop() {
345        super.onStop();
346    }
347
348    @Override
349    public void onPause() {
350        super.onPause();
351    }
352
353    @Override
354    public void onSaveInstanceState(Bundle outState) {
355        super.onSaveInstanceState(outState);
356        if (mListView != null) {
357            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
358        }
359        if (mSelectedFolderUri != null) {
360            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
361        }
362        outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
363    }
364
365    @Override
366    public void onDestroyView() {
367        if (mCursorAdapter != null) {
368            mCursorAdapter.destroy();
369        }
370        // Clear the adapter.
371        setListAdapter(null);
372        if (mFolderObserver != null) {
373            mFolderObserver.unregisterAndDestroy();
374            mFolderObserver = null;
375        }
376        if (mAccountObserver != null) {
377            mAccountObserver.unregisterAndDestroy();
378            mAccountObserver = null;
379        }
380        if (mAllAccountsObserver != null) {
381            mAllAccountsObserver.unregisterAndDestroy();
382            mAllAccountsObserver = null;
383        }
384        super.onDestroyView();
385    }
386
387    @Override
388    public void onListItemClick(ListView l, View v, int position, long id) {
389        viewFolderOrChangeAccount(position);
390    }
391
392    /**
393     * Display the conversation list from the folder at the position given.
394     * @param position a zero indexed position into the list.
395     */
396    private void viewFolderOrChangeAccount(int position) {
397        final Object item = getListAdapter().getItem(position);
398        LogUtils.i(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
399        final Folder folder;
400        if (item instanceof DrawerItem) {
401            final DrawerItem drawerItem = (DrawerItem) item;
402            // Could be a folder or account.
403            final int itemType = mCursorAdapter.getItemType(drawerItem);
404            if (itemType == DrawerItem.VIEW_ACCOUNT) {
405                // Account, so switch.
406                folder = null;
407                final Account account = drawerItem.mAccount;
408                mAccountChanger.changeAccount(account);
409            } else if (itemType == DrawerItem.VIEW_FOLDER) {
410                // Folder type, so change folders only.
411                folder = drawerItem.mFolder;
412                mSelectedFolderType = drawerItem.mFolderType;
413                LogUtils.i(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
414                        folder, mSelectedFolderType);
415            } else {
416                // Do nothing.
417                LogUtils.i(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
418                        + " Clicked on unset item in drawer. Offending item is " + item);
419                return;
420            }
421        } else if (item instanceof Folder) {
422            folder = (Folder) item;
423        } else if (item instanceof ObjectCursor){
424            folder = ((ObjectCursor<Folder>) item).getModel();
425        } else {
426            // Don't know how we got here.
427            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
428            folder = null;
429        }
430        if (folder != null) {
431            // Since we may be looking at hierarchical views, if we can
432            // determine the parent of the folder we have tapped, set it here.
433            // If we are looking at the folder we are already viewing, don't
434            // update its parent!
435            folder.parent = folder.equals(mParentFolder) ? null : mParentFolder;
436            // Go to the conversation list for this folder.
437            mFolderChanger.onFolderSelected(folder);
438        }
439    }
440
441    @Override
442    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
443        mListView.setEmptyView(null);
444        final Uri folderListUri;
445        if (mType == TYPE_TREE) {
446            // Folder trees, they specify a URI at construction time.
447            folderListUri = mFolderListUri;
448        } else if (mType == TYPE_DRAWER) {
449            // Drawers should have a valid account
450            if (mCurrentAccount != null) {
451                folderListUri = mCurrentAccount.folderListUri;
452            } else {
453                LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
454                return null;
455            }
456        } else {
457            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
458            return null;
459        }
460        return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
461                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
462    }
463
464    @Override
465    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
466        mCursorAdapter.setCursor(data);
467    }
468
469    @Override
470    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
471        mCursorAdapter.setCursor(null);
472    }
473
474    /**
475     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
476     */
477    private interface FolderListFragmentCursorAdapter extends ListAdapter {
478        /** Update the folder list cursor with the cursor given here. */
479        void setCursor(ObjectCursor<Folder> cursor);
480        /**
481         * Given an item, find the type of the item, which should only be {@link
482         * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
483         * @return item the type of the item.
484         */
485        int getItemType(DrawerItem item);
486        /** Get the folder associated with this item. **/
487        Folder getFullFolder(DrawerItem item);
488        /** Notify that the all accounts changed. */
489        void notifyAllAccountsChanged();
490        /** Remove all observers and destroy the object. */
491        void destroy();
492        /** Notifies the adapter that the data has changed. */
493        void notifyDataSetChanged();
494    }
495
496    /**
497     * An adapter for flat folder lists.
498     */
499    private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
500
501        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
502            @Override
503            public void onChanged() {
504                recalculateList();
505            }
506        };
507        /** No resource used for string header in folder list */
508        private static final int NO_HEADER_RESOURCE = -1;
509        /** Cache of most recently used folders */
510        private final RecentFolderList mRecentFolders;
511        /** True if the list is sectioned, false otherwise */
512        private final boolean mIsSectioned;
513        /** All the items */
514        private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
515        /** Cursor into the folder list. This might be null. */
516        private ObjectCursor<Folder> mCursor = null;
517        /** Watcher for tracking and receiving unread counts for mail */
518        private FolderWatcher mFolderWatcher = null;
519
520        /**
521         * Creates a {@link FolderListAdapter}.This is a flat folder list of all the folders for the
522         * given account.
523         * @param isSectioned TODO(viki):
524         */
525        public FolderListAdapter(boolean isSectioned) {
526            super();
527            mIsSectioned = isSectioned;
528            final RecentFolderController controller = mActivity.getRecentFolderController();
529            if (controller != null && mIsSectioned) {
530                mRecentFolders = mRecentFolderObserver.initialize(controller);
531            } else {
532                mRecentFolders = null;
533            }
534            mFolderWatcher = new FolderWatcher(mActivity, this);
535            initFolderWatcher();
536        }
537
538        @Override
539        public void notifyAllAccountsChanged() {
540            initFolderWatcher();
541            recalculateList();
542        }
543
544        /**
545         * If accounts have not yet been added to folder watcher due to various
546         * null pointer issues, add them.
547         */
548        public void initFolderWatcher() {
549            if (mAllAccounts == null) {
550                return;
551            }
552            for (final Account account : mAllAccounts) {
553                mFolderWatcher.startWatching(account.settings.defaultInbox);
554            }
555        }
556
557        @Override
558        public View getView(int position, View convertView, ViewGroup parent) {
559            final DrawerItem item = (DrawerItem) getItem(position);
560            final View view = item.getView(position, convertView, parent);
561            final int type = item.mType;
562            if (mListView != null) {
563                final boolean isSelected =
564                        item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType);
565                if (type == DrawerItem.VIEW_FOLDER) {
566                    mListView.setItemChecked(position, isSelected);
567                }
568                // If this is the current folder, also check to verify that the unread count
569                // matches what the action bar shows.
570                if (type == DrawerItem.VIEW_FOLDER
571                        && isSelected
572                        && (mCurrentFolderForUnreadCheck != null)
573                        && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
574                    ((FolderItemView) view).overrideUnreadCount(
575                            mCurrentFolderForUnreadCheck.unreadCount);
576                }
577            }
578            LogUtils.i(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item);
579            return view;
580        }
581
582        @Override
583        public int getViewTypeCount() {
584            // Accounts, headers, folders (all parts of drawer view types)
585            return DrawerItem.getViewTypes();
586        }
587
588        @Override
589        public int getItemViewType(int position) {
590            return ((DrawerItem) getItem(position)).mType;
591        }
592
593        @Override
594        public int getCount() {
595            return mItemList.size();
596        }
597
598        @Override
599        public boolean isEnabled(int position) {
600            return ((DrawerItem) getItem(position)).isItemEnabled();
601        }
602
603        private Uri getCurrentAccountUri() {
604            return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
605        }
606
607        @Override
608        public boolean areAllItemsEnabled() {
609            // The headers and current accounts are not enabled.
610            return false;
611        }
612
613        /**
614         * Returns all the recent folders from the list given here. Safe to call with a null list.
615         * @param recentList a list of all recently accessed folders.
616         * @return a valid list of folders, which are all recent folders.
617         */
618        private List<Folder> getRecentFolders(RecentFolderList recentList) {
619            final List<Folder> folderList = new ArrayList<Folder>();
620            if (recentList == null) {
621                return folderList;
622            }
623            // Get all recent folders, after removing system folders.
624            for (final Folder f : recentList.getRecentFolderList(null)) {
625                if (!f.isProviderFolder()) {
626                    folderList.add(f);
627                }
628            }
629            return folderList;
630        }
631
632        /**
633         * Responsible for verifying mCursor, and ensuring any recalculate
634         * conditions are met. Also calls notifyDataSetChanged once it's finished
635         * populating {@link FolderListAdapter#mItemList}
636         */
637        private void recalculateList() {
638            if (mAllAccountsObserver != null) {
639                mAllAccounts = mAllAccountsObserver.getAllAccounts();
640            }
641            final boolean haveAccount = (mAllAccounts != null && mAllAccounts.length > 0);
642            if (!haveAccount) {
643                // TODO(viki): How do we get a notification that we have accounts now? Currently
644                // we don't, and we should.
645                return;
646            }
647            final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>();
648            recalculateListAccounts(newFolderList);
649            recalculateListFolders(newFolderList);
650            mItemList = newFolderList;
651            // Ask the list to invalidate its views.
652            notifyDataSetChanged();
653        }
654
655        /**
656         * Recalculates the accounts if not null and adds them to the list.
657         *
658         * @param itemList List of drawer items to populate
659         */
660        private void recalculateListAccounts(List<DrawerItem> itemList) {
661            if (mAllAccounts != null) {
662                initFolderWatcher();
663                // Add all accounts and then the current account
664                final Uri currentAccountUri = getCurrentAccountUri();
665                for (final Account account : mAllAccounts) {
666                    if (!currentAccountUri.equals(account.uri)) {
667                        final int unreadCount =
668                                mFolderWatcher.getUnreadCount(account.settings.defaultInbox);
669                        itemList.add(
670                                DrawerItem.ofAccount(mActivity, account, unreadCount, false));
671                    }
672                }
673                final int unreadCount = mFolderWatcher.getUnreadCount(
674                        mCurrentAccount.settings.defaultInbox);
675                itemList.add(DrawerItem.ofAccount(mActivity, mCurrentAccount, unreadCount, true));
676            }
677            // TODO(shahrk): Add support for when there's only one account and allAccounts
678            // isn't available yet
679        }
680
681        /**
682         * Recalculates the system, recent and user label lists.
683         * This method modifies all the three lists on every single invocation.
684         *
685         * @param itemList List of drawer items to populate
686         */
687        private void recalculateListFolders(List<DrawerItem> itemList) {
688            // If we are waiting for folder initialization, we don't have any kinds of folders,
689            // just the "Waiting for initialization" item.
690            if (isCursorInvalid(mCursor)) {
691                itemList.add(DrawerItem.forWaitView(mActivity));
692                return;
693            }
694
695            if (!mIsSectioned) {
696                // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers.
697                do {
698                    final Folder f = mCursor.getModel();
699                    if (!isFolderTypeExcluded(f)) {
700                        itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER,
701                                mCursor.getPosition()));
702                    }
703                } while (mCursor.moveToNext());
704                return;
705            }
706
707            // Otherwise, this is an adapter for a sectioned list.
708            final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
709            final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
710            do {
711                final Folder f = mCursor.getModel();
712                if (!isFolderTypeExcluded(f)) {
713                    if (f.isProviderFolder() && f.isInbox()) {
714                        inboxFolders.add(DrawerItem.ofFolder(
715                                mActivity, f, DrawerItem.FOLDER_SYSTEM, mCursor.getPosition()));
716                    } else {
717                        allFoldersList.add(DrawerItem.ofFolder(
718                                mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition()));
719                    }
720                }
721            } while (mCursor.moveToNext());
722
723            // Add all inboxes (sectioned included) before recents.
724            addFolderSection(itemList, inboxFolders, NO_HEADER_RESOURCE);
725
726            // Add most recently folders (in alphabetical order) next.
727            addRecentsToList(itemList);
728
729            // Add the remaining provider folders followed by all labels.
730            addFolderSection(itemList, allFoldersList,  R.string.all_folders_heading);
731        }
732
733        /**
734         * Given a list of folders as {@link DrawerItem}s, add them to the item
735         * list as needed. Passing in a non-0 integer for the resource will
736         * enable a header
737         *
738         * @param destination List of drawer items to populate
739         * @param source List of drawer items representing folders to add to the drawer
740         * @param headerStringResource
741         *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
742         *            is required, or res-id otherwise
743         */
744        private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source,
745                int headerStringResource) {
746            if (source.size() > 0) {
747                if(headerStringResource != NO_HEADER_RESOURCE) {
748                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
749                }
750                destination.addAll(source);
751            }
752        }
753
754        /**
755         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
756         *
757         * @param destination List of drawer items to populate
758         */
759        private void addRecentsToList(List<DrawerItem> destination) {
760            // If there are recent folders, add them.
761            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
762
763            // Remove any excluded folder types
764            if (mExcludedFolderTypes != null) {
765                final Iterator<Folder> iterator = recentFolderList.iterator();
766                while (iterator.hasNext()) {
767                    if (isFolderTypeExcluded(iterator.next())) {
768                        iterator.remove();
769                    }
770                }
771            }
772
773            if (recentFolderList.size() > 0) {
774                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
775                // Recent folders are not queried for position.
776                final int position = -1;
777                for (Folder f : recentFolderList) {
778                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
779                            position));
780                }
781            }
782        }
783
784        /**
785         * Check if the cursor provided is valid.
786         * @param mCursor
787         * @return True if cursor is invalid, false otherwise
788         */
789        private boolean isCursorInvalid(Cursor mCursor) {
790            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
791                    || !mCursor.moveToFirst();
792        }
793
794        @Override
795        public void setCursor(ObjectCursor<Folder> cursor) {
796            mCursor = cursor;
797            recalculateList();
798        }
799
800        @Override
801        public Object getItem(int position) {
802            return mItemList.get(position);
803        }
804
805        @Override
806        public long getItemId(int position) {
807            return getItem(position).hashCode();
808        }
809
810        @Override
811        public final void destroy() {
812            mRecentFolderObserver.unregisterAndDestroy();
813        }
814
815        @Override
816        public int getItemType(DrawerItem item) {
817            return item.mType;
818        }
819
820        // TODO(viki): This is strange. We have the full folder and yet we create on from scratch.
821        @Override
822        public Folder getFullFolder(DrawerItem folderItem) {
823            if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
824                return folderItem.mFolder;
825            } else {
826                final int pos = folderItem.mPosition;
827                if (pos > -1 && mCursor != null && !mCursor.isClosed()
828                        && mCursor.moveToPosition(folderItem.mPosition)) {
829                    return mCursor.getModel();
830                } else {
831                    return null;
832                }
833            }
834        }
835    }
836
837    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
838            implements FolderListFragmentCursorAdapter{
839
840        private static final int PARENT = 0;
841        private static final int CHILD = 1;
842        private final Uri mParentUri;
843        private final Folder mParent;
844        private final FolderItemView.DropHandler mDropHandler;
845        private ObjectCursor<Folder> mCursor;
846
847        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
848            super(mActivity.getActivityContext(), R.layout.folder_item);
849            mDropHandler = mActivity;
850            mParent = parentFolder;
851            mParentUri = parentFolder.uri;
852            setCursor(c);
853        }
854
855        @Override
856        public int getViewTypeCount() {
857            // Child and Parent
858            return 2;
859        }
860
861        @Override
862        public int getItemViewType(int position) {
863            final Folder f = getItem(position);
864            return f.uri.equals(mParentUri) ? PARENT : CHILD;
865        }
866
867        @Override
868        public View getView(int position, View convertView, ViewGroup parent) {
869            final FolderItemView folderItemView;
870            final Folder folder = getItem(position);
871            boolean isParent = folder.uri.equals(mParentUri);
872            if (convertView != null) {
873                folderItemView = (FolderItemView) convertView;
874            } else {
875                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
876                folderItemView = (FolderItemView) LayoutInflater.from(
877                        mActivity.getActivityContext()).inflate(resId, null);
878            }
879            folderItemView.bind(folder, mDropHandler);
880            if (folder.uri.equals(mSelectedFolderUri)) {
881                getListView().setItemChecked(position, true);
882                // If this is the current folder, also check to verify that the unread count
883                // matches what the action bar shows.
884                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
885                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
886                if (unreadCountDiffers) {
887                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
888                }
889            }
890            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
891            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
892            return folderItemView;
893        }
894
895        @Override
896        public void setCursor(ObjectCursor<Folder> cursor) {
897            mCursor = cursor;
898            clear();
899            if (mParent != null) {
900                add(mParent);
901            }
902            if (cursor != null && cursor.getCount() > 0) {
903                cursor.moveToFirst();
904                do {
905                    Folder f = cursor.getModel();
906                    f.parent = mParent;
907                    add(f);
908                } while (cursor.moveToNext());
909            }
910        }
911
912        @Override
913        public void destroy() {
914            // Do nothing.
915        }
916
917        @Override
918        public int getItemType(DrawerItem item) {
919            // Always returns folders for now.
920            return DrawerItem.VIEW_FOLDER;
921        }
922
923        @Override
924        public Folder getFullFolder(DrawerItem folderItem) {
925            final int pos = folderItem.mPosition;
926            if (mCursor == null || mCursor.isClosed()) {
927                return null;
928            }
929            if (pos > -1 && mCursor != null && !mCursor.isClosed()
930                    && mCursor.moveToPosition(folderItem.mPosition)) {
931                return mCursor.getModel();
932            } else {
933                return null;
934            }
935        }
936
937        @Override
938        public void notifyAllAccountsChanged() {
939            // Do nothing. We don't care about changes to all accounts.
940        }
941    }
942
943    public Folder getParentFolder() {
944        return mParentFolder;
945    }
946
947    /**
948     * Sets the currently selected folder safely.
949     * @param folder
950     */
951    private void setSelectedFolder(Folder folder) {
952        if (folder == null) {
953            mSelectedFolderUri = Uri.EMPTY;
954            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
955            return;
956        }
957        mCurrentFolderForUnreadCheck = folder;
958        mSelectedFolderUri = folder.uri;
959        setSelectedFolderType(folder);
960        if (mCursorAdapter != null) {
961            mCursorAdapter.notifyDataSetChanged();
962        }
963    }
964
965    /**
966     * Sets the selected folder type safely.
967     * @param folder folder to set to.
968     */
969    private void setSelectedFolderType(Folder folder) {
970        if (mSelectedFolderType == DrawerItem.UNSET) {
971            mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM
972                    : DrawerItem.FOLDER_USER;
973        }
974    }
975
976    /**
977     * Sets the current account to the one provided here.
978     * @param account the current account to set to.
979     */
980    private void setSelectedAccount(Account account){
981        final boolean changed = (account != null) && (mCurrentAccount == null
982                || !mCurrentAccount.uri.equals(account.uri));
983        mCurrentAccount = account;
984        if (changed) {
985            // If currentAccount is different from the one we set, restart the loader. Look at the
986            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
987            // don't just do restartLoader.
988            final LoaderManager manager = getLoaderManager();
989            manager.destroyLoader(FOLDER_LOADER_ID);
990            manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
991            // An updated cursor causes the entire list to refresh. No need to refresh the list.
992        } else if (account == null) {
993            // This should never happen currently, but is a safeguard against a very incorrect
994            // non-null account -> null account transition.
995            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
996            final LoaderManager manager = getLoaderManager();
997            manager.destroyLoader(FOLDER_LOADER_ID);
998        }
999    }
1000
1001    public interface FolderListSelectionListener {
1002        public void onFolderSelected(Folder folder);
1003    }
1004
1005    /**
1006     * Get whether the FolderListFragment is currently showing the hierarchy
1007     * under a single parent.
1008     */
1009    public boolean showingHierarchy() {
1010        return mParentFolder != null;
1011    }
1012
1013    /**
1014     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1015     */
1016    private boolean isFolderTypeExcluded(final Folder folder) {
1017        if (mExcludedFolderTypes == null) {
1018            return false;
1019        }
1020
1021        for (final int excludedType : mExcludedFolderTypes) {
1022            if (folder.isType(excludedType)) {
1023                return true;
1024            }
1025        }
1026
1027        return false;
1028    }
1029}
1030