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