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