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