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 
18 package com.android.mail.ui;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ObjectAnimator;
23 import android.app.Activity;
24 import android.app.ListFragment;
25 import android.app.LoaderManager;
26 import android.content.Loader;
27 import android.database.DataSetObserver;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.support.annotation.NonNull;
31 import android.support.v4.widget.DrawerLayout;
32 import android.text.TextUtils;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.ArrayAdapter;
37 import android.widget.BaseAdapter;
38 import android.widget.ImageView;
39 import android.widget.ListAdapter;
40 import android.widget.ListView;
41 
42 import com.android.bitmap.BitmapCache;
43 import com.android.bitmap.UnrefedBitmapCache;
44 import com.android.mail.R;
45 import com.android.mail.analytics.Analytics;
46 import com.android.mail.bitmap.AccountAvatarDrawable;
47 import com.android.mail.bitmap.ContactResolver;
48 import com.android.mail.browse.MergedAdapter;
49 import com.android.mail.content.ObjectCursor;
50 import com.android.mail.content.ObjectCursorLoader;
51 import com.android.mail.drawer.DrawerItem;
52 import com.android.mail.drawer.FooterItem;
53 import com.android.mail.providers.Account;
54 import com.android.mail.providers.AccountObserver;
55 import com.android.mail.providers.AllAccountObserver;
56 import com.android.mail.providers.Folder;
57 import com.android.mail.providers.FolderObserver;
58 import com.android.mail.providers.FolderWatcher;
59 import com.android.mail.providers.RecentFolderObserver;
60 import com.android.mail.providers.UIProvider;
61 import com.android.mail.providers.UIProvider.FolderType;
62 import com.android.mail.utils.FolderUri;
63 import com.android.mail.utils.LogTag;
64 import com.android.mail.utils.LogUtils;
65 import com.android.mail.utils.Utils;
66 import com.google.common.collect.Lists;
67 
68 import java.util.ArrayList;
69 import java.util.Iterator;
70 import 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  */
103 public 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      */
FolderListFragment()232     public FolderListFragment() {
233         super();
234     }
235 
236     @Override
toString()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      */
ofTree(Folder folder)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      */
ofTopLevelTree(Uri folderListUri, final ArrayList<Integer> excludedFolderTypes)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      */
getBundleFromArgs(Folder parentFolder, Uri folderListUri, final ArrayList<Integer> excludedFolderTypes)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
onActivityCreated(Bundle savedState)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 
getBitmapCache()434     public BitmapCache getBitmapCache() {
435         return mImagesCache;
436     }
437 
getContactResolver()438     public ContactResolver getContactResolver() {
439         return mContactResolver;
440     }
441 
toggleDrawerState()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      */
setInstanceFromBundle(Bundle args)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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 
addListHeader(LayoutInflater inflater, View rootView, ListView list)503     protected void addListHeader(LayoutInflater inflater, View rootView, ListView list) {
504         // Default impl does nothing
505     }
506 
507     @Override
onStart()508     public void onStart() {
509         super.onStart();
510     }
511 
512     @Override
onStop()513     public void onStop() {
514         super.onStop();
515     }
516 
517     @Override
onPause()518     public void onPause() {
519         super.onPause();
520     }
521 
522     @Override
onSaveInstanceState(Bundle outState)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
onDestroyView()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
onListItemClick(ListView l, View v, int position, long id)570     public void onListItemClick(ListView l, View v, int position, long id) {
571         viewFolderOrChangeAccount(position);
572     }
573 
getDefaultInbox(Account account)574     private Folder getDefaultInbox(Account account) {
575         if (account == null || mFolderWatcher == null) {
576             return null;
577         }
578         return mFolderWatcher.getDefaultInbox(account);
579     }
580 
getUnreadCount(Account account)581     protected int getUnreadCount(Account account) {
582         if (account == null || mFolderWatcher == null) {
583             return 0;
584         }
585         return mFolderWatcher.getUnreadCount(account);
586     }
587 
changeAccount(final Account account)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      */
viewFolderOrChangeAccount(int position)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 
onFolderSelected(Folder folder, String analyticsLabel)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 
onAccountSelected(Account account)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data)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
onLoaderReset(Loader<ObjectCursor<Folder>> loader)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      */
getAllAccounts()735     public Account[] getAllAccounts() {
736         if (mAllAccountsObserver != null) {
737             return mAllAccountsObserver.getAllAccounts();
738         }
739         return new Account[0];
740     }
741 
newAccountsAdapter()742     protected AccountsAdapter newAccountsAdapter() {
743         return new AccountsAdapter();
744     }
745 
746     @Override
onUnreadCountChange()747     public void onUnreadCountChange() {
748         if (mAccountsAdapter != null) {
749             mAccountsAdapter.notifyDataSetChanged();
750         }
751     }
752 
isMiniDrawerEnabled()753     public boolean isMiniDrawerEnabled() {
754         return mMiniDrawerEnabled;
755     }
756 
setMiniDrawerEnabled(boolean enabled)757     public void setMiniDrawerEnabled(boolean enabled) {
758         mMiniDrawerEnabled = enabled;
759         setMinimized(isMinimized()); // init visual state
760     }
761 
isMinimized()762     public boolean isMinimized() {
763         return mMiniDrawerEnabled && mIsMinimized;
764     }
765 
setMinimized(boolean minimized)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 
animateMinimized(boolean minimized)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 
onDrawerDragStarted()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 
onDrawerDrag(float percent)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. */
setCursor(ObjectCursor<Folder> cursor)850         void setCursor(ObjectCursor<Folder> cursor);
getCursor()851         ObjectCursor<Folder> getCursor();
852         /** Update the all folder list cursor with the cursor given here. */
setAllFolderListCursor(ObjectCursor<Folder> cursor)853         void setAllFolderListCursor(ObjectCursor<Folder> cursor);
854         /** Remove all observers and destroy the object. */
destroy()855         void destroy();
856         /** Notifies the adapter that the data has changed. */
notifyDataSetChanged()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          */
FolderAdapter(boolean isDivided)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
getView(int position, View convertView, ViewGroup parent)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
getViewTypeCount()929         public int getViewTypeCount() {
930             // Accounts, headers, folders (all parts of drawer view types)
931             return DrawerItem.getViewTypeCount();
932         }
933 
934         @Override
getItemViewType(int position)935         public int getItemViewType(int position) {
936             return ((DrawerItem) getItem(position)).getType();
937         }
938 
939         @Override
getCount()940         public int getCount() {
941             return mItemList.size();
942         }
943 
944         @Override
isEnabled(int position)945         public boolean isEnabled(int position) {
946             final DrawerItem drawerItem = ((DrawerItem) getItem(position));
947             return drawerItem != null && drawerItem.isItemEnabled();
948         }
949 
950         @Override
areAllItemsEnabled()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          */
getRecentFolders(RecentFolderList recentList)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          */
rebuildFolderList()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          */
recalculateListFolders()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
recalculateFlatListFolders(List<DrawerItem> itemList)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
recalculateDividedListFolders(List<DrawerItem> itemList)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          */
addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source, int headerStringResource)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          */
addRecentsToList(List<DrawerItem> destination)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          */
isCursorInvalid()1160         private boolean isCursorInvalid() {
1161             return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
1162                     || !mCursor.moveToFirst();
1163         }
1164 
1165         @Override
setCursor(ObjectCursor<Folder> cursor)1166         public void setCursor(ObjectCursor<Folder> cursor) {
1167             mCursor = cursor;
1168             rebuildAccountList();
1169             rebuildFolderList();
1170         }
1171 
1172         @Override
getCursor()1173         public ObjectCursor<Folder> getCursor() {
1174             return mCursor;
1175         }
1176 
1177         @Override
setAllFolderListCursor(final ObjectCursor<Folder> cursor)1178         public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1179             mAllFolderListCursor = cursor;
1180             rebuildAccountList();
1181             rebuildFolderList();
1182         }
1183 
1184         @Override
getItem(int position)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
getItemId(int position)1195         public long getItemId(int position) {
1196             return getItem(position).hashCode();
1197         }
1198 
1199         @Override
destroy()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 
HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder)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
getViewTypeCount()1221         public int getViewTypeCount() {
1222             // Child and Parent
1223             return 2;
1224         }
1225 
1226         @Override
getItemViewType(int position)1227         public int getItemViewType(int position) {
1228             final Folder f = getItem(position);
1229             return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
1230         }
1231 
1232         @Override
getView(int position, View convertView, ViewGroup parent)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
setCursor(ObjectCursor<Folder> cursor)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
getCursor()1278         public ObjectCursor<Folder> getCursor() {
1279             throw new UnsupportedOperationException("drawers don't have hierarchical folders");
1280         }
1281 
1282         @Override
setAllFolderListCursor(final ObjectCursor<Folder> cursor)1283         public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1284             // Not necessary in HierarchicalFolderListAdapter
1285         }
1286 
1287         @Override
destroy()1288         public void destroy() {
1289             // Do nothing.
1290         }
1291     }
1292 
rebuildAccountList()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 
AccountsAdapter()1308         public AccountsAdapter() {
1309             mAccounts = new ArrayList<>();
1310         }
1311 
setAccounts(List<DrawerItem> accounts)1312         public void setAccounts(List<DrawerItem> accounts) {
1313             mAccounts = accounts;
1314             notifyDataSetChanged();
1315         }
1316 
1317         @Override
getCount()1318         public int getCount() {
1319             return mAccounts.size();
1320         }
1321 
1322         @Override
getItem(int position)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
getItemId(int position)1333         public long getItemId(int position) {
1334             return getItem(position).hashCode();
1335         }
1336 
1337         @Override
getView(int position, View convertView, ViewGroup parent)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      */
buildAccountListDrawerItems()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 
getCurrentAccountUri()1363     private Uri getCurrentAccountUri() {
1364         return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
1365     }
1366 
getCurrentAccountEmailAddress()1367     protected String getCurrentAccountEmailAddress() {
1368         return mCurrentAccount == null ? "" : mCurrentAccount.getEmailAddress();
1369     }
1370 
getMergedAdapter()1371     protected MergedAdapter<ListAdapter> getMergedAdapter() {
1372         return mMergedAdapter;
1373     }
1374 
getFoldersCursor()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 
FooterAdapter()1383         private FooterAdapter() {
1384             update();
1385         }
1386 
1387         @Override
getCount()1388         public int getCount() {
1389             return mFooterItems.size();
1390         }
1391 
1392         @Override
getItem(int position)1393         public DrawerItem getItem(int position) {
1394             return mFooterItems.get(position);
1395         }
1396 
1397         @Override
getItemId(int position)1398         public long getItemId(int position) {
1399             return position;
1400         }
1401 
1402         @Override
getViewTypeCount()1403         public int getViewTypeCount() {
1404             // Accounts, headers, folders (all parts of drawer view types)
1405             return DrawerItem.getViewTypeCount();
1406         }
1407 
1408         @Override
getItemViewType(int position)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
getView(int position, View convertView, ViewGroup parent)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          */
update()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      */
setSelectedFolder(Folder folder)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 
isSelectedFolder(@onNull Folder folder)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      */
setSelectedAccount(Account account)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 
updateFooterItems()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      */
isFolderTypeExcluded(final Folder folder)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      */
getListViewChoiceMode()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 
setPendingFooterClick(FooterItem itemClicked)1578         public void setPendingFooterClick(FooterItem itemClicked) {
1579             mPendingFooterClick = itemClicked;
1580         }
1581 
1582         @Override
onDrawerSlide(View drawerView, float slideOffset)1583         public void onDrawerSlide(View drawerView, float slideOffset) {}
1584 
1585         @Override
onDrawerOpened(View drawerView)1586         public void onDrawerOpened(View drawerView) {}
1587 
1588         @Override
onDrawerClosed(View drawerView)1589         public void onDrawerClosed(View drawerView) {
1590             if (mPendingFooterClick != null) {
1591                 mPendingFooterClick.onFooterClicked();
1592                 mPendingFooterClick = null;
1593             }
1594         }
1595 
1596         @Override
onDrawerStateChanged(int newState)1597         public void onDrawerStateChanged(int newState) {}
1598 
1599     }
1600 
1601     private class FolderOrAccountListener extends DataSetObserver {
1602 
1603         @Override
onChanged()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
getListAdapter()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 
setAccounts(Account[] accounts, Account currentAccount)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
getCount()1646         public int getCount() {
1647             return mAccounts.size();
1648         }
1649 
1650         @Override
getItem(int position)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
getItemId(int position)1661         public long getItemId(int position) {
1662             return getItem(position).hashCode();
1663         }
1664 
1665         @Override
getView(int position, View convertView, ViewGroup parent)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 
MiniDrawerAccountItem(ImageView iv)1682             public MiniDrawerAccountItem(ImageView iv) {
1683                 view = iv;
1684                 view.setOnClickListener(this);
1685             }
1686 
setupDrawable()1687             public void setupDrawable() {
1688                 mDrawable = new AccountAvatarDrawable(getResources(), getBitmapCache(),
1689                         getContactResolver());
1690                 mDrawable.setDecodeDimensions(mMiniDrawerAvatarDecodeSize,
1691                         mMiniDrawerAvatarDecodeSize);
1692                 view.setImageDrawable(mDrawable);
1693             }
1694 
setAccount(Account acct)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
onClick(View v)1706             public void onClick(View v) {
1707                 onAccountSelected(mAccount);
1708             }
1709         }
1710     }
1711 
setupMiniDrawerAccountsAdapter()1712     protected void setupMiniDrawerAccountsAdapter() {
1713         mMiniDrawerAccountsAdapter = new MiniDrawerAccountsAdapter();
1714     }
1715 
getMiniDrawerAccountsAdapter()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 
FadeAnimatorListener(View v, boolean fadeOut)1725         FadeAnimatorListener(View v, boolean fadeOut) {
1726             mView = v;
1727             mFadeOut = fadeOut;
1728         }
1729 
1730         @Override
onAnimationStart(Animator animation)1731         public void onAnimationStart(Animator animation) {
1732             if (!mFadeOut) {
1733                 mView.setVisibility(View.VISIBLE);
1734             }
1735             mCanceled = false;
1736         }
1737 
1738         @Override
onAnimationCancel(Animator animation)1739         public void onAnimationCancel(Animator animation) {
1740             mCanceled = true;
1741         }
1742 
1743         @Override
onAnimationEnd(Animator animation)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