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