• 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.net.Uri;
25 import android.os.Bundle;
26 import android.support.v4.text.BidiFormatter;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.ArrayAdapter;
31 import android.widget.BaseAdapter;
32 import android.widget.ImageView;
33 import android.widget.ListAdapter;
34 import android.widget.ListView;
35 
36 import com.android.mail.R;
37 import com.android.mail.adapter.DrawerItem;
38 import com.android.mail.analytics.Analytics;
39 import com.android.mail.content.ObjectCursor;
40 import com.android.mail.content.ObjectCursorLoader;
41 import com.android.mail.providers.Account;
42 import com.android.mail.providers.AccountObserver;
43 import com.android.mail.providers.AllAccountObserver;
44 import com.android.mail.providers.DrawerClosedObserver;
45 import com.android.mail.providers.Folder;
46 import com.android.mail.providers.FolderObserver;
47 import com.android.mail.providers.FolderWatcher;
48 import com.android.mail.providers.RecentFolderObserver;
49 import com.android.mail.providers.UIProvider;
50 import com.android.mail.providers.UIProvider.FolderType;
51 import com.android.mail.utils.FolderUri;
52 import com.android.mail.utils.LogTag;
53 import com.android.mail.utils.LogUtils;
54 
55 import java.util.ArrayList;
56 import java.util.Iterator;
57 import java.util.List;
58 
59 /**
60  * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
61  * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
62  * in a drawer along with the list of folders.
63  *
64  * This class has the following use-cases:
65  * <ul>
66  *     <li>
67  *         Show a list of accounts and a divided list of folders. In this case, the list shows
68  *         Accounts, Inboxes, Recent Folders, All folders.
69  *         Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
70  *         folders switches folders.
71  *         This is created through XML resources as a {@link DrawerFragment}. Since it is created
72  *         through resources, it receives all arguments through callbacks.
73  *     </li>
74  *     <li>
75  *         Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
76  *         Drafts, Starred, and any user-created folders. For providers that allow nested folders,
77  *         this will only show the folders at the top-level.
78  *         <br /> Tapping on a parent folder creates a new fragment with the child folders at
79  *         that level.
80  *     </li>
81  *     <li>
82  *         Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
83  *         {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
84  *         any folder for a given account.
85  *     </li>
86  * </ul>
87  */
88 public class FolderListFragment extends ListFragment implements
89         LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
90     private static final String LOG_TAG = LogTag.getLogTag();
91     /** The parent activity */
92     private ControllableActivity mActivity;
93     private BidiFormatter mBidiFormatter;
94     /** The underlying list view */
95     private ListView mListView;
96     /** URI that points to the list of folders for the current account. */
97     private Uri mFolderListUri;
98     /**
99      * True if you want a divided FolderList. A divided folder list shows the following groups:
100      * Inboxes, Recent Folders, All folders.
101      *
102      * An undivided FolderList shows all folders without any divisions and without recent folders.
103      * This is true only for the drawer: for all others it is false.
104      */
105     protected boolean mIsDivided = false;
106     /** True if the folder list belongs to a folder selection activity (one account only) */
107     protected boolean mHideAccounts = true;
108     /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
109     private ArrayList<Integer> mExcludedFolderTypes;
110     /** Object that changes folders on our behalf. */
111     private FolderSelector mFolderChanger;
112     /** Object that changes accounts on our behalf */
113     private AccountController mAccountController;
114 
115     /** The currently selected folder (the folder being viewed).  This is never null. */
116     private FolderUri mSelectedFolderUri = FolderUri.EMPTY;
117     /**
118      * The current folder from the controller.  This is meant only to check when the unread count
119      * goes out of sync and fixing it.
120      */
121     private Folder mCurrentFolderForUnreadCheck;
122     /** Parent of the current folder, or null if the current folder is not a child. */
123     private Folder mParentFolder;
124 
125     private static final int FOLDER_LIST_LOADER_ID = 0;
126     /** Loader id for the list of all folders in the account */
127     private static final int ALL_FOLDER_LIST_LOADER_ID = 1;
128     /** Key to store {@link #mParentFolder}. */
129     private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
130     /** Key to store {@link #mFolderListUri}. */
131     private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
132     /** Key to store {@link #mExcludedFolderTypes} */
133     private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
134 
135     private static final String BUNDLE_LIST_STATE = "flf-list-state";
136     private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
137     private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
138 
139     private FolderListFragmentCursorAdapter mCursorAdapter;
140     /** Observer to wait for changes to the current folder so we can change the selected folder */
141     private FolderObserver mFolderObserver = null;
142     /** Listen for account changes. */
143     private AccountObserver mAccountObserver = null;
144     /** Listen for account changes. */
145     private DrawerClosedObserver mDrawerObserver = null;
146     /** Listen to changes to list of all accounts */
147     private AllAccountObserver mAllAccountsObserver = null;
148     /**
149      * Type of currently selected folder: {@link DrawerItem#FOLDER_INBOX},
150      * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_OTHER}.
151      * Set as {@link DrawerItem#UNSET} to begin with, as there is nothing selected yet.
152      */
153     private int mSelectedFolderType = DrawerItem.UNSET;
154     /** The current account according to the controller */
155     private Account mCurrentAccount;
156     /** The account we will change to once the drawer (if any) is closed */
157     private Account mNextAccount = null;
158     /** The folder we will change to once the drawer (if any) is closed */
159     private Folder mNextFolder = null;
160 
161     /**
162      * Constructor needs to be public to handle orientation changes and activity lifecycle events.
163      */
FolderListFragment()164     public FolderListFragment() {
165         super();
166     }
167 
168     @Override
toString()169     public String toString() {
170         final StringBuilder sb = new StringBuilder(super.toString());
171         sb.setLength(sb.length() - 1);
172         sb.append(" folder=");
173         sb.append(mFolderListUri);
174         sb.append(" parent=");
175         sb.append(mParentFolder);
176         sb.append(" adapterCount=");
177         sb.append(mCursorAdapter != null ? mCursorAdapter.getCount() : -1);
178         sb.append("}");
179         return sb.toString();
180     }
181 
182     /**
183      * Creates a new instance of {@link FolderListFragment}, initialized
184      * to display the folder and its immediate children.
185      * @param folder parent folder whose children are shown
186      *
187      */
ofTree(Folder folder)188     public static FolderListFragment ofTree(Folder folder) {
189         final FolderListFragment fragment = new FolderListFragment();
190         fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
191         return fragment;
192     }
193 
194     /**
195      * Creates a new instance of {@link FolderListFragment}, initialized
196      * to display the top level: where we have no parent folder, but we have a list of folders
197      * from the account.
198      * @param folderListUri the URI which contains all the list of folders
199      * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
200      */
ofTopLevelTree(Uri folderListUri, final ArrayList<Integer> excludedFolderTypes)201     public static FolderListFragment ofTopLevelTree(Uri folderListUri,
202             final ArrayList<Integer> excludedFolderTypes) {
203         final FolderListFragment fragment = new FolderListFragment();
204         fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
205         return fragment;
206     }
207 
208     /**
209      * Construct a bundle that represents the state of this fragment.
210      *
211      * @param parentFolder non-null for trees, the parent of this list
212      * @param folderListUri the URI which contains all the list of folders
213      * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
214      * @return Bundle containing parentFolder, divided list boolean and
215      *         excluded folder types
216      */
getBundleFromArgs(Folder parentFolder, Uri folderListUri, final ArrayList<Integer> excludedFolderTypes)217     private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
218             final ArrayList<Integer> excludedFolderTypes) {
219         final Bundle args = new Bundle(3);
220         if (parentFolder != null) {
221             args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
222         }
223         if (folderListUri != null) {
224             args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
225         }
226         if (excludedFolderTypes != null) {
227             args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
228         }
229         return args;
230     }
231 
232     @Override
onActivityCreated(Bundle savedState)233     public void onActivityCreated(Bundle savedState) {
234         super.onActivityCreated(savedState);
235         // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
236         // only activity creating a ConversationListContext is a MailActivity which is of type
237         // ControllableActivity, so this cast should be safe. If this cast fails, some other
238         // activity is creating ConversationListFragments. This activity must be of type
239         // ControllableActivity.
240         final Activity activity = getActivity();
241         if (! (activity instanceof ControllableActivity)){
242             LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
243                     "create it. Cannot proceed.");
244             return;
245         }
246         mActivity = (ControllableActivity) activity;
247         mBidiFormatter = BidiFormatter.getInstance();
248         final FolderController controller = mActivity.getFolderController();
249         // Listen to folder changes in the future
250         mFolderObserver = new FolderObserver() {
251             @Override
252             public void onChanged(Folder newFolder) {
253                 setSelectedFolder(newFolder);
254             }
255         };
256         final Folder currentFolder;
257         if (controller != null) {
258             // Only register for selected folder updates if we have a controller.
259             currentFolder = mFolderObserver.initialize(controller);
260             mCurrentFolderForUnreadCheck = currentFolder;
261         } else {
262             currentFolder = null;
263         }
264 
265         // Initialize adapter for folder/heirarchical list.  Note this relies on
266         // mActivity being initialized.
267         final Folder selectedFolder;
268         if (mParentFolder != null) {
269             mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
270             selectedFolder = mActivity.getHierarchyFolder();
271         } else {
272             mCursorAdapter = new FolderListAdapter(mIsDivided);
273             selectedFolder = currentFolder;
274         }
275         // Is the selected folder fresher than the one we have restored from a bundle?
276         if (selectedFolder != null
277                 && !selectedFolder.folderUri.equals(mSelectedFolderUri)) {
278             setSelectedFolder(selectedFolder);
279         }
280 
281         // Assign observers for current account & all accounts
282         final AccountController accountController = mActivity.getAccountController();
283         mAccountObserver = new AccountObserver() {
284             @Override
285             public void onChanged(Account newAccount) {
286                 setSelectedAccount(newAccount);
287             }
288         };
289         mFolderChanger = mActivity.getFolderSelector();
290         if (accountController != null) {
291             // Current account and its observer.
292             setSelectedAccount(mAccountObserver.initialize(accountController));
293             // List of all accounts and its observer.
294             mAllAccountsObserver = new AllAccountObserver(){
295                 @Override
296                 public void onChanged(Account[] allAccounts) {
297                     mCursorAdapter.notifyAllAccountsChanged();
298                 }
299             };
300             mAllAccountsObserver.initialize(accountController);
301             mAccountController = accountController;
302 
303             // Observer for when the drawer is closed
304             mDrawerObserver = new DrawerClosedObserver() {
305                 @Override
306                 public void onDrawerClosed() {
307                     // First, check if there's a folder to change to
308                     if (mNextFolder != null) {
309                         mFolderChanger.onFolderSelected(mNextFolder);
310                         mNextFolder = null;
311                     }
312                     // Next, check if there's an account to change to
313                     if (mNextAccount != null) {
314                         mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
315                         mNextAccount = null;
316                     }
317                 }
318             };
319             mDrawerObserver.initialize(accountController);
320         }
321 
322         if (mActivity.isFinishing()) {
323             // Activity is finishing, just bail.
324             return;
325         }
326 
327         mListView.setChoiceMode(getListViewChoiceMode());
328 
329         setListAdapter(mCursorAdapter);
330     }
331 
332     /**
333      * Set the instance variables from the arguments provided here.
334      * @param args bundle of arguments with keys named ARG_*
335      */
setInstanceFromBundle(Bundle args)336     private void setInstanceFromBundle(Bundle args) {
337         if (args == null) {
338             return;
339         }
340         mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
341         final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
342         if (folderUri != null) {
343             mFolderListUri = Uri.parse(folderUri);
344         }
345         mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
346     }
347 
348     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)349     public View onCreateView(LayoutInflater inflater, ViewGroup container,
350             Bundle savedState) {
351         setInstanceFromBundle(getArguments());
352 
353         final View rootView = inflater.inflate(R.layout.folder_list, null);
354         mListView = (ListView) rootView.findViewById(android.R.id.list);
355         mListView.setEmptyView(null);
356         mListView.setDivider(null);
357         if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
358             mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
359         }
360         if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
361             mSelectedFolderUri =
362                     new FolderUri(Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)));
363             mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
364         } else if (mParentFolder != null) {
365             mSelectedFolderUri = mParentFolder.folderUri;
366             // No selected folder type required for hierarchical lists.
367         }
368 
369         return rootView;
370     }
371 
372     @Override
onStart()373     public void onStart() {
374         super.onStart();
375     }
376 
377     @Override
onStop()378     public void onStop() {
379         super.onStop();
380     }
381 
382     @Override
onPause()383     public void onPause() {
384         super.onPause();
385     }
386 
387     @Override
onSaveInstanceState(Bundle outState)388     public void onSaveInstanceState(Bundle outState) {
389         super.onSaveInstanceState(outState);
390         if (mListView != null) {
391             outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
392         }
393         if (mSelectedFolderUri != null) {
394             outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
395         }
396         outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
397     }
398 
399     @Override
onDestroyView()400     public void onDestroyView() {
401         if (mCursorAdapter != null) {
402             mCursorAdapter.destroy();
403         }
404         // Clear the adapter.
405         setListAdapter(null);
406         if (mFolderObserver != null) {
407             mFolderObserver.unregisterAndDestroy();
408             mFolderObserver = null;
409         }
410         if (mAccountObserver != null) {
411             mAccountObserver.unregisterAndDestroy();
412             mAccountObserver = null;
413         }
414         if (mAllAccountsObserver != null) {
415             mAllAccountsObserver.unregisterAndDestroy();
416             mAllAccountsObserver = null;
417         }
418         if (mDrawerObserver != null) {
419             mDrawerObserver.unregisterAndDestroy();
420             mDrawerObserver = null;
421         }
422         super.onDestroyView();
423     }
424 
425     @Override
onListItemClick(ListView l, View v, int position, long id)426     public void onListItemClick(ListView l, View v, int position, long id) {
427         viewFolderOrChangeAccount(position);
428     }
429 
getDefaultInbox(Account account)430     private Folder getDefaultInbox(Account account) {
431         if (account == null || mCursorAdapter == null) {
432             return null;
433         }
434         return mCursorAdapter.getDefaultInbox(account);
435     }
436 
changeAccount(final Account account)437     private void changeAccount(final Account account) {
438         // Switching accounts takes you to the default inbox for that account.
439         mSelectedFolderType = DrawerItem.FOLDER_INBOX;
440         mNextAccount = account;
441         mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
442         Analytics.getInstance().sendEvent("switch_account", "drawer_account_switch", null, 0);
443     }
444 
445     /**
446      * Display the conversation list from the folder at the position given.
447      * @param position a zero indexed position into the list.
448      */
viewFolderOrChangeAccount(int position)449     private void viewFolderOrChangeAccount(int position) {
450         final Object item = getListAdapter().getItem(position);
451         LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
452         final Folder folder;
453         int folderType = DrawerItem.UNSET;
454 
455         if (item instanceof DrawerItem) {
456             final DrawerItem drawerItem = (DrawerItem) item;
457             // Could be a folder or account.
458             final int itemType = mCursorAdapter.getItemType(drawerItem);
459             if (itemType == DrawerItem.VIEW_ACCOUNT) {
460                 // Account, so switch.
461                 folder = null;
462                 final Account account = drawerItem.mAccount;
463 
464                 if (account != null && account.settings.defaultInbox.equals(mSelectedFolderUri)) {
465                     // We're already in the default inbox for account, just re-check item ...
466                     final int defaultInboxPosition = position + 1;
467                     if (mListView.getChildAt(defaultInboxPosition) != null) {
468                         mListView.setItemChecked(defaultInboxPosition, true);
469                     }
470                     // ... and close the drawer (no new target folders/accounts)
471                     mAccountController.closeDrawer(false, mNextAccount,
472                             getDefaultInbox(mNextAccount));
473                 } else {
474                     changeAccount(account);
475                 }
476             } else if (itemType == DrawerItem.VIEW_FOLDER) {
477                 // Folder type, so change folders only.
478                 folder = drawerItem.mFolder;
479                 mSelectedFolderType = folderType = drawerItem.mFolderType;
480                 LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
481                         folder, mSelectedFolderType);
482             } else {
483                 // Do nothing.
484                 LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
485                         + " Clicked on unset item in drawer. Offending item is " + item);
486                 return;
487             }
488         } else if (item instanceof Folder) {
489             folder = (Folder) item;
490         } else {
491             // Don't know how we got here.
492             LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
493             folder = null;
494         }
495         if (folder != null) {
496             // Not changing the account.
497             final Account nextAccount = null;
498             // Go to the conversation list for this folder.
499             if (!folder.folderUri.equals(mSelectedFolderUri)) {
500                 mNextFolder = folder;
501                 mAccountController.closeDrawer(true, nextAccount, folder);
502 
503                 final String label = (folderType == DrawerItem.FOLDER_RECENT) ? "recent" : "normal";
504                 Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(),
505                         label, 0);
506 
507             } else {
508                 // Clicked on same folder, just close drawer
509                 mAccountController.closeDrawer(false, nextAccount, folder);
510             }
511         }
512     }
513 
514     @Override
onCreateLoader(int id, Bundle args)515     public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
516         mListView.setEmptyView(null);
517         final Uri folderListUri;
518         if (id == FOLDER_LIST_LOADER_ID) {
519             if (mFolderListUri != null) {
520                 // Folder trees, they specify a URI at construction time.
521                 folderListUri = mFolderListUri;
522             } else {
523                 // Drawers get the folder list from the current account.
524                 folderListUri = mCurrentAccount.folderListUri;
525             }
526         } else if (id == ALL_FOLDER_LIST_LOADER_ID) {
527             folderListUri = mCurrentAccount.allFolderListUri;
528         } else {
529             LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
530             return null;
531         }
532         return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
533                 UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
534     }
535 
536     @Override
onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data)537     public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
538         if (mCursorAdapter != null) {
539             if (loader.getId() == FOLDER_LIST_LOADER_ID) {
540                 mCursorAdapter.setCursor(data);
541             } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
542                 mCursorAdapter.setAllFolderListCursor(data);
543             }
544         }
545     }
546 
547     @Override
onLoaderReset(Loader<ObjectCursor<Folder>> loader)548     public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
549         if (mCursorAdapter != null) {
550             if (loader.getId() == FOLDER_LIST_LOADER_ID) {
551                 mCursorAdapter.setCursor(null);
552             } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
553                 mCursorAdapter.setAllFolderListCursor(null);
554             }
555         }
556     }
557 
558     /**
559      *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
560      *  frequency of use.
561      * @return a list of accounts, sorted by frequency of use
562      */
getAllAccounts()563     private Account[] getAllAccounts() {
564         if (mAllAccountsObserver != null) {
565             return mAllAccountsObserver.getAllAccounts();
566         }
567         return new Account[0];
568     }
569 
570     /**
571      * Interface for all cursor adapters that allow setting a cursor and being destroyed.
572      */
573     private interface FolderListFragmentCursorAdapter extends ListAdapter {
574         /** Update the folder list cursor with the cursor given here. */
setCursor(ObjectCursor<Folder> cursor)575         void setCursor(ObjectCursor<Folder> cursor);
576         /** Update the all folder list cursor with the cursor given here. */
setAllFolderListCursor(ObjectCursor<Folder> cursor)577         void setAllFolderListCursor(ObjectCursor<Folder> cursor);
578         /**
579          * Given an item, find the type of the item, which should only be {@link
580          * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
581          * @return item the type of the item.
582          */
getItemType(DrawerItem item)583         int getItemType(DrawerItem item);
584         /** Notify that the all accounts changed. */
notifyAllAccountsChanged()585         void notifyAllAccountsChanged();
586         /** Remove all observers and destroy the object. */
destroy()587         void destroy();
588         /** Notifies the adapter that the data has changed. */
notifyDataSetChanged()589         void notifyDataSetChanged();
590         /** Returns default inbox for this account. */
getDefaultInbox(Account account)591         Folder getDefaultInbox(Account account);
592         /** Returns the index of the first selected item, or -1 if no selection */
getSelectedPosition()593         int getSelectedPosition();
594     }
595 
596     /**
597      * An adapter for flat folder lists.
598      */
599     private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
600 
601         private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
602             @Override
603             public void onChanged() {
604                 if (!isCursorInvalid()) {
605                     recalculateList();
606                 }
607             }
608         };
609         /** No resource used for string header in folder list */
610         private static final int NO_HEADER_RESOURCE = -1;
611         /** Cache of most recently used folders */
612         private final RecentFolderList mRecentFolders;
613         /** True if the list is divided, false otherwise. See the comment on
614          * {@link FolderListFragment#mIsDivided} for more information */
615         private final boolean mIsDivided;
616         /** All the items */
617         private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
618         /** Cursor into the folder list. This might be null. */
619         private ObjectCursor<Folder> mCursor = null;
620         /** Cursor into the all folder list. This might be null. */
621         private ObjectCursor<Folder> mAllFolderListCursor = null;
622         /** Watcher for tracking and receiving unread counts for mail */
623         private FolderWatcher mFolderWatcher = null;
624         private boolean mRegistered = false;
625 
626         /**
627          * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
628          *
629          * @param isDivided true if folder list is flat, false if divided by label group. See
630          *                   the comments on {@link #mIsDivided} for more information
631          */
FolderListAdapter(boolean isDivided)632         public FolderListAdapter(boolean isDivided) {
633             super();
634             mIsDivided = isDivided;
635             final RecentFolderController controller = mActivity.getRecentFolderController();
636             if (controller != null && mIsDivided) {
637                 mRecentFolders = mRecentFolderObserver.initialize(controller);
638             } else {
639                 mRecentFolders = null;
640             }
641             mFolderWatcher = new FolderWatcher(mActivity, this);
642             mFolderWatcher.updateAccountList(getAllAccounts());
643         }
644 
645         @Override
notifyAllAccountsChanged()646         public void notifyAllAccountsChanged() {
647             if (!mRegistered && mAccountController != null) {
648                 // TODO(viki): Round-about way of setting the watcher. http://b/8750610
649                 mAccountController.setFolderWatcher(mFolderWatcher);
650                 mRegistered = true;
651             }
652             mFolderWatcher.updateAccountList(getAllAccounts());
653             recalculateList();
654         }
655 
656         @Override
getView(int position, View convertView, ViewGroup parent)657         public View getView(int position, View convertView, ViewGroup parent) {
658             final DrawerItem item = (DrawerItem) getItem(position);
659             final View view = item.getView(convertView, parent);
660             final int type = item.mType;
661             final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedFolderType);
662             if (type == DrawerItem.VIEW_FOLDER) {
663                 mListView.setItemChecked(position, isSelected);
664             }
665             // If this is the current folder, also check to verify that the unread count
666             // matches what the action bar shows.
667             if (type == DrawerItem.VIEW_FOLDER
668                     && isSelected
669                     && (mCurrentFolderForUnreadCheck != null)
670                     && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
671                 ((FolderItemView) view).overrideUnreadCount(
672                         mCurrentFolderForUnreadCheck.unreadCount);
673             }
674             return view;
675         }
676 
677         @Override
getViewTypeCount()678         public int getViewTypeCount() {
679             // Accounts, headers, folders (all parts of drawer view types)
680             return DrawerItem.getViewTypes();
681         }
682 
683         @Override
getItemViewType(int position)684         public int getItemViewType(int position) {
685             return ((DrawerItem) getItem(position)).mType;
686         }
687 
688         @Override
getCount()689         public int getCount() {
690             return mItemList.size();
691         }
692 
693         @Override
isEnabled(int position)694         public boolean isEnabled(int position) {
695             final DrawerItem drawerItem = ((DrawerItem) getItem(position));
696             return drawerItem != null && drawerItem.isItemEnabled();
697         }
698 
getCurrentAccountUri()699         private Uri getCurrentAccountUri() {
700             return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
701         }
702 
703         @Override
areAllItemsEnabled()704         public boolean areAllItemsEnabled() {
705             // We have headers and thus some items are not enabled.
706             return false;
707         }
708 
709         /**
710          * Returns all the recent folders from the list given here. Safe to call with a null list.
711          * @param recentList a list of all recently accessed folders.
712          * @return a valid list of folders, which are all recent folders.
713          */
getRecentFolders(RecentFolderList recentList)714         private List<Folder> getRecentFolders(RecentFolderList recentList) {
715             final List<Folder> folderList = new ArrayList<Folder>();
716             if (recentList == null) {
717                 return folderList;
718             }
719             // Get all recent folders, after removing system folders.
720             for (final Folder f : recentList.getRecentFolderList(null)) {
721                 if (!f.isProviderFolder()) {
722                     folderList.add(f);
723                 }
724             }
725             return folderList;
726         }
727 
728         /**
729          * Responsible for verifying mCursor, and ensuring any recalculate
730          * conditions are met. Also calls notifyDataSetChanged once it's finished
731          * populating {@link FolderListAdapter#mItemList}
732          */
recalculateList()733         private void recalculateList() {
734             final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>();
735             // Don't show accounts for single-account-based folder selection (i.e. widgets)
736             if (!mHideAccounts) {
737                 recalculateListAccounts(newFolderList);
738             }
739             recalculateListFolders(newFolderList);
740             mItemList = newFolderList;
741             // Ask the list to invalidate its views.
742             notifyDataSetChanged();
743         }
744 
745         /**
746          * Recalculates the accounts if not null and adds them to the list.
747          *
748          * @param itemList List of drawer items to populate
749          */
recalculateListAccounts(List<DrawerItem> itemList)750         private void recalculateListAccounts(List<DrawerItem> itemList) {
751             final Account[] allAccounts = getAllAccounts();
752             // Add all accounts and then the current account
753             final Uri currentAccountUri = getCurrentAccountUri();
754             for (final Account account : allAccounts) {
755                 final int unreadCount = mFolderWatcher.getUnreadCount(account);
756                 itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
757                         currentAccountUri.equals(account.uri), mBidiFormatter));
758             }
759             if (mCurrentAccount == null) {
760                 LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account.");
761             }
762         }
763 
764         /**
765          * Recalculates the system, recent and user label lists.
766          * This method modifies all the three lists on every single invocation.
767          *
768          * @param itemList List of drawer items to populate
769          */
recalculateListFolders(List<DrawerItem> itemList)770         private void recalculateListFolders(List<DrawerItem> itemList) {
771             // If we are waiting for folder initialization, we don't have any kinds of folders,
772             // just the "Waiting for initialization" item. Note, this should only be done
773             // when we're waiting for account initialization or initial sync.
774             if (isCursorInvalid()) {
775                 if(!mCurrentAccount.isAccountReady()) {
776                     itemList.add(DrawerItem.ofWaitView(mActivity, mBidiFormatter));
777                 }
778                 return;
779             }
780 
781             if (!mIsDivided) {
782                 // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
783                 do {
784                     final Folder f = mCursor.getModel();
785                     if (!isFolderTypeExcluded(f)) {
786                         itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER,
787                                 mBidiFormatter));
788                     }
789                 } while (mCursor.moveToNext());
790 
791                 return;
792             }
793 
794             // Otherwise, this is an adapter for a divided list.
795             final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
796             final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
797             do {
798                 final Folder f = mCursor.getModel();
799                 if (!isFolderTypeExcluded(f)) {
800                     if (f.isInbox()) {
801                         inboxFolders.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_INBOX,
802                                 mBidiFormatter));
803                     } else {
804                         allFoldersList.add(DrawerItem.ofFolder(mActivity, f,
805                                 DrawerItem.FOLDER_OTHER, mBidiFormatter));
806                     }
807                 }
808             } while (mCursor.moveToNext());
809 
810             // If we have the all folder list, verify that the current folder exists
811             boolean currentFolderFound = false;
812             if (mAllFolderListCursor != null) {
813                 final String folderName = mSelectedFolderUri.toString();
814                 LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);
815 
816                 if (mAllFolderListCursor.moveToFirst()) {
817                     LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
818                     do {
819                         final Folder f = mAllFolderListCursor.getModel();
820                         if (!isFolderTypeExcluded(f)) {
821                             if (f.folderUri.equals(mSelectedFolderUri)) {
822                                 LogUtils.d(LOG_TAG, "Found %s !", folderName);
823                                 currentFolderFound = true;
824                             }
825                         }
826                     } while (!currentFolderFound && mAllFolderListCursor.moveToNext());
827                 }
828 
829                 if (!currentFolderFound && mSelectedFolderUri != FolderUri.EMPTY
830                         && mCurrentAccount != null && mAccountController != null
831                         && mAccountController.isDrawerPullEnabled()) {
832                     LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
833                             folderName, mCurrentAccount.name);
834                     changeAccount(mCurrentAccount);
835                 }
836             }
837 
838             // Add all inboxes (sectioned Inboxes included) before recent folders.
839             addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
840 
841             // Add recent folders next.
842             addRecentsToList(itemList);
843 
844             // Add the remaining folders.
845             addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
846         }
847 
848         /**
849          * Given a list of folders as {@link DrawerItem}s, add them as a group.
850          * Passing in a non-0 integer for the resource will enable a header.
851          *
852          * @param destination List of drawer items to populate
853          * @param source List of drawer items representing folders to add to the drawer
854          * @param headerStringResource
855          *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
856          *            is required, or res-id otherwise. The integer is interpreted as the string
857          *            for the header's title.
858          */
addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source, int headerStringResource)859         private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
860                 int headerStringResource) {
861             if (source.size() > 0) {
862                 if(headerStringResource != NO_HEADER_RESOURCE) {
863                     destination.add(DrawerItem.ofHeader(mActivity, headerStringResource,
864                             mBidiFormatter));
865                 }
866                 destination.addAll(source);
867             }
868         }
869 
870         /**
871          * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
872          *
873          * @param destination List of drawer items to populate
874          */
addRecentsToList(List<DrawerItem> destination)875         private void addRecentsToList(List<DrawerItem> destination) {
876             // If there are recent folders, add them.
877             final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
878 
879             // Remove any excluded folder types
880             if (mExcludedFolderTypes != null) {
881                 final Iterator<Folder> iterator = recentFolderList.iterator();
882                 while (iterator.hasNext()) {
883                     if (isFolderTypeExcluded(iterator.next())) {
884                         iterator.remove();
885                     }
886                 }
887             }
888 
889             if (recentFolderList.size() > 0) {
890                 destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading,
891                         mBidiFormatter));
892                 // Recent folders are not queried for position.
893                 for (Folder f : recentFolderList) {
894                     destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
895                             mBidiFormatter));
896                 }
897             }
898         }
899 
900         /**
901          * Check if the cursor provided is valid.
902          * @return True if cursor is invalid, false otherwise
903          */
isCursorInvalid()904         private boolean isCursorInvalid() {
905             return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
906                     || !mCursor.moveToFirst();
907         }
908 
909         @Override
setCursor(ObjectCursor<Folder> cursor)910         public void setCursor(ObjectCursor<Folder> cursor) {
911             mCursor = cursor;
912             recalculateList();
913         }
914 
915         @Override
setAllFolderListCursor(final ObjectCursor<Folder> cursor)916         public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
917             mAllFolderListCursor = cursor;
918             recalculateList();
919         }
920 
921         @Override
getItem(int position)922         public Object getItem(int position) {
923             // Is there an attempt made to access outside of the drawer item list?
924             if (position >= mItemList.size()) {
925                 return null;
926             } else {
927                 return mItemList.get(position);
928             }
929         }
930 
931         @Override
getItemId(int position)932         public long getItemId(int position) {
933             return getItem(position).hashCode();
934         }
935 
936         @Override
destroy()937         public final void destroy() {
938             mRecentFolderObserver.unregisterAndDestroy();
939         }
940 
941         @Override
getDefaultInbox(Account account)942         public Folder getDefaultInbox(Account account) {
943             if (mFolderWatcher != null) {
944                 return mFolderWatcher.getDefaultInbox(account);
945             }
946             return null;
947         }
948 
949         @Override
getItemType(DrawerItem item)950         public int getItemType(DrawerItem item) {
951             return item.mType;
952         }
953 
954         @Override
getSelectedPosition()955         public int getSelectedPosition() {
956             for (int i = 0; i < mItemList.size(); i++) {
957                 final DrawerItem item = (DrawerItem) getItem(i);
958                 final boolean isSelected =
959                         item.isHighlighted(mSelectedFolderUri, mSelectedFolderType);
960                 if (isSelected) {
961                     return i;
962                 }
963             }
964 
965             return -1;
966         }
967     }
968 
969     private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
970             implements FolderListFragmentCursorAdapter {
971 
972         private static final int PARENT = 0;
973         private static final int CHILD = 1;
974         private final FolderUri mParentUri;
975         private final Folder mParent;
976         private final FolderItemView.DropHandler mDropHandler;
977 
HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder)978         public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
979             super(mActivity.getActivityContext(), R.layout.folder_item);
980             mDropHandler = mActivity;
981             mParent = parentFolder;
982             mParentUri = parentFolder.folderUri;
983             setCursor(c);
984         }
985 
986         @Override
getViewTypeCount()987         public int getViewTypeCount() {
988             // Child and Parent
989             return 2;
990         }
991 
992         @Override
getItemViewType(int position)993         public int getItemViewType(int position) {
994             final Folder f = getItem(position);
995             return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
996         }
997 
998         @Override
getView(int position, View convertView, ViewGroup parent)999         public View getView(int position, View convertView, ViewGroup parent) {
1000             final FolderItemView folderItemView;
1001             final Folder folder = getItem(position);
1002             boolean isParent = folder.folderUri.equals(mParentUri);
1003             if (convertView != null) {
1004                 folderItemView = (FolderItemView) convertView;
1005             } else {
1006                 int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
1007                 folderItemView = (FolderItemView) LayoutInflater.from(
1008                         mActivity.getActivityContext()).inflate(resId, null);
1009             }
1010             folderItemView.bind(folder, mDropHandler, mBidiFormatter);
1011             if (folder.folderUri.equals(mSelectedFolderUri)) {
1012                 getListView().setItemChecked(position, true);
1013                 // If this is the current folder, also check to verify that the unread count
1014                 // matches what the action bar shows.
1015                 final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1016                         && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1017                 if (unreadCountDiffers) {
1018                     folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1019                 }
1020             }
1021             Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1022             Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1023             return folderItemView;
1024         }
1025 
1026         @Override
setCursor(ObjectCursor<Folder> cursor)1027         public void setCursor(ObjectCursor<Folder> cursor) {
1028             clear();
1029             if (mParent != null) {
1030                 add(mParent);
1031             }
1032             if (cursor != null && cursor.getCount() > 0) {
1033                 cursor.moveToFirst();
1034                 do {
1035                     add(cursor.getModel());
1036                 } while (cursor.moveToNext());
1037             }
1038         }
1039 
1040         @Override
setAllFolderListCursor(final ObjectCursor<Folder> cursor)1041         public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
1042             // Not necessary in HierarchicalFolderListAdapter
1043         }
1044 
1045         @Override
destroy()1046         public void destroy() {
1047             // Do nothing.
1048         }
1049 
1050         @Override
getDefaultInbox(Account account)1051         public Folder getDefaultInbox(Account account) {
1052             return null;
1053         }
1054 
1055         @Override
getItemType(DrawerItem item)1056         public int getItemType(DrawerItem item) {
1057             // Always returns folders for now.
1058             return DrawerItem.VIEW_FOLDER;
1059         }
1060 
1061         @Override
notifyAllAccountsChanged()1062         public void notifyAllAccountsChanged() {
1063             // Do nothing. We don't care about changes to all accounts.
1064         }
1065 
1066         @Override
getSelectedPosition()1067         public int getSelectedPosition() {
1068             final int count = getCount();
1069             for (int i = 0; i < count; i++) {
1070                 final Folder folder = getItem(i);
1071                 final boolean isSelected = folder.folderUri.equals(mSelectedFolderUri);
1072                 if (isSelected) {
1073                     return i;
1074                 }
1075             }
1076             return -1;
1077         }
1078     }
1079 
1080     /**
1081      * Sets the currently selected folder safely.
1082      * @param folder the folder to change to. It is an error to pass null here.
1083      */
setSelectedFolder(Folder folder)1084     private void setSelectedFolder(Folder folder) {
1085         if (folder == null) {
1086             mSelectedFolderUri = FolderUri.EMPTY;
1087             mCurrentFolderForUnreadCheck = null;
1088             LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1089             return;
1090         }
1091 
1092         final boolean viewChanged =
1093                 !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1094 
1095         // There are two cases in which the folder type is not set by this class.
1096         // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
1097         //    folder but its type was never set.
1098         // 2. The user backs into the default inbox. Going 'back' from the conversation list of
1099         //    any folder will take you to the default inbox for that account. (If you are in the
1100         //    default inbox already, back exits the app.)
1101         // In both these cases, the selected folder type is not set, and must be set.
1102         if (mSelectedFolderType == DrawerItem.UNSET || (mCurrentAccount != null
1103                 && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
1104             mSelectedFolderType =
1105                     folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
1106         }
1107 
1108         mCurrentFolderForUnreadCheck = folder;
1109         mSelectedFolderUri = folder.folderUri;
1110         if (mCursorAdapter != null && viewChanged) {
1111             mCursorAdapter.notifyDataSetChanged();
1112         }
1113     }
1114 
updateScroll()1115     public void updateScroll() {
1116         final int selectedPosition = mCursorAdapter.getSelectedPosition();
1117         if (selectedPosition >= 0) {
1118             // TODO: setSelection() jumps the item to the top of the list "hiding" the accounts
1119             // TODO: and smoothScrollToPosition() is too slow for lots of labels/folders
1120             // It's called "setSelection" but it's really more like "jumpScrollToPosition"
1121             // mListView.setSelection(selectedPosition);
1122         }
1123     }
1124 
1125     /**
1126      * Sets the current account to the one provided here.
1127      * @param account the current account to set to.
1128      */
setSelectedAccount(Account account)1129     private void setSelectedAccount(Account account){
1130         final boolean changed = (account != null) && (mCurrentAccount == null
1131                 || !mCurrentAccount.uri.equals(account.uri));
1132         mCurrentAccount = account;
1133         if (changed) {
1134             // We no longer have proper folder objects. Let the new ones come in
1135             mCursorAdapter.setCursor(null);
1136             // If currentAccount is different from the one we set, restart the loader. Look at the
1137             // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1138             // don't just do restartLoader.
1139             final LoaderManager manager = getLoaderManager();
1140             manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1141             manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1142             manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1143             manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
1144             // An updated cursor causes the entire list to refresh. No need to refresh the list.
1145             // But we do need to blank out the current folder, since the account might not be
1146             // synced.
1147             mSelectedFolderUri = FolderUri.EMPTY;
1148             mCurrentFolderForUnreadCheck = null;
1149         } else if (account == null) {
1150             // This should never happen currently, but is a safeguard against a very incorrect
1151             // non-null account -> null account transition.
1152             LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1153             final LoaderManager manager = getLoaderManager();
1154             manager.destroyLoader(FOLDER_LIST_LOADER_ID);
1155             manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
1156         }
1157     }
1158 
1159     /**
1160      * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1161      */
isFolderTypeExcluded(final Folder folder)1162     private boolean isFolderTypeExcluded(final Folder folder) {
1163         if (mExcludedFolderTypes == null) {
1164             return false;
1165         }
1166 
1167         for (final int excludedType : mExcludedFolderTypes) {
1168             if (folder.isType(excludedType)) {
1169                 return true;
1170             }
1171         }
1172 
1173         return false;
1174     }
1175 
1176     /**
1177      * @return the choice mode to use for the {@link ListView}
1178      */
getListViewChoiceMode()1179     protected int getListViewChoiceMode() {
1180         return mAccountController.getFolderListViewChoiceMode();
1181     }
1182 }
1183