FolderListFragment.java revision b298ad0629d3eb31002e7595d42e65151d6ae90b
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 onDrawerDrag(float percent) {
846        mMiniDrawerView.setAlpha(1f - percent);
847        mListView.setAlpha(percent);
848        mMiniDrawerView.setVisibility(View.VISIBLE);
849        mListView.setVisibility(View.VISIBLE);
850    }
851
852    /**
853     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
854     */
855    private interface FolderListFragmentCursorAdapter extends ListAdapter {
856        /** Update the folder list cursor with the cursor given here. */
857        void setCursor(ObjectCursor<Folder> cursor);
858        ObjectCursor<Folder> getCursor();
859        /** Update the all folder list cursor with the cursor given here. */
860        void setAllFolderListCursor(ObjectCursor<Folder> cursor);
861        /** Remove all observers and destroy the object. */
862        void destroy();
863        /** Notifies the adapter that the data has changed. */
864        void notifyDataSetChanged();
865    }
866
867    /**
868     * An adapter for flat folder lists.
869     */
870    private class FolderAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
871
872        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
873            @Override
874            public void onChanged() {
875                if (!isCursorInvalid()) {
876                    rebuildFolderList();
877                }
878            }
879        };
880        /** No resource used for string header in folder list */
881        private static final int BLANK_HEADER_RESOURCE = -1;
882        /** Cache of most recently used folders */
883        private final RecentFolderList mRecentFolders;
884        /** True if the list is divided, false otherwise. See the comment on
885         * {@link FolderListFragment#mIsDivided} for more information */
886        private final boolean mIsDivided;
887        /** All the items */
888        private List<DrawerItem> mItemList = new ArrayList<>();
889        /** Cursor into the folder list. This might be null. */
890        private ObjectCursor<Folder> mCursor = null;
891        /** Cursor into the all folder list. This might be null. */
892        private ObjectCursor<Folder> mAllFolderListCursor = null;
893
894        /**
895         * Creates a {@link FolderAdapter}. This is a list of all the accounts and folders.
896         *
897         * @param isDivided true if folder list is flat, false if divided by label group. See
898         *                   the comments on {@link #mIsDivided} for more information
899         */
900        public FolderAdapter(boolean isDivided) {
901            super();
902            mIsDivided = isDivided;
903            final RecentFolderController controller = mActivity.getRecentFolderController();
904            if (controller != null && mIsDivided) {
905                mRecentFolders = mRecentFolderObserver.initialize(controller);
906            } else {
907                mRecentFolders = null;
908            }
909        }
910
911        @Override
912        public View getView(int position, View convertView, ViewGroup parent) {
913            final DrawerItem item = (DrawerItem) getItem(position);
914            final View view = item.getView(convertView, parent);
915            final @DrawerItem.DrawerItemType int type = item.getType();
916            final boolean isSelected =
917                    item.isHighlighted(mSelectedFolderUri, mSelectedDrawerItemCategory);
918            if (type == DrawerItem.VIEW_FOLDER) {
919                mListView.setItemChecked((mAccountsAdapter != null ?
920                        mAccountsAdapter.getCount() : 0) +
921                        position + mListView.getHeaderViewsCount(), isSelected);
922            }
923            // If this is the current folder, also check to verify that the unread count
924            // matches what the action bar shows.
925            if (type == DrawerItem.VIEW_FOLDER
926                    && isSelected
927                    && (mCurrentFolderForUnreadCheck != null)
928                    && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
929                ((FolderItemView) view).overrideUnreadCount(
930                        mCurrentFolderForUnreadCheck.unreadCount);
931            }
932            return view;
933        }
934
935        @Override
936        public int getViewTypeCount() {
937            // Accounts, headers, folders (all parts of drawer view types)
938            return DrawerItem.getViewTypeCount();
939        }
940
941        @Override
942        public int getItemViewType(int position) {
943            return ((DrawerItem) getItem(position)).getType();
944        }
945
946        @Override
947        public int getCount() {
948            return mItemList.size();
949        }
950
951        @Override
952        public boolean isEnabled(int position) {
953            final DrawerItem drawerItem = ((DrawerItem) getItem(position));
954            return drawerItem != null && drawerItem.isItemEnabled();
955        }
956
957        @Override
958        public boolean areAllItemsEnabled() {
959            // We have headers and thus some items are not enabled.
960            return false;
961        }
962
963        /**
964         * Returns all the recent folders from the list given here. Safe to call with a null list.
965         * @param recentList a list of all recently accessed folders.
966         * @return a valid list of folders, which are all recent folders.
967         */
968        private List<Folder> getRecentFolders(RecentFolderList recentList) {
969            final List<Folder> folderList = new ArrayList<>();
970            if (recentList == null) {
971                return folderList;
972            }
973            // Get all recent folders, after removing system folders.
974            for (final Folder f : recentList.getRecentFolderList(null)) {
975                if (!f.isProviderFolder()) {
976                    folderList.add(f);
977                }
978            }
979            return folderList;
980        }
981
982        /**
983         * Responsible for verifying mCursor, and ensuring any recalculate
984         * conditions are met. Also calls notifyDataSetChanged once it's finished
985         * populating {@link com.android.mail.ui.FolderListFragment.FolderAdapter#mItemList}
986         */
987        private void rebuildFolderList() {
988            final boolean oldInboxPresent = mInboxPresent;
989            mItemList = recalculateListFolders();
990            if (mAccountController != null && mInboxPresent && !oldInboxPresent) {
991                // We didn't have an inbox folder before, but now we do. This can occur when
992                // setting up a new account. We automatically create the "starred" virtual
993                // virtual folder, but we won't create the inbox until it gets synced.
994                // This means that we'll start out looking at the "starred" folder, and the
995                // user will need to manually switch to the inbox. See b/13793316
996                mAccountController.switchToDefaultInboxOrChangeAccount(mCurrentAccount);
997            }
998            // Ask the list to invalidate its views.
999            notifyDataSetChanged();
1000        }
1001
1002        /**
1003         * Recalculates the system, recent and user label lists.
1004         * This method modifies all the three lists on every single invocation.
1005         */
1006        private List<DrawerItem> recalculateListFolders() {
1007            final List<DrawerItem> itemList = new ArrayList<>();
1008            // If we are waiting for folder initialization, we don't have any kinds of folders,
1009            // just the "Waiting for initialization" item. Note, this should only be done
1010            // when we're waiting for account initialization or initial sync.
1011            if (isCursorInvalid()) {
1012                if(!mCurrentAccount.isAccountReady()) {
1013                    itemList.add(DrawerItem.ofWaitView(mActivity));
1014                }
1015                return itemList;
1016            }
1017            if (mIsDivided) {
1018                //Choose an adapter for a divided list with sections
1019                return recalculateDividedListFolders(itemList);
1020            } else {
1021                // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
1022                return recalculateFlatListFolders(itemList);
1023            }
1024        }
1025
1026        // Recalculate folder list intended to be flat (no hearders or sections shown).
1027        // This is commonly used for the widget or other simple folder selections
1028        private List<DrawerItem> recalculateFlatListFolders(List<DrawerItem> itemList) {
1029            final List<DrawerItem> inboxFolders = new ArrayList<>();
1030            final List<DrawerItem> allFoldersList = new ArrayList<>();
1031            do {
1032                final Folder f = mCursor.getModel();
1033                if (!isFolderTypeExcluded(f)) {
1034                    // Prioritize inboxes
1035                    if (f.isInbox()) {
1036                        inboxFolders.add(DrawerItem.ofFolder(
1037                                mActivity, f, DrawerItem.FOLDER_OTHER));
1038                    } else {
1039                        allFoldersList.add(
1040                                DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER));
1041                    }
1042                }
1043            } while (mCursor.moveToNext());
1044            itemList.addAll(inboxFolders);
1045            itemList.addAll(allFoldersList);
1046            return itemList;
1047        }
1048
1049        // Recalculate folder list divided by sections (inboxes, recents, all, etc...)
1050        // This is primarily used by the drawer
1051        private List<DrawerItem> recalculateDividedListFolders(List<DrawerItem> itemList) {
1052            final List<DrawerItem> allFoldersList = new ArrayList<>();
1053            final List<DrawerItem> inboxFolders = new ArrayList<>();
1054            do {
1055                final Folder f = mCursor.getModel();
1056                if (!isFolderTypeExcluded(f)) {
1057                    if (f.isInbox()) {
1058                        inboxFolders.add(DrawerItem.ofFolder(
1059                                mActivity, f, DrawerItem.FOLDER_INBOX));
1060                    } else {
1061                        allFoldersList.add(DrawerItem.ofFolder(
1062                                mActivity, f, DrawerItem.FOLDER_OTHER));
1063                    }
1064                }
1065            } while (mCursor.moveToNext());
1066
1067            // If we have the all folder list, verify that the current folder exists
1068            boolean currentFolderFound = false;
1069            if (mAllFolderListCursor != null) {
1070                final String folderName = mSelectedFolderUri.toString();
1071                LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);
1072
1073                if (mAllFolderListCursor.moveToFirst()) {
1074                    LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
1075                    do {
1076                        final Folder f = mAllFolderListCursor.getModel();
1077                        if (!isFolderTypeExcluded(f)) {
1078                            if (f.folderUri.equals(mSelectedFolderUri)) {
1079                                LogUtils.d(LOG_TAG, "Found %s !", folderName);
1080                                currentFolderFound = true;
1081                            }
1082                        }
1083                    } while (!currentFolderFound && mAllFolderListCursor.moveToNext());
1084                }
1085
1086                // The search folder will not be found here because it is excluded from the drawer.
1087                // Don't switch off from the current folder if it's search.
1088                if (!currentFolderFound && !Folder.isType(FolderType.SEARCH, mSelectedFolderType)
1089                        && mSelectedFolderUri != FolderUri.EMPTY
1090                        && mCurrentAccount != null && mAccountController != null
1091                        && mAccountController.isDrawerPullEnabled()) {
1092                    LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
1093                            folderName, mCurrentAccount.getEmailAddress());
1094                    changeAccount(mCurrentAccount);
1095                }
1096            }
1097
1098            mInboxPresent = (inboxFolders.size() > 0);
1099
1100            // Add all inboxes (sectioned Inboxes included) before recent folders.
1101            addFolderDivision(itemList, inboxFolders, BLANK_HEADER_RESOURCE);
1102
1103            // Add recent folders next.
1104            addRecentsToList(itemList);
1105
1106            // Add the remaining folders.
1107            addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
1108
1109            return itemList;
1110        }
1111
1112        /**
1113         * Given a list of folders as {@link DrawerItem}s, add them as a group.
1114         * Passing in a non-0 integer for the resource will enable a header.
1115         *
1116         * @param destination List of drawer items to populate
1117         * @param source List of drawer items representing folders to add to the drawer
1118         * @param headerStringResource
1119         *            {@link FolderAdapter#BLANK_HEADER_RESOURCE} if no header text
1120         *            is required, or res-id otherwise. The integer is interpreted as the string
1121         *            for the header's title.
1122         */
1123        private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
1124                int headerStringResource) {
1125            if (source.size() > 0) {
1126                if(headerStringResource != BLANK_HEADER_RESOURCE) {
1127                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
1128                } else {
1129                    destination.add(DrawerItem.ofBlankHeader(mActivity));
1130                }
1131                destination.addAll(source);
1132            }
1133        }
1134
1135        /**
1136         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
1137         *
1138         * @param destination List of drawer items to populate
1139         */
1140        private void addRecentsToList(List<DrawerItem> destination) {
1141            // If there are recent folders, add them.
1142            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
1143
1144            // Remove any excluded folder types
1145            if (mExcludedFolderTypes != null) {
1146                final Iterator<Folder> iterator = recentFolderList.iterator();
1147                while (iterator.hasNext()) {
1148                    if (isFolderTypeExcluded(iterator.next())) {
1149                        iterator.remove();
1150                    }
1151                }
1152            }
1153
1154            if (recentFolderList.size() > 0) {
1155                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
1156                // Recent folders are not queried for position.
1157                for (Folder f : recentFolderList) {
1158                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT));
1159                }
1160            }
1161        }
1162
1163        /**
1164         * Check if the cursor provided is valid.
1165         * @return True if cursor is invalid, false otherwise
1166         */
1167        private boolean isCursorInvalid() {
1168            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
1169                    || !mCursor.moveToFirst();
1170        }
1171
1172        @Override
1173        public void setCursor(ObjectCursor<Folder> cursor) {
1174            mCursor = cursor;
1175            rebuildAccountList();
1176            rebuildFolderList();
1177        }
1178
1179        @Override
1180        public ObjectCursor<Folder> getCursor() {
1181            return mCursor;
1182        }
1183
1184        @Override
1185        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1186            mAllFolderListCursor = cursor;
1187            rebuildAccountList();
1188            rebuildFolderList();
1189        }
1190
1191        @Override
1192        public Object getItem(int position) {
1193            // Is there an attempt made to access outside of the drawer item list?
1194            if (position >= mItemList.size()) {
1195                return null;
1196            } else {
1197                return mItemList.get(position);
1198            }
1199        }
1200
1201        @Override
1202        public long getItemId(int position) {
1203            return getItem(position).hashCode();
1204        }
1205
1206        @Override
1207        public final void destroy() {
1208            mRecentFolderObserver.unregisterAndDestroy();
1209        }
1210    }
1211
1212    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
1213            implements FolderListFragmentCursorAdapter {
1214
1215        private static final int PARENT = 0;
1216        private static final int CHILD = 1;
1217        private final FolderUri mParentUri;
1218        private final Folder mParent;
1219
1220        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
1221            super(mActivity.getActivityContext(), R.layout.folder_item);
1222            mParent = parentFolder;
1223            mParentUri = parentFolder.folderUri;
1224            setCursor(c);
1225        }
1226
1227        @Override
1228        public int getViewTypeCount() {
1229            // Child and Parent
1230            return 2;
1231        }
1232
1233        @Override
1234        public int getItemViewType(int position) {
1235            final Folder f = getItem(position);
1236            return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
1237        }
1238
1239        @Override
1240        public View getView(int position, View convertView, ViewGroup parent) {
1241            final FolderItemView folderItemView;
1242            final Folder folder = getItem(position);
1243
1244            if (convertView != null) {
1245                folderItemView = (FolderItemView) convertView;
1246            } else {
1247                folderItemView = (FolderItemView) LayoutInflater.from(
1248                        mActivity.getActivityContext()).inflate(R.layout.folder_item, null);
1249            }
1250            folderItemView.bind(folder, mParentUri);
1251
1252            if (folder.folderUri.equals(mSelectedFolderUri)) {
1253                final ListView listView = getListView();
1254                listView.setItemChecked((mAccountsAdapter != null ?
1255                        mAccountsAdapter.getCount() : 0) +
1256                        position + listView.getHeaderViewsCount(), true);
1257                // If this is the current folder, also check to verify that the unread count
1258                // matches what the action bar shows.
1259                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1260                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1261                if (unreadCountDiffers) {
1262                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1263                }
1264            }
1265            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1266            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1267            return folderItemView;
1268        }
1269
1270        @Override
1271        public void setCursor(ObjectCursor<Folder> cursor) {
1272            clear();
1273            if (mParent != null) {
1274                add(mParent);
1275            }
1276            if (cursor != null && cursor.getCount() > 0) {
1277                cursor.moveToFirst();
1278                do {
1279                    add(cursor.getModel());
1280                } while (cursor.moveToNext());
1281            }
1282        }
1283
1284        @Override
1285        public ObjectCursor<Folder> getCursor() {
1286            throw new UnsupportedOperationException("drawers don't have hierarchical folders");
1287        }
1288
1289        @Override
1290        public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1291            // Not necessary in HierarchicalFolderListAdapter
1292        }
1293
1294        @Override
1295        public void destroy() {
1296            // Do nothing.
1297        }
1298    }
1299
1300    public void rebuildAccountList() {
1301        if (!mIsFolderSelectionActivity) {
1302            if (mAccountsAdapter != null) {
1303                mAccountsAdapter.setAccounts(buildAccountListDrawerItems());
1304            }
1305            if (mMiniDrawerAccountsAdapter != null) {
1306                mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount);
1307            }
1308        }
1309    }
1310
1311    protected static class AccountsAdapter extends BaseAdapter {
1312
1313        private List<DrawerItem> mAccounts;
1314
1315        public AccountsAdapter() {
1316            mAccounts = new ArrayList<>();
1317        }
1318
1319        public void setAccounts(List<DrawerItem> accounts) {
1320            mAccounts = accounts;
1321            notifyDataSetChanged();
1322        }
1323
1324        @Override
1325        public int getCount() {
1326            return mAccounts.size();
1327        }
1328
1329        @Override
1330        public Object getItem(int position) {
1331            // Is there an attempt made to access outside of the drawer item list?
1332            if (position >= mAccounts.size()) {
1333                return null;
1334            } else {
1335                return mAccounts.get(position);
1336            }
1337        }
1338
1339        @Override
1340        public long getItemId(int position) {
1341            return getItem(position).hashCode();
1342        }
1343
1344        @Override
1345        public View getView(int position, View convertView, ViewGroup parent) {
1346            final DrawerItem item = (DrawerItem) getItem(position);
1347            return item.getView(convertView, parent);
1348        }
1349    }
1350
1351    /**
1352     * Builds the drawer items for the list of accounts.
1353     */
1354    private List<DrawerItem> buildAccountListDrawerItems() {
1355        final Account[] allAccounts = getAllAccounts();
1356        final List<DrawerItem> accountList = new ArrayList<>(allAccounts.length);
1357        // Add all accounts and then the current account
1358        final Uri currentAccountUri = getCurrentAccountUri();
1359        for (final Account account : allAccounts) {
1360            final int unreadCount = getUnreadCount(account);
1361            accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
1362                    currentAccountUri.equals(account.uri), mImagesCache, mContactResolver));
1363        }
1364        if (mCurrentAccount == null) {
1365            LogUtils.wtf(LOG_TAG, "buildAccountListDrawerItems() with null current account.");
1366        }
1367        return accountList;
1368    }
1369
1370    private Uri getCurrentAccountUri() {
1371        return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
1372    }
1373
1374    protected String getCurrentAccountEmailAddress() {
1375        return mCurrentAccount == null ? "" : mCurrentAccount.getEmailAddress();
1376    }
1377
1378    protected MergedAdapter<ListAdapter> getMergedAdapter() {
1379        return mMergedAdapter;
1380    }
1381
1382    public ObjectCursor<Folder> getFoldersCursor() {
1383        return (mFolderAdapter != null) ? mFolderAdapter.getCursor() : null;
1384    }
1385
1386    private class FooterAdapter extends BaseAdapter {
1387
1388        private final List<DrawerItem> mFooterItems = Lists.newArrayList();
1389
1390        private FooterAdapter() {
1391            update();
1392        }
1393
1394        @Override
1395        public int getCount() {
1396            return mFooterItems.size();
1397        }
1398
1399        @Override
1400        public DrawerItem getItem(int position) {
1401            return mFooterItems.get(position);
1402        }
1403
1404        @Override
1405        public long getItemId(int position) {
1406            return position;
1407        }
1408
1409        @Override
1410        public int getViewTypeCount() {
1411            // Accounts, headers, folders (all parts of drawer view types)
1412            return DrawerItem.getViewTypeCount();
1413        }
1414
1415        @Override
1416        public int getItemViewType(int position) {
1417            return getItem(position).getType();
1418        }
1419
1420        /**
1421         * @param convertView a view, possibly null, to be recycled.
1422         * @param parent the parent hosting this view.
1423         * @return a view for the footer item displaying the given text and image.
1424         */
1425        @Override
1426        public View getView(int position, View convertView, ViewGroup parent) {
1427            return getItem(position).getView(convertView, parent);
1428        }
1429
1430        /**
1431         * Recomputes the footer drawer items depending on whether the current account
1432         * is populated with URIs that navigate to appropriate destinations.
1433         */
1434        private void update() {
1435            // if the parent activity shows a drawer, these items should participate in that drawer
1436            // (if it shows a *pane* they should *not* participate in that pane)
1437            if (mIsFolderSelectionActivity) {
1438                return;
1439            }
1440
1441            mFooterItems.clear();
1442
1443            if (mCurrentAccount != null) {
1444                mFooterItems.add(DrawerItem.ofSettingsItem(mActivity, mCurrentAccount,
1445                        mDrawerListener));
1446            }
1447
1448            if (mCurrentAccount != null && !Utils.isEmpty(mCurrentAccount.helpIntentUri)) {
1449                mFooterItems.add(DrawerItem.ofHelpItem(mActivity, mCurrentAccount,
1450                        mDrawerListener));
1451            }
1452
1453            if (!mFooterItems.isEmpty()) {
1454                mFooterItems.add(0, DrawerItem.ofBlankHeader(mActivity));
1455                mFooterItems.add(DrawerItem.ofBottomSpace(mActivity));
1456            }
1457
1458            notifyDataSetChanged();
1459        }
1460    }
1461
1462    /**
1463     * Sets the currently selected folder safely.
1464     * @param folder the folder to change to. It is an error to pass null here.
1465     */
1466    private void setSelectedFolder(Folder folder) {
1467        if (folder == null) {
1468            mSelectedFolderUri = FolderUri.EMPTY;
1469            mCurrentFolderForUnreadCheck = null;
1470            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1471            return;
1472        }
1473
1474        final boolean viewChanged =
1475                !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1476
1477        // There are two cases in which the folder type is not set by this class.
1478        // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
1479        //    folder but its type was never set.
1480        // 2. The user backs into the default inbox. Going 'back' from the conversation list of
1481        //    any folder will take you to the default inbox for that account. (If you are in the
1482        //    default inbox already, back exits the app.)
1483        // In both these cases, the selected folder type is not set, and must be set.
1484        if (mSelectedDrawerItemCategory == DrawerItem.UNSET || (mCurrentAccount != null
1485                && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
1486            mSelectedDrawerItemCategory =
1487                    folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
1488            mSelectedFolderType = folder.type;
1489        }
1490
1491        mCurrentFolderForUnreadCheck = folder;
1492        mSelectedFolderUri = folder.folderUri;
1493        if (viewChanged) {
1494            if (mFolderAdapter != null) {
1495                mFolderAdapter.notifyDataSetChanged();
1496            }
1497            if (mMiniDrawerView != null) {
1498                mMiniDrawerView.refresh();
1499            }
1500        }
1501    }
1502
1503    public boolean isSelectedFolder(@NonNull Folder folder) {
1504        return folder.folderUri.equals(mSelectedFolderUri);
1505    }
1506
1507    /**
1508     * Sets the current account to the one provided here.
1509     * @param account the current account to set to.
1510     */
1511    private void setSelectedAccount(Account account) {
1512        final boolean changed = (account != null) && (mCurrentAccount == null
1513                || !mCurrentAccount.uri.equals(account.uri));
1514        mCurrentAccount = account;
1515        if (changed) {
1516            // Verify that the new account supports sending application feedback
1517            updateFooterItems();
1518            // We no longer have proper folder objects. Let the new ones come in
1519            mFolderAdapter.setCursor(null);
1520            // If currentAccount is different from the one we set, restart the loader. Look at the
1521            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1522            // don't just do restartLoader.
1523            final LoaderManager manager = getLoaderManager();
1524            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1525            manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1526            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1527            manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1528            // An updated cursor causes the entire list to refresh. No need to refresh the list.
1529            // But we do need to blank out the current folder, since the account might not be
1530            // synced.
1531            mSelectedFolderUri = FolderUri.EMPTY;
1532            mCurrentFolderForUnreadCheck = null;
1533
1534            // also set/update the mini-drawer
1535            if (mMiniDrawerAccountsAdapter != null) {
1536                mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount);
1537            }
1538
1539        } else if (account == null) {
1540            // This should never happen currently, but is a safeguard against a very incorrect
1541            // non-null account -> null account transition.
1542            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1543            final LoaderManager manager = getLoaderManager();
1544            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1545            manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1546        }
1547    }
1548
1549    private void updateFooterItems() {
1550        mFooterAdapter.update();
1551    }
1552
1553    /**
1554     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1555     */
1556    private boolean isFolderTypeExcluded(final Folder folder) {
1557        if (mExcludedFolderTypes == null) {
1558            return false;
1559        }
1560
1561        for (final int excludedType : mExcludedFolderTypes) {
1562            if (folder.isType(excludedType)) {
1563                return true;
1564            }
1565        }
1566
1567        return false;
1568    }
1569
1570    /**
1571     * @return the choice mode to use for the {@link ListView}
1572     */
1573    protected int getListViewChoiceMode() {
1574        return mAccountController.getFolderListViewChoiceMode();
1575    }
1576
1577
1578    /**
1579     * Drawer listener for footer functionality to react to drawer state.
1580     */
1581    public class DrawerStateListener implements DrawerLayout.DrawerListener {
1582
1583        private FooterItem mPendingFooterClick;
1584
1585        public void setPendingFooterClick(FooterItem itemClicked) {
1586            mPendingFooterClick = itemClicked;
1587        }
1588
1589        @Override
1590        public void onDrawerSlide(View drawerView, float slideOffset) {}
1591
1592        @Override
1593        public void onDrawerOpened(View drawerView) {}
1594
1595        @Override
1596        public void onDrawerClosed(View drawerView) {
1597            if (mPendingFooterClick != null) {
1598                mPendingFooterClick.onFooterClicked();
1599                mPendingFooterClick = null;
1600            }
1601        }
1602
1603        @Override
1604        public void onDrawerStateChanged(int newState) {}
1605
1606    }
1607
1608    private class FolderOrAccountListener extends DataSetObserver {
1609
1610        @Override
1611        public void onChanged() {
1612            // First, check if there's a folder to change to
1613            if (mNextFolder != null) {
1614                mFolderChanger.onFolderSelected(mNextFolder);
1615                mNextFolder = null;
1616            }
1617            // Next, check if there's an account to change to
1618            if (mNextAccount != null) {
1619                mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
1620                mNextAccount = null;
1621            }
1622        }
1623    }
1624
1625    @Override
1626    public ListAdapter getListAdapter() {
1627        // Ensures that we get the adapter with the header views.
1628        throw new UnsupportedOperationException("Use getListView().getAdapter() instead "
1629                + "which accounts for any header or footer views.");
1630    }
1631
1632    protected class MiniDrawerAccountsAdapter extends BaseAdapter {
1633
1634        private List<Account> mAccounts = new ArrayList<>();
1635
1636        public void setAccounts(Account[] accounts, Account currentAccount) {
1637            mAccounts.clear();
1638            if (currentAccount == null) {
1639                notifyDataSetChanged();
1640                return;
1641            }
1642            mAccounts.add(currentAccount);
1643            // TODO: sort by most recent accounts
1644            for (final Account account : accounts) {
1645                if (!account.getEmailAddress().equals(currentAccount.getEmailAddress())) {
1646                    mAccounts.add(account);
1647                }
1648            }
1649            notifyDataSetChanged();
1650        }
1651
1652        @Override
1653        public int getCount() {
1654            return mAccounts.size();
1655        }
1656
1657        @Override
1658        public Object getItem(int position) {
1659            // Is there an attempt made to access outside of the drawer item list?
1660            if (position >= mAccounts.size()) {
1661                return null;
1662            } else {
1663                return mAccounts.get(position);
1664            }
1665        }
1666
1667        @Override
1668        public long getItemId(int position) {
1669            return getItem(position).hashCode();
1670        }
1671
1672        @Override
1673        public View getView(int position, View convertView, ViewGroup parent) {
1674            final ImageView iv = convertView != null ? (ImageView) convertView :
1675                    (ImageView) LayoutInflater.from(getActivity()).inflate(
1676                    R.layout.mini_drawer_recent_account_item, parent, false /* attachToRoot */);
1677            final MiniDrawerAccountItem item = new MiniDrawerAccountItem(iv);
1678            item.setupDrawable();
1679            item.setAccount(mAccounts.get(position));
1680            iv.setTag(item);
1681            return iv;
1682        }
1683
1684        private class MiniDrawerAccountItem implements View.OnClickListener {
1685            private Account mAccount;
1686            private AccountAvatarDrawable mDrawable;
1687            public final ImageView view;
1688
1689            public MiniDrawerAccountItem(ImageView iv) {
1690                view = iv;
1691                view.setOnClickListener(this);
1692            }
1693
1694            public void setupDrawable() {
1695                mDrawable = new AccountAvatarDrawable(getResources(), getBitmapCache(),
1696                        getContactResolver());
1697                mDrawable.setDecodeDimensions(mMiniDrawerAvatarDecodeSize,
1698                        mMiniDrawerAvatarDecodeSize);
1699                view.setImageDrawable(mDrawable);
1700            }
1701
1702            public void setAccount(Account acct) {
1703                mAccount = acct;
1704                mDrawable.bind(mAccount.getSenderName(), mAccount.getEmailAddress());
1705                String contentDescription = mAccount.getDisplayName();
1706                if (TextUtils.isEmpty(contentDescription)) {
1707                    contentDescription = mAccount.getEmailAddress();
1708                }
1709                view.setContentDescription(contentDescription);
1710            }
1711
1712            @Override
1713            public void onClick(View v) {
1714                onAccountSelected(mAccount);
1715            }
1716        }
1717    }
1718
1719    protected void setupMiniDrawerAccountsAdapter() {
1720        mMiniDrawerAccountsAdapter = new MiniDrawerAccountsAdapter();
1721    }
1722
1723    protected ListAdapter getMiniDrawerAccountsAdapter() {
1724        return mMiniDrawerAccountsAdapter;
1725    }
1726
1727    private static class FadeAnimatorListener extends AnimatorListenerAdapter {
1728        private boolean mCanceled;
1729        private final View mView;
1730        private final boolean mFadeOut;
1731
1732        FadeAnimatorListener(View v, boolean fadeOut) {
1733            mView = v;
1734            mFadeOut = fadeOut;
1735        }
1736
1737        @Override
1738        public void onAnimationStart(Animator animation) {
1739            if (!mFadeOut) {
1740                mView.setVisibility(View.VISIBLE);
1741            }
1742            mCanceled = false;
1743        }
1744
1745        @Override
1746        public void onAnimationCancel(Animator animation) {
1747            mCanceled = true;
1748        }
1749
1750        @Override
1751        public void onAnimationEnd(Animator animation) {
1752            if (!mCanceled) {
1753                // Only need to set visibility to INVISIBLE for fade-out and not fade-in.
1754                if (mFadeOut) {
1755                    mView.setVisibility(View.INVISIBLE);
1756                }
1757                // If the animation is canceled, then the next animation onAnimationEnd will disable
1758                // the hardware layer.
1759                mView.setLayerType(View.LAYER_TYPE_NONE, null);
1760            }
1761        }
1762    }
1763
1764}
1765