FolderListFragment.java revision cf44d22d342720f9f806d55373c2a3cc974c07c0
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.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.app.Activity;
23import android.app.ListFragment;
24import android.app.LoaderManager;
25import android.content.Loader;
26import android.net.Uri;
27import android.os.Bundle;
28import android.support.v4.widget.DrawerLayout;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.animation.DecelerateInterpolator;
33import android.view.animation.Interpolator;
34import android.widget.AbsListView;
35import android.widget.ArrayAdapter;
36import android.widget.BaseAdapter;
37import android.widget.ImageView;
38import android.widget.ListAdapter;
39import android.widget.ListView;
40import android.widget.TextView;
41
42import com.android.oldbitmap.AltBitmapCache;
43import com.android.oldbitmap.BitmapCache;
44import com.android.mail.R;
45import com.android.mail.adapter.DrawerItem;
46import com.android.mail.analytics.Analytics;
47import com.android.mail.bitmap.ContactResolver;
48import com.android.mail.browse.MergedAdapter;
49import com.android.mail.browse.ScrollIndicatorsView;
50import com.android.mail.content.ObjectCursor;
51import com.android.mail.content.ObjectCursorLoader;
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.FolderUri;
63import com.android.mail.utils.LogTag;
64import com.android.mail.utils.LogUtils;
65import com.android.mail.utils.Utils;
66import com.google.common.collect.Lists;
67
68import java.util.ArrayList;
69import java.util.Iterator;
70import java.util.List;
71
72/**
73 * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
74 * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
75 * in a drawer along with the list of folders.
76 *
77 * This class has the following use-cases:
78 * <ul>
79 *     <li>
80 *         Show a list of accounts and a divided list of folders. In this case, the list shows
81 *         Accounts, Inboxes, Recent Folders, All folders, Help, and Feedback.
82 *         Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
83 *         folders switches folders. Tapping on Help takes the user to HTML help pages. Tapping on
84 *         Feedback takes the user to a screen for submitting text and a screenshot of the
85 *         application to a feedback system.
86 *         This is created through XML resources as a {@link DrawerFragment}. Since it is created
87 *         through resources, it receives all arguments through callbacks.
88 *     </li>
89 *     <li>
90 *         Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
91 *         Drafts, Starred, and any user-created folders. For providers that allow nested folders,
92 *         this will only show the folders at the top-level.
93 *         <br /> Tapping on a parent folder creates a new fragment with the child folders at
94 *         that level.
95 *     </li>
96 *     <li>
97 *         Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
98 *         {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
99 *         any folder for a given account.
100 *     </li>
101 * </ul>
102 */
103public class FolderListFragment extends ListFragment implements
104        LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
105    private static final String LOG_TAG = LogTag.getLogTag();
106    /** The parent activity */
107    private ControllableActivity mActivity;
108    /** The underlying list view */
109    private ListView mListView;
110    private ViewGroup mFloatyFooter;
111    /** URI that points to the list of folders for the current account. */
112    private Uri mFolderListUri;
113    /**
114     * True if you want a divided FolderList. A divided folder list shows the following groups:
115     * Inboxes, Recent Folders, All folders.
116     *
117     * An undivided FolderList shows all folders without any divisions and without recent folders.
118     * This is true only for the drawer: for all others it is false.
119     */
120    protected boolean mIsDivided = false;
121    /** True if the folder list belongs to a folder selection activity (one account only) */
122    protected boolean mHideAccounts = true;
123    /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
124    private ArrayList<Integer> mExcludedFolderTypes;
125    /** Object that changes folders on our behalf. */
126    private FolderSelector mFolderChanger;
127    /** Object that changes accounts on our behalf */
128    private AccountController mAccountController;
129
130    /** The currently selected folder (the folder being viewed).  This is never null. */
131    private FolderUri mSelectedFolderUri = FolderUri.EMPTY;
132    /**
133     * The current folder from the controller.  This is meant only to check when the unread count
134     * goes out of sync and fixing it.
135     */
136    private Folder mCurrentFolderForUnreadCheck;
137    /** Parent of the current folder, or null if the current folder is not a child. */
138    private Folder mParentFolder;
139
140    private static final int FOLDER_LIST_LOADER_ID = 0;
141    /** Loader id for the list of all folders in the account */
142    private static final int ALL_FOLDER_LIST_LOADER_ID = 1;
143    /** Key to store {@link #mParentFolder}. */
144    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
145    /** Key to store {@link #mFolderListUri}. */
146    private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
147    /** Key to store {@link #mExcludedFolderTypes} */
148    private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
149
150    private static final String BUNDLE_LIST_STATE = "flf-list-state";
151    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
152    private static final String BUNDLE_SELECTED_ITEM_TYPE = "flf-selected-item-type";
153    private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
154    private static final String BUNDLE_INBOX_PRESENT = "flf-inbox-present";
155
156    /** Number of avatars to we whould like to fit in the avatar cache */
157    private static final int IMAGE_CACHE_COUNT = 10;
158    /**
159     * This is the fractional portion of the total cache size above that's dedicated to non-pooled
160     * bitmaps. (This is basically the portion of cache dedicated to GIFs.)
161     */
162    private static final float AVATAR_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f;
163    /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */
164    private static final int AVATAR_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100;
165
166
167    /** Adapter used by the list that wraps both the folder adapter and the accounts adapter. */
168    private MergedAdapter<ListAdapter> mMergedAdapter;
169    /** Adapter containing the list of accounts. */
170    protected AccountsAdapter mAccountsAdapter;
171    /** Adapter containing the list of folders and, optionally, headers and the wait view. */
172    private FolderListFragmentCursorAdapter mFolderAdapter;
173    /** Adapter containing the Help and Feedback views */
174    private FooterAdapter mFooterAdapter;
175    /** Observer to wait for changes to the current folder so we can change the selected folder */
176    private FolderObserver mFolderObserver = null;
177    /** Listen for account changes. */
178    private AccountObserver mAccountObserver = null;
179    /** Listen for account changes. */
180    private DrawerClosedObserver mDrawerObserver = null;
181    /** Listen to changes to list of all accounts */
182    private AllAccountObserver mAllAccountsObserver = null;
183    /**
184     * Type of currently selected folder: {@link DrawerItem#FOLDER_INBOX},
185     * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_OTHER}.
186     * Set as {@link DrawerItem#UNSET} to begin with, as there is nothing selected yet.
187     */
188    private int mSelectedDrawerItemType = DrawerItem.UNSET;
189
190    /** The FolderType of the selected folder {@link FolderType} */
191    private int mSelectedFolderType = FolderType.INBOX;
192    /** The current account according to the controller */
193    protected Account mCurrentAccount;
194    /** The account we will change to once the drawer (if any) is closed */
195    private Account mNextAccount = null;
196    /** The folder we will change to once the drawer (if any) is closed */
197    private Folder mNextFolder = null;
198    /** Watcher for tracking and receiving unread counts for mail */
199    private FolderWatcher mFolderWatcher = null;
200    private boolean mRegistered = false;
201
202    private final DrawerStateListener mDrawerListener = new DrawerStateListener();
203
204    private boolean mShowFooter;
205
206    private boolean mFooterIsAnimating = false;
207
208    private static final Interpolator INTERPOLATOR_SHOW_FLOATY = new DecelerateInterpolator(2.0f);
209    private static final Interpolator INTERPOLATOR_HIDE_FLOATY = new DecelerateInterpolator();
210
211    private BitmapCache mImagesCache;
212    private ContactResolver mContactResolver;
213
214    private boolean mInboxPresent;
215
216    /**
217     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
218     */
219    public FolderListFragment() {
220        super();
221    }
222
223    @Override
224    public String toString() {
225        final StringBuilder sb = new StringBuilder(super.toString());
226        sb.setLength(sb.length() - 1);
227        sb.append(" folder=");
228        sb.append(mFolderListUri);
229        sb.append(" parent=");
230        sb.append(mParentFolder);
231        sb.append(" adapterCount=");
232        sb.append(mMergedAdapter != null ? mMergedAdapter.getCount() : -1);
233        sb.append("}");
234        return sb.toString();
235    }
236
237    /**
238     * Creates a new instance of {@link FolderListFragment}, initialized
239     * to display the folder and its immediate children.
240     * @param folder parent folder whose children are shown
241     *
242     */
243    public static FolderListFragment ofTree(Folder folder) {
244        final FolderListFragment fragment = new FolderListFragment();
245        fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
246        return fragment;
247    }
248
249    /**
250     * Creates a new instance of {@link FolderListFragment}, initialized
251     * to display the top level: where we have no parent folder, but we have a list of folders
252     * from the account.
253     * @param folderListUri the URI which contains all the list of folders
254     * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
255     */
256    public static FolderListFragment ofTopLevelTree(Uri folderListUri,
257            final ArrayList<Integer> excludedFolderTypes) {
258        final FolderListFragment fragment = new FolderListFragment();
259        fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
260        return fragment;
261    }
262
263    /**
264     * Construct a bundle that represents the state of this fragment.
265     *
266     * @param parentFolder non-null for trees, the parent of this list
267     * @param folderListUri the URI which contains all the list of folders
268     * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
269     * @return Bundle containing parentFolder, divided list boolean and
270     *         excluded folder types
271     */
272    private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
273            final ArrayList<Integer> excludedFolderTypes) {
274        final Bundle args = new Bundle(3);
275        if (parentFolder != null) {
276            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
277        }
278        if (folderListUri != null) {
279            args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
280        }
281        if (excludedFolderTypes != null) {
282            args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
283        }
284        return args;
285    }
286
287    @Override
288    public void onActivityCreated(Bundle savedState) {
289        super.onActivityCreated(savedState);
290        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
291        // only activity creating a ConversationListContext is a MailActivity which is of type
292        // ControllableActivity, so this cast should be safe. If this cast fails, some other
293        // activity is creating ConversationListFragments. This activity must be of type
294        // ControllableActivity.
295        final Activity activity = getActivity();
296        if (!(activity instanceof ControllableActivity)) {
297            LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
298                    "create it. Cannot proceed.");
299            return;
300        }
301        mActivity = (ControllableActivity) activity;
302        final FolderController controller = mActivity.getFolderController();
303        // Listen to folder changes in the future
304        mFolderObserver = new FolderObserver() {
305            @Override
306            public void onChanged(Folder newFolder) {
307                setSelectedFolder(newFolder);
308            }
309        };
310        final Folder currentFolder;
311        if (controller != null) {
312            // Only register for selected folder updates if we have a controller.
313            currentFolder = mFolderObserver.initialize(controller);
314            mCurrentFolderForUnreadCheck = currentFolder;
315        } else {
316            currentFolder = null;
317        }
318
319        // Initialize adapter for folder/hierarchical list.  Note this relies on
320        // mActivity being initialized.
321        final Folder selectedFolder;
322        if (mParentFolder != null) {
323            mFolderAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
324            selectedFolder = mActivity.getHierarchyFolder();
325        } else {
326            mFolderAdapter = new FolderAdapter(mIsDivided);
327            selectedFolder = currentFolder;
328        }
329
330        mAccountsAdapter = newAccountsAdapter();
331        mFooterAdapter = new FooterAdapter();
332        updateFloatyFooter();
333
334        // Is the selected folder fresher than the one we have restored from a bundle?
335        if (selectedFolder != null
336                && !selectedFolder.folderUri.equals(mSelectedFolderUri)) {
337            setSelectedFolder(selectedFolder);
338        }
339
340        // Assign observers for current account & all accounts
341        final AccountController accountController = mActivity.getAccountController();
342        mAccountObserver = new AccountObserver() {
343            @Override
344            public void onChanged(Account newAccount) {
345                setSelectedAccount(newAccount);
346            }
347        };
348        mFolderChanger = mActivity.getFolderSelector();
349        if (accountController != null) {
350            // Current account and its observer.
351            setSelectedAccount(mAccountObserver.initialize(accountController));
352            // List of all accounts and its observer.
353            mAllAccountsObserver = new AllAccountObserver(){
354                @Override
355                public void onChanged(Account[] allAccounts) {
356                    if (!mRegistered && mAccountController != null) {
357                        // TODO(viki): Round-about way of setting the watcher. http://b/8750610
358                        mAccountController.setFolderWatcher(mFolderWatcher);
359                        mRegistered = true;
360                    }
361                    mFolderWatcher.updateAccountList(getAllAccounts());
362                    mAccountsAdapter.rebuildAccountList();
363                }
364            };
365            mAllAccountsObserver.initialize(accountController);
366            mAccountController = accountController;
367
368            // Observer for when the drawer is closed
369            mDrawerObserver = new DrawerClosedObserver() {
370                @Override
371                public void onDrawerClosed() {
372                    // First, check if there's a folder to change to
373                    if (mNextFolder != null) {
374                        mFolderChanger.onFolderSelected(mNextFolder);
375                        mNextFolder = null;
376                    }
377                    // Next, check if there's an account to change to
378                    if (mNextAccount != null) {
379                        mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
380                        mNextAccount = null;
381                    }
382                }
383            };
384            mDrawerObserver.initialize(accountController);
385
386            final DrawerController dc = mActivity.getDrawerController();
387            if (dc != null) {
388                dc.registerDrawerListener(mDrawerListener);
389            }
390        }
391
392        if (mActivity.isFinishing()) {
393            // Activity is finishing, just bail.
394            return;
395        }
396
397        mListView.setChoiceMode(getListViewChoiceMode());
398
399        mMergedAdapter = new MergedAdapter<ListAdapter>();
400        mMergedAdapter.setAdapters(mAccountsAdapter, mFolderAdapter, mFooterAdapter);
401
402        mFolderWatcher = new FolderWatcher(mActivity, mAccountsAdapter);
403        mFolderWatcher.updateAccountList(getAllAccounts());
404
405        setListAdapter(mMergedAdapter);
406
407        final int avatarSize = getActivity().getResources().getDimensionPixelSize(
408                R.dimen.account_avatar_dimension);
409
410        mImagesCache = new AltBitmapCache(Utils.isLowRamDevice(getActivity()) ?
411                0 : avatarSize * avatarSize * IMAGE_CACHE_COUNT,
412                AVATAR_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION,
413                AVATAR_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY);
414        mContactResolver = new ContactResolver(getActivity().getContentResolver(),
415                mImagesCache);
416    }
417
418    /**
419     * Set the instance variables from the arguments provided here.
420     * @param args bundle of arguments with keys named ARG_*
421     */
422    private void setInstanceFromBundle(Bundle args) {
423        if (args == null) {
424            return;
425        }
426        mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
427        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
428        if (folderUri != null) {
429            mFolderListUri = Uri.parse(folderUri);
430        }
431        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
432    }
433
434    @Override
435    public View onCreateView(LayoutInflater inflater, ViewGroup container,
436            Bundle savedState) {
437        setInstanceFromBundle(getArguments());
438
439        final View rootView = inflater.inflate(R.layout.folder_list, null);
440        final ScrollNotifyingListView lv = (ScrollNotifyingListView) rootView.findViewById(
441                android.R.id.list);
442        mListView = lv;
443        mListView.setEmptyView(null);
444        mListView.setDivider(null);
445        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
446            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
447        }
448        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
449            mSelectedFolderUri =
450                    new FolderUri(Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)));
451            mSelectedDrawerItemType = savedState.getInt(BUNDLE_SELECTED_ITEM_TYPE);
452            mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
453        } else if (mParentFolder != null) {
454            mSelectedFolderUri = mParentFolder.folderUri;
455            // No selected folder type required for hierarchical lists.
456        }
457        if (savedState != null) {
458            mInboxPresent = savedState.getBoolean(BUNDLE_INBOX_PRESENT, true);
459        } else {
460            mInboxPresent = true;
461        }
462
463        // only show the footer on drawered layouts, and definitely not in the folder selection
464        // activity
465        mShowFooter = !mHideAccounts
466                && getResources().getBoolean(R.bool.show_help_and_feedback_in_drawer);
467
468        // TODO: clean this up when things are more certain
469        final boolean floatyFooterEnabled = false;
470                // mShowFooter && getResources().getBoolean(R.bool.show_drawer_floaty_footer);
471        final ViewGroup ff = (ViewGroup) rootView.findViewById(R.id.floaty_footer);
472        ff.setVisibility(floatyFooterEnabled ? View.VISIBLE : View.GONE);
473        if (floatyFooterEnabled) {
474            mFloatyFooter = ff;
475        }
476
477        final ScrollIndicatorsView scrollbars =
478                (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
479        scrollbars.setSourceView(lv);
480
481        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
482
483            private int mLastState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
484
485            @Override
486            public void onScrollStateChanged(AbsListView view, int scrollState) {
487                mLastState = scrollState;
488            }
489
490            @Override
491            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
492                    int totalItemCount) {
493                // have the floaty footer react only after some non-zero scroll movement
494                if (mLastState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
495                    // ignore onScrolls that are generated as the list data is updated
496                    return;
497                }
498
499                if (mListView.canScrollVertically(-1)) {
500                    // typically we want scroll motion to hide the floaty footer
501                    hideFloatyFooter();
502                } else {
503                    // except at the top, when we should show it
504                    showFloatyFooter(false /* onlyWhenClosed */);
505                }
506            }
507
508        });
509
510        return rootView;
511    }
512
513    @Override
514    public void onStart() {
515        super.onStart();
516    }
517
518    @Override
519    public void onStop() {
520        super.onStop();
521    }
522
523    @Override
524    public void onPause() {
525        super.onPause();
526    }
527
528    @Override
529    public void onSaveInstanceState(Bundle outState) {
530        super.onSaveInstanceState(outState);
531        if (mListView != null) {
532            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
533        }
534        if (mSelectedFolderUri != null) {
535            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
536        }
537        outState.putInt(BUNDLE_SELECTED_ITEM_TYPE, mSelectedDrawerItemType);
538        outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
539        outState.putBoolean(BUNDLE_INBOX_PRESENT, mInboxPresent);
540    }
541
542    @Override
543    public void onDestroyView() {
544        if (mFolderAdapter != null) {
545            mFolderAdapter.destroy();
546        }
547        // Clear the adapter.
548        setListAdapter(null);
549        if (mFolderObserver != null) {
550            mFolderObserver.unregisterAndDestroy();
551            mFolderObserver = null;
552        }
553        if (mAccountObserver != null) {
554            mAccountObserver.unregisterAndDestroy();
555            mAccountObserver = null;
556        }
557        if (mAllAccountsObserver != null) {
558            mAllAccountsObserver.unregisterAndDestroy();
559            mAllAccountsObserver = null;
560        }
561        if (mDrawerObserver != null) {
562            mDrawerObserver.unregisterAndDestroy();
563            mDrawerObserver = null;
564        }
565        super.onDestroyView();
566
567        if (mActivity != null) {
568            final DrawerController dc = mActivity.getDrawerController();
569            if (dc != null) {
570                dc.unregisterDrawerListener(mDrawerListener);
571            }
572        }
573    }
574
575    @Override
576    public void onListItemClick(ListView l, View v, int position, long id) {
577        viewFolderOrChangeAccount(position);
578    }
579
580    private Folder getDefaultInbox(Account account) {
581        if (account == null || mFolderWatcher == null) {
582            return null;
583        }
584        return mFolderWatcher.getDefaultInbox(account);
585    }
586
587    protected int getUnreadCount(Account account) {
588        if (account == null || mFolderWatcher == null) {
589            return 0;
590        }
591        return mFolderWatcher.getUnreadCount(account);
592    }
593
594    protected void changeAccount(final Account account) {
595        // Switching accounts takes you to the default inbox for that account.
596        mSelectedDrawerItemType = DrawerItem.FOLDER_INBOX;
597        mSelectedFolderType = FolderType.INBOX;
598        mNextAccount = account;
599        mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
600        Analytics.getInstance().sendEvent("switch_account", "drawer_account_switch", null, 0);
601    }
602
603    /**
604     * Display the conversation list from the folder at the position given.
605     * @param position a zero indexed position into the list.
606     */
607    protected void viewFolderOrChangeAccount(int position) {
608        final Object item = getListAdapter().getItem(position);
609        LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
610        final Folder folder;
611        int folderType = DrawerItem.UNSET;
612
613        if (item instanceof DrawerItem) {
614            final DrawerItem drawerItem = (DrawerItem) item;
615            // Could be a folder or account.
616            final int itemType = drawerItem.mType;
617            if (itemType == DrawerItem.VIEW_ACCOUNT) {
618                // Account, so switch.
619                folder = null;
620                onAccountSelected(drawerItem.mAccount);
621            } else if (itemType == DrawerItem.VIEW_FOLDER) {
622                // Folder type, so change folders only.
623                folder = drawerItem.mFolder;
624                mSelectedDrawerItemType = folderType = drawerItem.mFolderType;
625                mSelectedFolderType = folder.type;
626                LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
627                        folder, mSelectedDrawerItemType);
628            } else {
629                // Do nothing.
630                LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
631                        + " Clicked on unset item in drawer. Offending item is " + item);
632                return;
633            }
634        } else if (item instanceof Folder) {
635            folder = (Folder) item;
636        } else if (item instanceof FooterItem) {
637            folder = null;
638            ((FooterItem) item).onClick(null /* unused */);
639        } else {
640            // Don't know how we got here.
641            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
642            folder = null;
643        }
644        if (folder != null) {
645            // Go to the conversation list for this folder.
646            if (!folder.folderUri.equals(mSelectedFolderUri)) {
647                mNextFolder = folder;
648                mAccountController.closeDrawer(true /** hasNewFolderOrAccount */,
649                        null /** nextAccount */,
650                        folder /** nextFolder */);
651
652                final String label = (folderType == DrawerItem.FOLDER_RECENT) ? "recent" : "normal";
653                Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(),
654                        label, 0);
655
656            } else {
657                // Clicked on same folder, just close drawer
658                mAccountController.closeDrawer(false /** hasNewFolderOrAccount */,
659                        null /** nextAccount */,
660                        folder /** nextFolder */);
661            }
662        }
663    }
664
665    protected void onAccountSelected(Account account) {
666        if (account != null && mSelectedFolderUri.equals(account.settings.defaultInbox)) {
667            // We're already in the default inbox for account,
668            // just close the drawer (no new target folders/accounts)
669            mAccountController.closeDrawer(false, mNextAccount,
670                    getDefaultInbox(mNextAccount));
671        } else {
672            changeAccount(account);
673        }
674    }
675
676    @Override
677    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
678        mListView.setEmptyView(null);
679        final Uri folderListUri;
680        if (id == FOLDER_LIST_LOADER_ID) {
681            if (mFolderListUri != null) {
682                // Folder trees, they specify a URI at construction time.
683                folderListUri = mFolderListUri;
684            } else {
685                // Drawers get the folder list from the current account.
686                folderListUri = mCurrentAccount.folderListUri;
687            }
688        } else if (id == ALL_FOLDER_LIST_LOADER_ID) {
689            folderListUri = mCurrentAccount.allFolderListUri;
690        } else {
691            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
692            return null;
693        }
694        return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
695                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
696    }
697
698    @Override
699    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
700        if (mFolderAdapter != null) {
701            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
702                mFolderAdapter.setCursor(data);
703            } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
704                mFolderAdapter.setAllFolderListCursor(data);
705            }
706            // new data means drawer list length may have changed, and the floaty footer visibility
707            // may need to be updated
708            showFloatyFooter(true /* onlyWhenClosed */);
709        }
710    }
711
712    @Override
713    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
714        if (mFolderAdapter != null) {
715            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
716                mFolderAdapter.setCursor(null);
717            } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
718                mFolderAdapter.setAllFolderListCursor(null);
719            }
720        }
721    }
722
723    /**
724     *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
725     *  frequency of use.
726     * @return a list of accounts, sorted by frequency of use
727     */
728    protected Account[] getAllAccounts() {
729        if (mAllAccountsObserver != null) {
730            return mAllAccountsObserver.getAllAccounts();
731        }
732        return new Account[0];
733    }
734
735    protected AccountsAdapter newAccountsAdapter() {
736        return new AccountsAdapter();
737    }
738
739    /**
740     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
741     */
742    private interface FolderListFragmentCursorAdapter extends ListAdapter {
743        /** Update the folder list cursor with the cursor given here. */
744        void setCursor(ObjectCursor<Folder> cursor);
745        /** Update the all folder list cursor with the cursor given here. */
746        void setAllFolderListCursor(ObjectCursor<Folder> cursor);
747        /** Remove all observers and destroy the object. */
748        void destroy();
749        /** Notifies the adapter that the data has changed. */
750        void notifyDataSetChanged();
751    }
752
753    /**
754     * An adapter for flat folder lists.
755     */
756    private class FolderAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
757
758        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
759            @Override
760            public void onChanged() {
761                if (!isCursorInvalid()) {
762                    rebuildFolderList();
763                }
764            }
765        };
766        /** No resource used for string header in folder list */
767        private static final int NO_HEADER_RESOURCE = -1;
768        /** Cache of most recently used folders */
769        private final RecentFolderList mRecentFolders;
770        /** True if the list is divided, false otherwise. See the comment on
771         * {@link FolderListFragment#mIsDivided} for more information */
772        private final boolean mIsDivided;
773        /** All the items */
774        private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
775        /** Cursor into the folder list. This might be null. */
776        private ObjectCursor<Folder> mCursor = null;
777        /** Cursor into the all folder list. This might be null. */
778        private ObjectCursor<Folder> mAllFolderListCursor = null;
779
780        /**
781         * Creates a {@link FolderAdapter}. This is a list of all the accounts and folders.
782         *
783         * @param isDivided true if folder list is flat, false if divided by label group. See
784         *                   the comments on {@link #mIsDivided} for more information
785         */
786        public FolderAdapter(boolean isDivided) {
787            super();
788            mIsDivided = isDivided;
789            final RecentFolderController controller = mActivity.getRecentFolderController();
790            if (controller != null && mIsDivided) {
791                mRecentFolders = mRecentFolderObserver.initialize(controller);
792            } else {
793                mRecentFolders = null;
794            }
795        }
796
797        @Override
798        public View getView(int position, View convertView, ViewGroup parent) {
799            final DrawerItem item = (DrawerItem) getItem(position);
800            final View view = item.getView(convertView, parent);
801            final int type = item.mType;
802            final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedDrawerItemType);
803            if (type == DrawerItem.VIEW_FOLDER) {
804                mListView.setItemChecked(mAccountsAdapter.getCount() + position, isSelected);
805            }
806            // If this is the current folder, also check to verify that the unread count
807            // matches what the action bar shows.
808            if (type == DrawerItem.VIEW_FOLDER
809                    && isSelected
810                    && (mCurrentFolderForUnreadCheck != null)
811                    && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
812                ((FolderItemView) view).overrideUnreadCount(
813                        mCurrentFolderForUnreadCheck.unreadCount);
814            }
815            return view;
816        }
817
818        @Override
819        public int getViewTypeCount() {
820            // Accounts, headers, folders (all parts of drawer view types)
821            return DrawerItem.getViewTypes();
822        }
823
824        @Override
825        public int getItemViewType(int position) {
826            return ((DrawerItem) getItem(position)).mType;
827        }
828
829        @Override
830        public int getCount() {
831            return mItemList.size();
832        }
833
834        @Override
835        public boolean isEnabled(int position) {
836            final DrawerItem drawerItem = ((DrawerItem) getItem(position));
837            return drawerItem != null && drawerItem.isItemEnabled();
838        }
839
840        @Override
841        public boolean areAllItemsEnabled() {
842            // We have headers and thus some items are not enabled.
843            return false;
844        }
845
846        /**
847         * Returns all the recent folders from the list given here. Safe to call with a null list.
848         * @param recentList a list of all recently accessed folders.
849         * @return a valid list of folders, which are all recent folders.
850         */
851        private List<Folder> getRecentFolders(RecentFolderList recentList) {
852            final List<Folder> folderList = new ArrayList<Folder>();
853            if (recentList == null) {
854                return folderList;
855            }
856            // Get all recent folders, after removing system folders.
857            for (final Folder f : recentList.getRecentFolderList(null)) {
858                if (!f.isProviderFolder()) {
859                    folderList.add(f);
860                }
861            }
862            return folderList;
863        }
864
865        /**
866         * Responsible for verifying mCursor, and ensuring any recalculate
867         * conditions are met. Also calls notifyDataSetChanged once it's finished
868         * populating {@link com.android.mail.ui.FolderListFragment.FolderAdapter#mItemList}
869         */
870        private void rebuildFolderList() {
871            final boolean oldInboxPresent = mInboxPresent;
872            mItemList = recalculateListFolders();
873            if ((mInboxPresent && !oldInboxPresent) || FolderUri.EMPTY.equals(mSelectedFolderUri)) {
874                // We didn't have an inbox folder before, but now we do. This can occur when
875                // setting up a new account. We automatically create the "starred" virtual
876                // virtual folder, but we won't create the inbox until it gets synced.
877                // This means that we'll start out looking at the "starred" folder, and the
878                // user will need to manually switch to the inbox. See b/13793316
879                mAccountController.switchToDefaultInboxOrChangeAccount(mCurrentAccount);
880            }
881            // Ask the list to invalidate its views.
882            notifyDataSetChanged();
883        }
884
885        /**
886         * Recalculates the system, recent and user label lists.
887         * This method modifies all the three lists on every single invocation.
888         */
889        private List<DrawerItem> recalculateListFolders() {
890            final List<DrawerItem> itemList = new ArrayList<DrawerItem>();
891            // If we are waiting for folder initialization, we don't have any kinds of folders,
892            // just the "Waiting for initialization" item. Note, this should only be done
893            // when we're waiting for account initialization or initial sync.
894            if (isCursorInvalid()) {
895                if(!mCurrentAccount.isAccountReady()) {
896                    itemList.add(DrawerItem.ofWaitView(mActivity));
897                }
898                return itemList;
899            }
900
901            if (!mIsDivided) {
902                // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
903                do {
904                    final Folder f = mCursor.getModel();
905                    if (!isFolderTypeExcluded(f)) {
906                        itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER));
907                    }
908                } while (mCursor.moveToNext());
909
910                return itemList;
911            }
912
913            // Otherwise, this is an adapter for a divided list.
914            final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
915            final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
916            do {
917                final Folder f = mCursor.getModel();
918                if (!isFolderTypeExcluded(f)) {
919                    if (f.isInbox()) {
920                        inboxFolders.add(DrawerItem.ofFolder(
921                                mActivity, f, DrawerItem.FOLDER_INBOX));
922                    } else {
923                        allFoldersList.add(DrawerItem.ofFolder(
924                                mActivity, f, DrawerItem.FOLDER_OTHER));
925                    }
926                }
927            } while (mCursor.moveToNext());
928
929            // If we have the all folder list, verify that the current folder exists
930            boolean currentFolderFound = false;
931            if (mAllFolderListCursor != null) {
932                final String folderName = mSelectedFolderUri.toString();
933                LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);
934
935                if (mAllFolderListCursor.moveToFirst()) {
936                    LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
937                    do {
938                        final Folder f = mAllFolderListCursor.getModel();
939                        if (!isFolderTypeExcluded(f)) {
940                            if (f.folderUri.equals(mSelectedFolderUri)) {
941                                LogUtils.d(LOG_TAG, "Found %s !", folderName);
942                                currentFolderFound = true;
943                            }
944                        }
945                    } while (!currentFolderFound && mAllFolderListCursor.moveToNext());
946                }
947
948                // The search folder will not be found here because it is excluded from the drawer.
949                // Don't switch off from the current folder if it's search.
950                if (!currentFolderFound && !Folder.isType(FolderType.SEARCH, mSelectedFolderType)
951                        && mSelectedFolderUri != FolderUri.EMPTY
952                        && mCurrentAccount != null && mAccountController != null
953                        && mAccountController.isDrawerPullEnabled()) {
954                    LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
955                            folderName, mCurrentAccount.getEmailAddress());
956                    changeAccount(mCurrentAccount);
957                }
958            }
959
960            mInboxPresent = (inboxFolders.size() > 0);
961
962            // Add all inboxes (sectioned Inboxes included) before recent folders.
963            addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
964
965            // Add recent folders next.
966            addRecentsToList(itemList);
967
968            // Add the remaining folders.
969            addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
970
971            return itemList;
972        }
973
974        /**
975         * Given a list of folders as {@link DrawerItem}s, add them as a group.
976         * Passing in a non-0 integer for the resource will enable a header.
977         *
978         * @param destination List of drawer items to populate
979         * @param source List of drawer items representing folders to add to the drawer
980         * @param headerStringResource
981         *            {@link FolderAdapter#NO_HEADER_RESOURCE} if no header
982         *            is required, or res-id otherwise. The integer is interpreted as the string
983         *            for the header's title.
984         */
985        private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
986                int headerStringResource) {
987            if (source.size() > 0) {
988                if(headerStringResource != NO_HEADER_RESOURCE) {
989                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
990                }
991                destination.addAll(source);
992            }
993        }
994
995        /**
996         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
997         *
998         * @param destination List of drawer items to populate
999         */
1000        private void addRecentsToList(List<DrawerItem> destination) {
1001            // If there are recent folders, add them.
1002            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
1003
1004            // Remove any excluded folder types
1005            if (mExcludedFolderTypes != null) {
1006                final Iterator<Folder> iterator = recentFolderList.iterator();
1007                while (iterator.hasNext()) {
1008                    if (isFolderTypeExcluded(iterator.next())) {
1009                        iterator.remove();
1010                    }
1011                }
1012            }
1013
1014            if (recentFolderList.size() > 0) {
1015                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
1016                // Recent folders are not queried for position.
1017                for (Folder f : recentFolderList) {
1018                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT));
1019                }
1020            }
1021        }
1022
1023        /**
1024         * Check if the cursor provided is valid.
1025         * @return True if cursor is invalid, false otherwise
1026         */
1027        private boolean isCursorInvalid() {
1028            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
1029                    || !mCursor.moveToFirst();
1030        }
1031
1032        @Override
1033        public void setCursor(ObjectCursor<Folder> cursor) {
1034            mCursor = cursor;
1035            mAccountsAdapter.rebuildAccountList();
1036            rebuildFolderList();
1037        }
1038
1039        @Override
1040        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1041            mAllFolderListCursor = cursor;
1042            mAccountsAdapter.rebuildAccountList();
1043            rebuildFolderList();
1044        }
1045
1046        @Override
1047        public Object getItem(int position) {
1048            // Is there an attempt made to access outside of the drawer item list?
1049            if (position >= mItemList.size()) {
1050                return null;
1051            } else {
1052                return mItemList.get(position);
1053            }
1054        }
1055
1056        @Override
1057        public long getItemId(int position) {
1058            return getItem(position).hashCode();
1059        }
1060
1061        @Override
1062        public final void destroy() {
1063            mRecentFolderObserver.unregisterAndDestroy();
1064        }
1065    }
1066
1067    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
1068            implements FolderListFragmentCursorAdapter {
1069
1070        private static final int PARENT = 0;
1071        private static final int CHILD = 1;
1072        private final FolderUri mParentUri;
1073        private final Folder mParent;
1074        private final FolderItemView.DropHandler mDropHandler;
1075
1076        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
1077            super(mActivity.getActivityContext(), R.layout.folder_item);
1078            mDropHandler = mActivity;
1079            mParent = parentFolder;
1080            mParentUri = parentFolder.folderUri;
1081            setCursor(c);
1082        }
1083
1084        @Override
1085        public int getViewTypeCount() {
1086            // Child and Parent
1087            return 2;
1088        }
1089
1090        @Override
1091        public int getItemViewType(int position) {
1092            final Folder f = getItem(position);
1093            return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
1094        }
1095
1096        @Override
1097        public View getView(int position, View convertView, ViewGroup parent) {
1098            final FolderItemView folderItemView;
1099            final Folder folder = getItem(position);
1100            boolean isParent = folder.folderUri.equals(mParentUri);
1101            if (convertView != null) {
1102                folderItemView = (FolderItemView) convertView;
1103            } else {
1104                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
1105                folderItemView = (FolderItemView) LayoutInflater.from(
1106                        mActivity.getActivityContext()).inflate(resId, null);
1107            }
1108            folderItemView.bind(folder, mDropHandler);
1109            if (folder.folderUri.equals(mSelectedFolderUri)) {
1110                getListView().setItemChecked(mAccountsAdapter.getCount() + position, true);
1111                // If this is the current folder, also check to verify that the unread count
1112                // matches what the action bar shows.
1113                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1114                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1115                if (unreadCountDiffers) {
1116                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1117                }
1118            }
1119            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1120            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1121            return folderItemView;
1122        }
1123
1124        @Override
1125        public void setCursor(ObjectCursor<Folder> cursor) {
1126            clear();
1127            if (mParent != null) {
1128                add(mParent);
1129            }
1130            if (cursor != null && cursor.getCount() > 0) {
1131                cursor.moveToFirst();
1132                do {
1133                    add(cursor.getModel());
1134                } while (cursor.moveToNext());
1135            }
1136        }
1137
1138        @Override
1139        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1140            // Not necessary in HierarchicalFolderListAdapter
1141        }
1142
1143        @Override
1144        public void destroy() {
1145            // Do nothing.
1146        }
1147    }
1148
1149    protected class AccountsAdapter extends BaseAdapter {
1150
1151        private List<DrawerItem> mAccounts;
1152
1153        public AccountsAdapter() {
1154            mAccounts = new ArrayList<DrawerItem>();
1155        }
1156
1157        public void rebuildAccountList() {
1158            if (!mHideAccounts) {
1159                mAccounts = buildAccountList();
1160                notifyDataSetChanged();
1161            }
1162        }
1163
1164        /**
1165         * Builds the list of accounts.
1166         */
1167        private List<DrawerItem> buildAccountList() {
1168            final Account[] allAccounts = getAllAccounts();
1169            final List<DrawerItem> accountList = new ArrayList<DrawerItem>(allAccounts.length);
1170            // Add all accounts and then the current account
1171            final Uri currentAccountUri = getCurrentAccountUri();
1172            for (final Account account : allAccounts) {
1173                final int unreadCount = getUnreadCount(account);
1174                accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
1175                        currentAccountUri.equals(account.uri), mImagesCache, mContactResolver));
1176            }
1177            if (mCurrentAccount == null) {
1178                LogUtils.wtf(LOG_TAG, "buildAccountList() with null current account.");
1179            }
1180            return accountList;
1181        }
1182
1183        protected Uri getCurrentAccountUri() {
1184            return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
1185        }
1186
1187        @Override
1188        public int getCount() {
1189            return mAccounts.size();
1190        }
1191
1192        @Override
1193        public Object getItem(int position) {
1194            // Is there an attempt made to access outside of the drawer item list?
1195            if (position >= mAccounts.size()) {
1196                return null;
1197            } else {
1198                return mAccounts.get(position);
1199            }
1200        }
1201
1202        @Override
1203        public long getItemId(int position) {
1204            return getItem(position).hashCode();
1205        }
1206
1207        @Override
1208        public View getView(int position, View convertView, ViewGroup parent) {
1209            final DrawerItem item = (DrawerItem) getItem(position);
1210            return item.getView(convertView, parent);
1211        }
1212    }
1213
1214    private class FooterAdapter extends BaseAdapter {
1215
1216        private final List<FooterItem> mFooterItems = Lists.newArrayList();
1217
1218        private FooterAdapter() {
1219            update();
1220        }
1221
1222        @Override
1223        public int getCount() {
1224            return mFooterItems.size();
1225        }
1226
1227        @Override
1228        public Object getItem(int position) {
1229            return mFooterItems.get(position);
1230        }
1231
1232        @Override
1233        public long getItemId(int position) {
1234            return position;
1235        }
1236
1237        /**
1238         * @param convertView a view, possibly null, to be recycled.
1239         * @param parent the parent hosting this view.
1240         * @return a view for the footer item displaying the given text and image.
1241         */
1242        @Override
1243        public View getView(int position, View convertView, ViewGroup parent) {
1244            final ViewGroup footerItemView;
1245            if (convertView != null) {
1246                footerItemView = (ViewGroup) convertView;
1247            } else {
1248                footerItemView = (ViewGroup) getActivity().getLayoutInflater().
1249                        inflate(R.layout.drawer_footer_item, parent, false);
1250            }
1251
1252            final FooterItem item = (FooterItem) getItem(position);
1253
1254            footerItemView.findViewById(R.id.top_border).setVisibility(
1255                    item.shouldShowTopBorder() ? View.VISIBLE : View.GONE);
1256
1257            // adjust the text of the footer item
1258            final TextView textView = (TextView) footerItemView.
1259                    findViewById(R.id.drawer_footer_text);
1260            textView.setText(item.getTextResourceID());
1261
1262            // adjust the icon of the footer item
1263            final ImageView imageView = (ImageView) footerItemView.
1264                    findViewById(R.id.drawer_footer_image);
1265            imageView.setImageResource(item.getImageResourceID());
1266            return footerItemView;
1267        }
1268
1269        /**
1270         * Recomputes the footer drawer items depending on whether the current account
1271         * is populated with URIs that navigate to appropriate destinations.
1272         */
1273        private void update() {
1274            // if the parent activity shows a drawer, these items should participate in that drawer
1275            // (if it shows a *pane* they should *not* participate in that pane)
1276            if (!mShowFooter) {
1277                return;
1278            }
1279
1280            mFooterItems.clear();
1281
1282            if (mCurrentAccount != null) {
1283                mFooterItems.add(new SettingsItem());
1284            }
1285
1286            if (mCurrentAccount != null && !Utils.isEmpty(mCurrentAccount.helpIntentUri)) {
1287                mFooterItems.add(new HelpItem());
1288            }
1289
1290            // if a feedback Uri exists, show the Feedback drawer item
1291            if (mCurrentAccount != null &&
1292                    !Utils.isEmpty(mCurrentAccount.sendFeedbackIntentUri)) {
1293                mFooterItems.add(new FeedbackItem());
1294            }
1295
1296            if (!mFooterItems.isEmpty()) {
1297                mFooterItems.get(0).setShowTopBorder(true);
1298            }
1299
1300            notifyDataSetChanged();
1301        }
1302    }
1303
1304    /**
1305     * Sets the currently selected folder safely.
1306     * @param folder the folder to change to. It is an error to pass null here.
1307     */
1308    private void setSelectedFolder(Folder folder) {
1309        if (folder == null) {
1310            mSelectedFolderUri = FolderUri.EMPTY;
1311            mCurrentFolderForUnreadCheck = null;
1312            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1313            return;
1314        }
1315
1316        final boolean viewChanged =
1317                !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1318
1319        // There are two cases in which the folder type is not set by this class.
1320        // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
1321        //    folder but its type was never set.
1322        // 2. The user backs into the default inbox. Going 'back' from the conversation list of
1323        //    any folder will take you to the default inbox for that account. (If you are in the
1324        //    default inbox already, back exits the app.)
1325        // In both these cases, the selected folder type is not set, and must be set.
1326        if (mSelectedDrawerItemType == DrawerItem.UNSET || (mCurrentAccount != null
1327                && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
1328            mSelectedDrawerItemType =
1329                    folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
1330            mSelectedFolderType = folder.type;
1331        }
1332
1333        mCurrentFolderForUnreadCheck = folder;
1334        mSelectedFolderUri = folder.folderUri;
1335        if (mFolderAdapter != null && viewChanged) {
1336            mFolderAdapter.notifyDataSetChanged();
1337        }
1338    }
1339
1340    /**
1341     * Sets the current account to the one provided here.
1342     * @param account the current account to set to.
1343     */
1344    private void setSelectedAccount(Account account) {
1345        final boolean changed = (account != null) && (mCurrentAccount == null
1346                || !mCurrentAccount.uri.equals(account.uri));
1347        mCurrentAccount = account;
1348        if (changed) {
1349            // Verify that the new account supports sending application feedback
1350            updateFooterItems();
1351            // We no longer have proper folder objects. Let the new ones come in
1352            mFolderAdapter.setCursor(null);
1353            // If currentAccount is different from the one we set, restart the loader. Look at the
1354            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1355            // don't just do restartLoader.
1356            final LoaderManager manager = getLoaderManager();
1357            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1358            manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1359            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1360            manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1361            // An updated cursor causes the entire list to refresh. No need to refresh the list.
1362            // But we do need to blank out the current folder, since the account might not be
1363            // synced.
1364            mSelectedFolderUri = FolderUri.EMPTY;
1365            mCurrentFolderForUnreadCheck = null;
1366        } else if (account == null) {
1367            // This should never happen currently, but is a safeguard against a very incorrect
1368            // non-null account -> null account transition.
1369            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1370            final LoaderManager manager = getLoaderManager();
1371            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1372            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1373        }
1374    }
1375
1376    private void updateFooterItems() {
1377        mFooterAdapter.update();
1378        updateFloatyFooter();
1379    }
1380
1381    private void updateFloatyFooter() {
1382        if (mFloatyFooter == null) {
1383            return;
1384        }
1385
1386        // assuming this isn't often (the caller is debounced), just remake the floaty footer
1387        // from scratch
1388        final ViewGroup container = (ViewGroup) mFloatyFooter.findViewById(
1389                R.id.floaty_footer_items);
1390        container.removeAllViews();
1391
1392        for (int i = 0; i < mFooterAdapter.getCount(); i++) {
1393            final View v = mFooterAdapter.getView(i, null /* convertView */, mFloatyFooter);
1394            // the floaty version has its own top border, so remove the list version's border
1395            if (i == 0) {
1396                v.findViewById(R.id.top_border).setVisibility(View.GONE);
1397            }
1398            v.setOnClickListener((FooterItem) mFooterAdapter.getItem(i));
1399            container.addView(v);
1400        }
1401    }
1402
1403    /**
1404     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1405     */
1406    private boolean isFolderTypeExcluded(final Folder folder) {
1407        if (mExcludedFolderTypes == null) {
1408            return false;
1409        }
1410
1411        for (final int excludedType : mExcludedFolderTypes) {
1412            if (folder.isType(excludedType)) {
1413                return true;
1414            }
1415        }
1416
1417        return false;
1418    }
1419
1420    /**
1421     * @return the choice mode to use for the {@link ListView}
1422     */
1423    protected int getListViewChoiceMode() {
1424        return mAccountController.getFolderListViewChoiceMode();
1425    }
1426
1427    private void showFloatyFooter(boolean onlyWhenClosed) {
1428        // don't ever show if the footer is disabled (in onCreateView)
1429        if (mFloatyFooter == null) {
1430            return;
1431        }
1432
1433        // don't show when onLoadFinished is the reason to show it, and the drawer is open
1434        // (minimize user-visible changes; that case is basically only relevant when closed)
1435        final boolean drawerIsOpen = mActivity.getDrawerController().isDrawerOpen();
1436        if (onlyWhenClosed && drawerIsOpen) {
1437            return;
1438        }
1439
1440        // show the footer, unless if we're already at the very bottom
1441        final int vis = getListView().canScrollVertically(+1) ? View.VISIBLE : View.GONE;
1442
1443        mFloatyFooter.animate().cancel();
1444        if (drawerIsOpen && vis == View.VISIBLE) {
1445            mFloatyFooter.animate()
1446                .translationY(0f)
1447                .setDuration(150)
1448                .setInterpolator(INTERPOLATOR_SHOW_FLOATY)
1449                .setListener(new AnimatorListenerAdapter() {
1450                    @Override
1451                    public void onAnimationStart(Animator animation) {
1452                        mFloatyFooter.setVisibility(vis);
1453                    }
1454                });
1455        } else {
1456            mFloatyFooter.setTranslationY(0f);
1457            mFloatyFooter.setVisibility(vis);
1458        }
1459
1460    }
1461
1462    private void hideFloatyFooter() {
1463        if (mFloatyFooter == null || mFloatyFooter.getVisibility() == View.GONE
1464                || mFooterIsAnimating) {
1465            return;
1466        }
1467        mFooterIsAnimating = true;
1468        mFloatyFooter.animate().cancel();
1469        mFloatyFooter.animate()
1470            .translationY(mFloatyFooter.getHeight())
1471            .setDuration(200)
1472            .setInterpolator(INTERPOLATOR_HIDE_FLOATY)
1473            .setListener(new AnimatorListenerAdapter() {
1474                @Override
1475                public void onAnimationEnd(Animator animation) {
1476                    mFooterIsAnimating = false;
1477                    mFloatyFooter.setVisibility(View.GONE);
1478                }
1479            });
1480    }
1481
1482    /**
1483     * The base class of all footer items. Subclasses must fill in the logic of
1484     * {@link #doFooterAction()} which contains the behavior when the item is selected.
1485     */
1486    private abstract class FooterItem implements View.OnClickListener {
1487
1488        private final int mImageResourceID;
1489        private final int mTextResourceID;
1490
1491        private boolean mShowTopBorder;
1492
1493        private FooterItem(final int imageResourceID, final int textResourceID) {
1494            mImageResourceID = imageResourceID;
1495            mTextResourceID = textResourceID;
1496        }
1497
1498        private int getImageResourceID() {
1499            return mImageResourceID;
1500        }
1501
1502        private int getTextResourceID() {
1503            return mTextResourceID;
1504        }
1505
1506        /**
1507         * Executes the behavior associated with this footer item.<br>
1508         * <br>
1509         * WARNING: you probably don't want to call this directly; use
1510         * {@link #onClick(View)} instead. This method actually performs the action, and its
1511         * execution is deferred from when the 'click' happens so we can smoothly close the drawer
1512         * beforehand.
1513         */
1514        abstract void doFooterAction();
1515
1516        @Override
1517        public final void onClick(View v) {
1518            // close the drawer and defer handling the click until onDrawerClosed
1519            mAccountController.closeDrawer(false /* hasNewFolderOrAccount */,
1520                    null /* nextAccount */, null /* nextFolder */);
1521            mDrawerListener.setPendingFooterClick(this);
1522        }
1523
1524        public boolean shouldShowTopBorder() {
1525            return mShowTopBorder;
1526        }
1527
1528        public void setShowTopBorder(boolean show) {
1529            mShowTopBorder = show;
1530        }
1531
1532        // for analytics
1533        String getEventLabel() {
1534            final StringBuilder sb = new StringBuilder("drawer_footer");
1535            sb.append("/");
1536            sb.append(mActivity.getViewMode().getModeString());
1537            return sb.toString();
1538        }
1539
1540    }
1541
1542    private class HelpItem extends FooterItem {
1543        protected HelpItem() {
1544            super(R.drawable.ic_menu_help, R.string.help_and_info);
1545        }
1546
1547        @Override
1548        void doFooterAction() {
1549            Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
1550                    R.id.help_info_menu_item, getEventLabel(), 0);
1551            Utils.showHelp(getActivity(), mCurrentAccount, mActivity.getHelpContext());
1552        }
1553    }
1554
1555    private class FeedbackItem extends FooterItem {
1556        protected FeedbackItem() {
1557            super(R.drawable.ic_menu_feedback, R.string.feedback);
1558        }
1559
1560        @Override
1561        void doFooterAction() {
1562            Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
1563                    R.id.feedback_menu_item, getEventLabel(), 0);
1564            Utils.sendFeedback(getActivity(), mCurrentAccount, false);
1565        }
1566    }
1567
1568    private class SettingsItem extends FooterItem {
1569        protected SettingsItem() {
1570            super(R.drawable.ic_menu_settings, R.string.menu_settings);
1571        }
1572
1573        @Override
1574        void doFooterAction() {
1575            Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
1576                    R.id.settings, getEventLabel(), 0);
1577            Utils.showSettings(mActivity.getActivityContext(), mCurrentAccount);
1578        }
1579    }
1580
1581    /**
1582     * Drawer listener for footer functionality to react to drawer state.
1583     */
1584    private class DrawerStateListener implements DrawerLayout.DrawerListener {
1585
1586        private FooterItem mPendingFooterClick;
1587
1588        public void setPendingFooterClick(FooterItem itemClicked) {
1589            mPendingFooterClick = itemClicked;
1590        }
1591
1592        @Override
1593        public void onDrawerSlide(View drawerView, float slideOffset) {}
1594
1595        @Override
1596        public void onDrawerOpened(View drawerView) {}
1597
1598        @Override
1599        public void onDrawerClosed(View drawerView) {
1600            showFloatyFooter(false /* onlyWhenClosed */);
1601            if (mPendingFooterClick != null) {
1602                mPendingFooterClick.doFooterAction();
1603                mPendingFooterClick = null;
1604            }
1605        }
1606
1607        @Override
1608        public void onDrawerStateChanged(int newState) {}
1609
1610    }
1611}
1612