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