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