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