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