blob: 766660f326fe58f052f9f7c29e0a7e1a5e819671 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.ui;
import android.app.Activity;
import android.app.ListFragment;
import android.app.LoaderManager;
import android.content.Loader;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.text.BidiFormatter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.android.mail.R;
import com.android.mail.adapter.DrawerItem;
import com.android.mail.analytics.Analytics;
import com.android.mail.browse.MergedAdapter;
import com.android.mail.content.ObjectCursor;
import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.AllAccountObserver;
import com.android.mail.providers.DrawerClosedObserver;
import com.android.mail.providers.Folder;
import com.android.mail.providers.FolderObserver;
import com.android.mail.providers.FolderWatcher;
import com.android.mail.providers.RecentFolderObserver;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.utils.FolderUri;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* This fragment shows the list of folders and the list of accounts. Prior to June 2013,
* the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
* in a drawer along with the list of folders.
*
* This class has the following use-cases:
* <ul>
* <li>
* Show a list of accounts and a divided list of folders. In this case, the list shows
* Accounts, Inboxes, Recent Folders, All folders.
* Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
* folders switches folders.
* This is created through XML resources as a {@link DrawerFragment}. Since it is created
* through resources, it receives all arguments through callbacks.
* </li>
* <li>
* Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
* Drafts, Starred, and any user-created folders. For providers that allow nested folders,
* this will only show the folders at the top-level.
* <br /> Tapping on a parent folder creates a new fragment with the child folders at
* that level.
* </li>
* <li>
* Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
* {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
* any folder for a given account.
* </li>
* </ul>
*/
public class FolderListFragment extends ListFragment implements
LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
private static final String LOG_TAG = LogTag.getLogTag();
/** The parent activity */
private ControllableActivity mActivity;
private BidiFormatter mBidiFormatter;
/** The underlying list view */
private ListView mListView;
/** URI that points to the list of folders for the current account. */
private Uri mFolderListUri;
/**
* True if you want a divided FolderList. A divided folder list shows the following groups:
* Inboxes, Recent Folders, All folders.
*
* An undivided FolderList shows all folders without any divisions and without recent folders.
* This is true only for the drawer: for all others it is false.
*/
protected boolean mIsDivided = false;
/** True if the folder list belongs to a folder selection activity (one account only) */
protected boolean mHideAccounts = true;
/** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
private ArrayList<Integer> mExcludedFolderTypes;
/** Object that changes folders on our behalf. */
private FolderSelector mFolderChanger;
/** Object that changes accounts on our behalf */
private AccountController mAccountController;
/** The currently selected folder (the folder being viewed). This is never null. */
private FolderUri mSelectedFolderUri = FolderUri.EMPTY;
/**
* The current folder from the controller. This is meant only to check when the unread count
* goes out of sync and fixing it.
*/
private Folder mCurrentFolderForUnreadCheck;
/** Parent of the current folder, or null if the current folder is not a child. */
private Folder mParentFolder;
private static final int FOLDER_LIST_LOADER_ID = 0;
/** Loader id for the list of all folders in the account */
private static final int ALL_FOLDER_LIST_LOADER_ID = 1;
/** Key to store {@link #mParentFolder}. */
private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
/** Key to store {@link #mFolderListUri}. */
private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
/** Key to store {@link #mExcludedFolderTypes} */
private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
private static final String BUNDLE_LIST_STATE = "flf-list-state";
private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
/** Adapter used by the list that wraps both the folder adapter and the accounts adapter. */
private MergedAdapter<ListAdapter> mMergedAdapter;
/** Adapter containing the list of accounts. */
private AccountsAdapter mAccountsAdapter;
/** Adapter containing the list of folders and, optionally, headers and the wait view. */
private FolderListFragmentCursorAdapter mFolderAdapter;
/** Observer to wait for changes to the current folder so we can change the selected folder */
private FolderObserver mFolderObserver = null;
/** Listen for account changes. */
private AccountObserver mAccountObserver = null;
/** Listen for account changes. */
private DrawerClosedObserver mDrawerObserver = null;
/** Listen to changes to list of all accounts */
private AllAccountObserver mAllAccountsObserver = null;
/**
* Type of currently selected folder: {@link DrawerItem#FOLDER_INBOX},
* {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_OTHER}.
* Set as {@link DrawerItem#UNSET} to begin with, as there is nothing selected yet.
*/
private int mSelectedFolderType = DrawerItem.UNSET;
/** The current account according to the controller */
private Account mCurrentAccount;
/** The account we will change to once the drawer (if any) is closed */
private Account mNextAccount = null;
/** The folder we will change to once the drawer (if any) is closed */
private Folder mNextFolder = null;
/** Watcher for tracking and receiving unread counts for mail */
private FolderWatcher mFolderWatcher = null;
private boolean mRegistered = false;
/**
* Constructor needs to be public to handle orientation changes and activity lifecycle events.
*/
public FolderListFragment() {
super();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(super.toString());
sb.setLength(sb.length() - 1);
sb.append(" folder=");
sb.append(mFolderListUri);
sb.append(" parent=");
sb.append(mParentFolder);
sb.append(" adapterCount=");
sb.append(mMergedAdapter != null ? mMergedAdapter.getCount() : -1);
sb.append("}");
return sb.toString();
}
/**
* Creates a new instance of {@link FolderListFragment}, initialized
* to display the folder and its immediate children.
* @param folder parent folder whose children are shown
*
*/
public static FolderListFragment ofTree(Folder folder) {
final FolderListFragment fragment = new FolderListFragment();
fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
return fragment;
}
/**
* Creates a new instance of {@link FolderListFragment}, initialized
* to display the top level: where we have no parent folder, but we have a list of folders
* from the account.
* @param folderListUri the URI which contains all the list of folders
* @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
*/
public static FolderListFragment ofTopLevelTree(Uri folderListUri,
final ArrayList<Integer> excludedFolderTypes) {
final FolderListFragment fragment = new FolderListFragment();
fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
return fragment;
}
/**
* Construct a bundle that represents the state of this fragment.
*
* @param parentFolder non-null for trees, the parent of this list
* @param folderListUri the URI which contains all the list of folders
* @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
* @return Bundle containing parentFolder, divided list boolean and
* excluded folder types
*/
private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
final ArrayList<Integer> excludedFolderTypes) {
final Bundle args = new Bundle(3);
if (parentFolder != null) {
args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
}
if (folderListUri != null) {
args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
}
if (excludedFolderTypes != null) {
args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
}
return args;
}
@Override
public void onActivityCreated(Bundle savedState) {
super.onActivityCreated(savedState);
// Strictly speaking, we get back an android.app.Activity from getActivity. However, the
// only activity creating a ConversationListContext is a MailActivity which is of type
// ControllableActivity, so this cast should be safe. If this cast fails, some other
// activity is creating ConversationListFragments. This activity must be of type
// ControllableActivity.
final Activity activity = getActivity();
if (! (activity instanceof ControllableActivity)){
LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
"create it. Cannot proceed.");
return;
}
mActivity = (ControllableActivity) activity;
mBidiFormatter = BidiFormatter.getInstance();
final FolderController controller = mActivity.getFolderController();
// Listen to folder changes in the future
mFolderObserver = new FolderObserver() {
@Override
public void onChanged(Folder newFolder) {
setSelectedFolder(newFolder);
}
};
final Folder currentFolder;
if (controller != null) {
// Only register for selected folder updates if we have a controller.
currentFolder = mFolderObserver.initialize(controller);
mCurrentFolderForUnreadCheck = currentFolder;
} else {
currentFolder = null;
}
// Initialize adapter for folder/heirarchical list. Note this relies on
// mActivity being initialized.
final Folder selectedFolder;
if (mParentFolder != null) {
mFolderAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
selectedFolder = mActivity.getHierarchyFolder();
} else {
mFolderAdapter = new FolderAdapter(mIsDivided);
selectedFolder = currentFolder;
}
mAccountsAdapter = new AccountsAdapter();
// Is the selected folder fresher than the one we have restored from a bundle?
if (selectedFolder != null
&& !selectedFolder.folderUri.equals(mSelectedFolderUri)) {
setSelectedFolder(selectedFolder);
}
// Assign observers for current account & all accounts
final AccountController accountController = mActivity.getAccountController();
mAccountObserver = new AccountObserver() {
@Override
public void onChanged(Account newAccount) {
setSelectedAccount(newAccount);
}
};
mFolderChanger = mActivity.getFolderSelector();
if (accountController != null) {
// Current account and its observer.
setSelectedAccount(mAccountObserver.initialize(accountController));
// List of all accounts and its observer.
mAllAccountsObserver = new AllAccountObserver(){
@Override
public void onChanged(Account[] allAccounts) {
if (!mRegistered && mAccountController != null) {
// TODO(viki): Round-about way of setting the watcher. http://b/8750610
mAccountController.setFolderWatcher(mFolderWatcher);
mRegistered = true;
}
mFolderWatcher.updateAccountList(getAllAccounts());
mAccountsAdapter.rebuildAccountList();
}
};
mAllAccountsObserver.initialize(accountController);
mAccountController = accountController;
// Observer for when the drawer is closed
mDrawerObserver = new DrawerClosedObserver() {
@Override
public void onDrawerClosed() {
// First, check if there's a folder to change to
if (mNextFolder != null) {
mFolderChanger.onFolderSelected(mNextFolder);
mNextFolder = null;
}
// Next, check if there's an account to change to
if (mNextAccount != null) {
mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
mNextAccount = null;
}
}
};
mDrawerObserver.initialize(accountController);
}
if (mActivity.isFinishing()) {
// Activity is finishing, just bail.
return;
}
mListView.setChoiceMode(getListViewChoiceMode());
mMergedAdapter = new MergedAdapter<ListAdapter>();
mMergedAdapter.setAdapters(mAccountsAdapter, mFolderAdapter);
mFolderWatcher = new FolderWatcher(mActivity, mAccountsAdapter);
mFolderWatcher.updateAccountList(getAllAccounts());
setListAdapter(mMergedAdapter);
}
/**
* Set the instance variables from the arguments provided here.
* @param args bundle of arguments with keys named ARG_*
*/
private void setInstanceFromBundle(Bundle args) {
if (args == null) {
return;
}
mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
if (folderUri != null) {
mFolderListUri = Uri.parse(folderUri);
}
mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedState) {
setInstanceFromBundle(getArguments());
final View rootView = inflater.inflate(R.layout.folder_list, null);
mListView = (ListView) rootView.findViewById(android.R.id.list);
mListView.setEmptyView(null);
mListView.setDivider(null);
if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
}
if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
mSelectedFolderUri =
new FolderUri(Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)));
mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
} else if (mParentFolder != null) {
mSelectedFolderUri = mParentFolder.folderUri;
// No selected folder type required for hierarchical lists.
}
return rootView;
}
@Override
public void onStart() {
super.onStart();
}
@Override
public void onStop() {
super.onStop();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mListView != null) {
outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
}
if (mSelectedFolderUri != null) {
outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
}
outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
}
@Override
public void onDestroyView() {
if (mFolderAdapter != null) {
mFolderAdapter.destroy();
}
// Clear the adapter.
setListAdapter(null);
if (mFolderObserver != null) {
mFolderObserver.unregisterAndDestroy();
mFolderObserver = null;
}
if (mAccountObserver != null) {
mAccountObserver.unregisterAndDestroy();
mAccountObserver = null;
}
if (mAllAccountsObserver != null) {
mAllAccountsObserver.unregisterAndDestroy();
mAllAccountsObserver = null;
}
if (mDrawerObserver != null) {
mDrawerObserver.unregisterAndDestroy();
mDrawerObserver = null;
}
super.onDestroyView();
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
viewFolderOrChangeAccount(position);
}
private Folder getDefaultInbox(Account account) {
if (account == null || mFolderWatcher == null) {
return null;
}
return mFolderWatcher.getDefaultInbox(account);
}
private void changeAccount(final Account account) {
// Switching accounts takes you to the default inbox for that account.
mSelectedFolderType = DrawerItem.FOLDER_INBOX;
mNextAccount = account;
mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
Analytics.getInstance().sendEvent("switch_account", "drawer_account_switch", null, 0);
}
/**
* Display the conversation list from the folder at the position given.
* @param position a zero indexed position into the list.
*/
private void viewFolderOrChangeAccount(int position) {
final Object item = getListAdapter().getItem(position);
LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
final Folder folder;
int folderType = DrawerItem.UNSET;
if (item instanceof DrawerItem) {
final DrawerItem drawerItem = (DrawerItem) item;
// Could be a folder or account.
final int itemType = drawerItem.mType;
if (itemType == DrawerItem.VIEW_ACCOUNT) {
// Account, so switch.
folder = null;
final Account account = drawerItem.mAccount;
if (account != null && mSelectedFolderUri.equals(account.settings.defaultInbox)) {
// We're already in the default inbox for account,
// just close the drawer (no new target folders/accounts)
mAccountController.closeDrawer(false, mNextAccount,
getDefaultInbox(mNextAccount));
} else {
changeAccount(account);
}
} else if (itemType == DrawerItem.VIEW_FOLDER) {
// Folder type, so change folders only.
folder = drawerItem.mFolder;
mSelectedFolderType = folderType = drawerItem.mFolderType;
LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
folder, mSelectedFolderType);
} else {
// Do nothing.
LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
+ " Clicked on unset item in drawer. Offending item is " + item);
return;
}
} else if (item instanceof Folder) {
folder = (Folder) item;
} else {
// Don't know how we got here.
LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
folder = null;
}
if (folder != null) {
// Not changing the account.
final Account nextAccount = null;
// Go to the conversation list for this folder.
if (!folder.folderUri.equals(mSelectedFolderUri)) {
mNextFolder = folder;
mAccountController.closeDrawer(true, nextAccount, folder);
final String label = (folderType == DrawerItem.FOLDER_RECENT) ? "recent" : "normal";
Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(),
label, 0);
} else {
// Clicked on same folder, just close drawer
mAccountController.closeDrawer(false, nextAccount, folder);
}
}
}
@Override
public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
mListView.setEmptyView(null);
final Uri folderListUri;
if (id == FOLDER_LIST_LOADER_ID) {
if (mFolderListUri != null) {
// Folder trees, they specify a URI at construction time.
folderListUri = mFolderListUri;
} else {
// Drawers get the folder list from the current account.
folderListUri = mCurrentAccount.folderListUri;
}
} else if (id == ALL_FOLDER_LIST_LOADER_ID) {
folderListUri = mCurrentAccount.allFolderListUri;
} else {
LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
return null;
}
return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
}
@Override
public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
if (mFolderAdapter != null) {
if (loader.getId() == FOLDER_LIST_LOADER_ID) {
mFolderAdapter.setCursor(data);
} else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
mFolderAdapter.setAllFolderListCursor(data);
}
}
}
@Override
public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
if (mFolderAdapter != null) {
if (loader.getId() == FOLDER_LIST_LOADER_ID) {
mFolderAdapter.setCursor(null);
} else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) {
mFolderAdapter.setAllFolderListCursor(null);
}
}
}
/**
* Returns the sorted list of accounts. The AAC always has the current list, sorted by
* frequency of use.
* @return a list of accounts, sorted by frequency of use
*/
private Account[] getAllAccounts() {
if (mAllAccountsObserver != null) {
return mAllAccountsObserver.getAllAccounts();
}
return new Account[0];
}
/**
* Interface for all cursor adapters that allow setting a cursor and being destroyed.
*/
private interface FolderListFragmentCursorAdapter extends ListAdapter {
/** Update the folder list cursor with the cursor given here. */
void setCursor(ObjectCursor<Folder> cursor);
/** Update the all folder list cursor with the cursor given here. */
void setAllFolderListCursor(ObjectCursor<Folder> cursor);
/** Remove all observers and destroy the object. */
void destroy();
/** Notifies the adapter that the data has changed. */
void notifyDataSetChanged();
}
/**
* An adapter for flat folder lists.
*/
private class FolderAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
@Override
public void onChanged() {
if (!isCursorInvalid()) {
rebuildFolderList();
}
}
};
/** No resource used for string header in folder list */
private static final int NO_HEADER_RESOURCE = -1;
/** Cache of most recently used folders */
private final RecentFolderList mRecentFolders;
/** True if the list is divided, false otherwise. See the comment on
* {@link FolderListFragment#mIsDivided} for more information */
private final boolean mIsDivided;
/** All the items */
private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
/** Cursor into the folder list. This might be null. */
private ObjectCursor<Folder> mCursor = null;
/** Cursor into the all folder list. This might be null. */
private ObjectCursor<Folder> mAllFolderListCursor = null;
/**
* Creates a {@link FolderAdapter}. This is a list of all the accounts and folders.
*
* @param isDivided true if folder list is flat, false if divided by label group. See
* the comments on {@link #mIsDivided} for more information
*/
public FolderAdapter(boolean isDivided) {
super();
mIsDivided = isDivided;
final RecentFolderController controller = mActivity.getRecentFolderController();
if (controller != null && mIsDivided) {
mRecentFolders = mRecentFolderObserver.initialize(controller);
} else {
mRecentFolders = null;
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final DrawerItem item = (DrawerItem) getItem(position);
final View view = item.getView(convertView, parent);
final int type = item.mType;
final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedFolderType);
if (type == DrawerItem.VIEW_FOLDER) {
mListView.setItemChecked(mAccountsAdapter.getCount() + position, isSelected);
}
// If this is the current folder, also check to verify that the unread count
// matches what the action bar shows.
if (type == DrawerItem.VIEW_FOLDER
&& isSelected
&& (mCurrentFolderForUnreadCheck != null)
&& item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
((FolderItemView) view).overrideUnreadCount(
mCurrentFolderForUnreadCheck.unreadCount);
}
return view;
}
@Override
public int getViewTypeCount() {
// Accounts, headers, folders (all parts of drawer view types)
return DrawerItem.getViewTypes();
}
@Override
public int getItemViewType(int position) {
return ((DrawerItem) getItem(position)).mType;
}
@Override
public int getCount() {
return mItemList.size();
}
@Override
public boolean isEnabled(int position) {
final DrawerItem drawerItem = ((DrawerItem) getItem(position));
return drawerItem != null && drawerItem.isItemEnabled();
}
@Override
public boolean areAllItemsEnabled() {
// We have headers and thus some items are not enabled.
return false;
}
/**
* Returns all the recent folders from the list given here. Safe to call with a null list.
* @param recentList a list of all recently accessed folders.
* @return a valid list of folders, which are all recent folders.
*/
private List<Folder> getRecentFolders(RecentFolderList recentList) {
final List<Folder> folderList = new ArrayList<Folder>();
if (recentList == null) {
return folderList;
}
// Get all recent folders, after removing system folders.
for (final Folder f : recentList.getRecentFolderList(null)) {
if (!f.isProviderFolder()) {
folderList.add(f);
}
}
return folderList;
}
/**
* Responsible for verifying mCursor, and ensuring any recalculate
* conditions are met. Also calls notifyDataSetChanged once it's finished
* populating {@link com.android.mail.ui.FolderListFragment.FolderAdapter#mItemList}
*/
private void rebuildFolderList() {
mItemList = recalculateListFolders();
// Ask the list to invalidate its views.
notifyDataSetChanged();
}
/**
* Recalculates the system, recent and user label lists.
* This method modifies all the three lists on every single invocation.
*/
private List<DrawerItem> recalculateListFolders() {
final List<DrawerItem> itemList = new ArrayList<DrawerItem>();
// If we are waiting for folder initialization, we don't have any kinds of folders,
// just the "Waiting for initialization" item. Note, this should only be done
// when we're waiting for account initialization or initial sync.
if (isCursorInvalid()) {
if(!mCurrentAccount.isAccountReady()) {
itemList.add(DrawerItem.ofWaitView(mActivity, mBidiFormatter));
}
return itemList;
}
if (!mIsDivided) {
// Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
do {
final Folder f = mCursor.getModel();
if (!isFolderTypeExcluded(f)) {
itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER,
mBidiFormatter));
}
} while (mCursor.moveToNext());
return itemList;
}
// Otherwise, this is an adapter for a divided list.
final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
do {
final Folder f = mCursor.getModel();
if (!isFolderTypeExcluded(f)) {
if (f.isInbox()) {
inboxFolders.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_INBOX,
mBidiFormatter));
} else {
allFoldersList.add(DrawerItem.ofFolder(mActivity, f,
DrawerItem.FOLDER_OTHER, mBidiFormatter));
}
}
} while (mCursor.moveToNext());
// If we have the all folder list, verify that the current folder exists
boolean currentFolderFound = false;
if (mAllFolderListCursor != null) {
final String folderName = mSelectedFolderUri.toString();
LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName);
if (mAllFolderListCursor.moveToFirst()) {
LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
do {
final Folder f = mAllFolderListCursor.getModel();
if (!isFolderTypeExcluded(f)) {
if (f.folderUri.equals(mSelectedFolderUri)) {
LogUtils.d(LOG_TAG, "Found %s !", folderName);
currentFolderFound = true;
}
}
} while (!currentFolderFound && mAllFolderListCursor.moveToNext());
}
if (!currentFolderFound && mSelectedFolderUri != FolderUri.EMPTY
&& mCurrentAccount != null && mAccountController != null
&& mAccountController.isDrawerPullEnabled()) {
LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
folderName, mCurrentAccount.name);
changeAccount(mCurrentAccount);
}
}
// Add all inboxes (sectioned Inboxes included) before recent folders.
addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
// Add recent folders next.
addRecentsToList(itemList);
// Add the remaining folders.
addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
return itemList;
}
/**
* Given a list of folders as {@link DrawerItem}s, add them as a group.
* Passing in a non-0 integer for the resource will enable a header.
*
* @param destination List of drawer items to populate
* @param source List of drawer items representing folders to add to the drawer
* @param headerStringResource
* {@link FolderAdapter#NO_HEADER_RESOURCE} if no header
* is required, or res-id otherwise. The integer is interpreted as the string
* for the header's title.
*/
private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
int headerStringResource) {
if (source.size() > 0) {
if(headerStringResource != NO_HEADER_RESOURCE) {
destination.add(DrawerItem.ofHeader(mActivity, headerStringResource,
mBidiFormatter));
}
destination.addAll(source);
}
}
/**
* Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
*
* @param destination List of drawer items to populate
*/
private void addRecentsToList(List<DrawerItem> destination) {
// If there are recent folders, add them.
final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
// Remove any excluded folder types
if (mExcludedFolderTypes != null) {
final Iterator<Folder> iterator = recentFolderList.iterator();
while (iterator.hasNext()) {
if (isFolderTypeExcluded(iterator.next())) {
iterator.remove();
}
}
}
if (recentFolderList.size() > 0) {
destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading,
mBidiFormatter));
// Recent folders are not queried for position.
for (Folder f : recentFolderList) {
destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
mBidiFormatter));
}
}
}
/**
* Check if the cursor provided is valid.
* @return True if cursor is invalid, false otherwise
*/
private boolean isCursorInvalid() {
return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
|| !mCursor.moveToFirst();
}
@Override
public void setCursor(ObjectCursor<Folder> cursor) {
mCursor = cursor;
mAccountsAdapter.rebuildAccountList();
rebuildFolderList();
}
@Override
public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
mAllFolderListCursor = cursor;
mAccountsAdapter.rebuildAccountList();
rebuildFolderList();
}
@Override
public Object getItem(int position) {
// Is there an attempt made to access outside of the drawer item list?
if (position >= mItemList.size()) {
return null;
} else {
return mItemList.get(position);
}
}
@Override
public long getItemId(int position) {
return getItem(position).hashCode();
}
@Override
public final void destroy() {
mRecentFolderObserver.unregisterAndDestroy();
}
}
private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
implements FolderListFragmentCursorAdapter {
private static final int PARENT = 0;
private static final int CHILD = 1;
private final FolderUri mParentUri;
private final Folder mParent;
private final FolderItemView.DropHandler mDropHandler;
public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
super(mActivity.getActivityContext(), R.layout.folder_item);
mDropHandler = mActivity;
mParent = parentFolder;
mParentUri = parentFolder.folderUri;
setCursor(c);
}
@Override
public int getViewTypeCount() {
// Child and Parent
return 2;
}
@Override
public int getItemViewType(int position) {
final Folder f = getItem(position);
return f.folderUri.equals(mParentUri) ? PARENT : CHILD;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final FolderItemView folderItemView;
final Folder folder = getItem(position);
boolean isParent = folder.folderUri.equals(mParentUri);
if (convertView != null) {
folderItemView = (FolderItemView) convertView;
} else {
int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
folderItemView = (FolderItemView) LayoutInflater.from(
mActivity.getActivityContext()).inflate(resId, null);
}
folderItemView.bind(folder, mDropHandler, mBidiFormatter);
if (folder.folderUri.equals(mSelectedFolderUri)) {
getListView().setItemChecked(mAccountsAdapter.getCount() + position, true);
// If this is the current folder, also check to verify that the unread count
// matches what the action bar shows.
final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
&& folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
if (unreadCountDiffers) {
folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
}
}
Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
return folderItemView;
}
@Override
public void setCursor(ObjectCursor<Folder> cursor) {
clear();
if (mParent != null) {
add(mParent);
}
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
do {
add(cursor.getModel());
} while (cursor.moveToNext());
}
}
@Override
public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) {
// Not necessary in HierarchicalFolderListAdapter
}
@Override
public void destroy() {
// Do nothing.
}
}
private class AccountsAdapter extends BaseAdapter {
private List<DrawerItem> mAccounts;
public AccountsAdapter() {
mAccounts = new ArrayList<DrawerItem>();
}
public void rebuildAccountList() {
if (!mHideAccounts) {
mAccounts = buildAccountList();
notifyDataSetChanged();
}
}
/**
* Builds the accounts adapter.
*/
private List<DrawerItem> buildAccountList() {
final List<DrawerItem> accountList = new ArrayList<DrawerItem>();
final Account[] allAccounts = getAllAccounts();
// Add all accounts and then the current account
final Uri currentAccountUri = getCurrentAccountUri();
for (final Account account : allAccounts) {
final int unreadCount = mFolderWatcher.getUnreadCount(account);
accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount,
currentAccountUri.equals(account.uri), mBidiFormatter));
}
if (mCurrentAccount == null) {
LogUtils.wtf(LOG_TAG, "buildAccountList() with null current account.");
}
return accountList;
}
private Uri getCurrentAccountUri() {
return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
}
@Override
public int getCount() {
return mAccounts.size();
}
@Override
public Object getItem(int position) {
// Is there an attempt made to access outside of the drawer item list?
if (position >= mAccounts.size()) {
return null;
} else {
return mAccounts.get(position);
}
}
@Override
public long getItemId(int position) {
return getItem(position).hashCode();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final DrawerItem item = (DrawerItem) getItem(position);
return item.getView(convertView, parent);
}
}
/**
* Sets the currently selected folder safely.
* @param folder the folder to change to. It is an error to pass null here.
*/
private void setSelectedFolder(Folder folder) {
if (folder == null) {
mSelectedFolderUri = FolderUri.EMPTY;
mCurrentFolderForUnreadCheck = null;
LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
return;
}
final boolean viewChanged =
!FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
// There are two cases in which the folder type is not set by this class.
// 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a
// folder but its type was never set.
// 2. The user backs into the default inbox. Going 'back' from the conversation list of
// any folder will take you to the default inbox for that account. (If you are in the
// default inbox already, back exits the app.)
// In both these cases, the selected folder type is not set, and must be set.
if (mSelectedFolderType == DrawerItem.UNSET || (mCurrentAccount != null
&& folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) {
mSelectedFolderType =
folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER;
}
mCurrentFolderForUnreadCheck = folder;
mSelectedFolderUri = folder.folderUri;
if (mFolderAdapter != null && viewChanged) {
mFolderAdapter.notifyDataSetChanged();
}
}
/**
* Sets the current account to the one provided here.
* @param account the current account to set to.
*/
private void setSelectedAccount(Account account){
final boolean changed = (account != null) && (mCurrentAccount == null
|| !mCurrentAccount.uri.equals(account.uri));
mCurrentAccount = account;
if (changed) {
// We no longer have proper folder objects. Let the new ones come in
mFolderAdapter.setCursor(null);
// If currentAccount is different from the one we set, restart the loader. Look at the
// comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
// don't just do restartLoader.
final LoaderManager manager = getLoaderManager();
manager.destroyLoader(FOLDER_LIST_LOADER_ID);
manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
// An updated cursor causes the entire list to refresh. No need to refresh the list.
// But we do need to blank out the current folder, since the account might not be
// synced.
mSelectedFolderUri = FolderUri.EMPTY;
mCurrentFolderForUnreadCheck = null;
} else if (account == null) {
// This should never happen currently, but is a safeguard against a very incorrect
// non-null account -> null account transition.
LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
final LoaderManager manager = getLoaderManager();
manager.destroyLoader(FOLDER_LIST_LOADER_ID);
manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID);
}
}
/**
* Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
*/
private boolean isFolderTypeExcluded(final Folder folder) {
if (mExcludedFolderTypes == null) {
return false;
}
for (final int excludedType : mExcludedFolderTypes) {
if (folder.isType(excludedType)) {
return true;
}
}
return false;
}
/**
* @return the choice mode to use for the {@link ListView}
*/
protected int getListViewChoiceMode() {
return mAccountController.getFolderListViewChoiceMode();
}
}