| /* |
| * 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.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Loader; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.Contacts.Photo; |
| import android.text.TextUtils; |
| 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.browse.LetterTileUtils; |
| import com.android.mail.content.ObjectCursor; |
| import com.android.mail.content.ObjectCursorLoader; |
| import com.android.mail.photomanager.BitmapUtil; |
| 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.LogTag; |
| import com.android.mail.utils.LogUtils; |
| import com.google.common.collect.ImmutableSet; |
| import com.android.mail.utils.Utils; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * The folder list UI component. |
| */ |
| public class FolderListFragment extends ListFragment implements |
| LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> { |
| private static final String LOG_TAG = LogTag.getLogTag(); |
| /** The parent activity */ |
| private ControllableActivity mActivity; |
| /** 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 sectioned FolderList, false otherwise. */ |
| protected boolean mIsSectioned; |
| /** True if the folder list belongs to a folder selection activity (one account only) */ |
| private boolean mHideAccounts; |
| /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */ |
| private ArrayList<Integer> mExcludedFolderTypes; |
| /** Object that changes folders on our behalf. */ |
| private FolderListSelectionListener mFolderChanger; |
| /** Object that changes accounts on our behalf */ |
| private AccountController mAccountChanger; |
| |
| /** The currently selected folder (the folder being viewed). This is never null. */ |
| private Uri mSelectedFolderUri = Uri.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_LOADER_ID = 0; |
| /** Key to store {@link #mParentFolder}. */ |
| private static final String ARG_PARENT_FOLDER = "arg-parent-folder"; |
| /** Key to store {@link #mIsSectioned} */ |
| private static final String ARG_IS_SECTIONED = "arg-is-sectioned"; |
| /** 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"; |
| /** Key to store {@link #mType} */ |
| private static final String ARG_TYPE = "arg-flf-type"; |
| /** Key to store {@link #mHideAccounts} */ |
| private static final String ARG_HIDE_ACCOUNTS = "arg-hide-accounts"; |
| |
| /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */ |
| private int mType; |
| /** This fragment is a drawer */ |
| private static final int TYPE_DRAWER = 0; |
| /** This fragment is a folder tree */ |
| private static final int TYPE_TREE = 1; |
| |
| 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"; |
| |
| private FolderListFragmentCursorAdapter mCursorAdapter; |
| /** 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_SYSTEM}, |
| * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_USER}. |
| * 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; |
| |
| /** |
| * 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(mCursorAdapter != null ? mCursorAdapter.getCount() : -1); |
| sb.append("}"); |
| return sb.toString(); |
| } |
| |
| /** |
| * Creates a new instance of {@link FolderListFragment}. Gets the current account and current |
| * folder through observers. |
| */ |
| public static FolderListFragment ofDrawer() { |
| final FolderListFragment fragment = new FolderListFragment(); |
| // The drawer is always sectioned |
| final boolean isSectioned = true; |
| fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null, false)); |
| return fragment; |
| } |
| |
| /** |
| * Creates a new instance of {@link FolderListFragment}, initialized |
| * to display the folder and its immediate children. |
| * @param folder parent folder whose children are shown |
| * @param hideAccounts True if accounts should be hidden, false otherwise |
| */ |
| public static FolderListFragment ofTree(Folder folder, final boolean hideAccounts) { |
| final FolderListFragment fragment = new FolderListFragment(); |
| // Trees are never sectioned. |
| final boolean isSectioned = false; |
| fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri, |
| isSectioned, null, hideAccounts)); |
| return fragment; |
| } |
| |
| /** |
| * Creates a new instance of {@link FolderListFragment}, initialized |
| * to display the folder and its immediate children. |
| * @param folderListUri the URI which contains all the list of folders |
| * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying |
| * @param hideAccounts True if accounts should be hidden, false otherwise |
| */ |
| public static FolderListFragment ofTopLevelTree(Uri folderListUri, |
| final ArrayList<Integer> excludedFolderTypes, final boolean hideAccounts) { |
| final FolderListFragment fragment = new FolderListFragment(); |
| // Trees are never sectioned. |
| final boolean isSectioned = false; |
| fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri, |
| isSectioned, excludedFolderTypes, hideAccounts)); |
| return fragment; |
| } |
| |
| /** |
| * Construct a bundle that represents the state of this fragment. |
| * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE} |
| * @param parentFolder non-null for trees, the parent of this list |
| * @param isSectioned true if this drawer is sectioned, false otherwise |
| * @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, sectioned list boolean and |
| * excluded folder types |
| */ |
| private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri, |
| boolean isSectioned, final ArrayList<Integer> excludedFolderTypes, |
| final boolean hideAccounts) { |
| final Bundle args = new Bundle(); |
| args.putInt(ARG_TYPE, type); |
| if (parentFolder != null) { |
| args.putParcelable(ARG_PARENT_FOLDER, parentFolder); |
| } |
| if (folderListUri != null) { |
| args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString()); |
| } |
| args.putBoolean(ARG_IS_SECTIONED, isSectioned); |
| if (excludedFolderTypes != null) { |
| args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes); |
| } |
| args.putBoolean(ARG_HIDE_ACCOUNTS, hideAccounts); |
| 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(); |
| Folder currentFolder = null; |
| if (! (activity instanceof ControllableActivity)){ |
| LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" + |
| "create it. Cannot proceed."); |
| } |
| mActivity = (ControllableActivity) activity; |
| final FolderController controller = mActivity.getFolderController(); |
| // Listen to folder changes in the future |
| mFolderObserver = new FolderObserver() { |
| @Override |
| public void onChanged(Folder newFolder) { |
| setSelectedFolder(newFolder); |
| } |
| }; |
| if (controller != null) { |
| // Only register for selected folder updates if we have a controller. |
| currentFolder = mFolderObserver.initialize(controller); |
| mCurrentFolderForUnreadCheck = currentFolder; |
| } |
| |
| // Initialize adapter for folder/heirarchical list. Note this relies on |
| // mActivity being initialized. |
| final Folder selectedFolder; |
| if (mParentFolder != null) { |
| mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder); |
| selectedFolder = mActivity.getHierarchyFolder(); |
| } else { |
| mCursorAdapter = new FolderListAdapter(mIsSectioned); |
| selectedFolder = currentFolder; |
| } |
| // Is the selected folder fresher than the one we have restored from a bundle? |
| if (selectedFolder != null && !selectedFolder.uri.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.getFolderListSelectionListener(); |
| 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) { |
| mCursorAdapter.notifyAllAccountsChanged(); |
| } |
| }; |
| mAllAccountsObserver.initialize(accountController); |
| mAccountChanger = 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) { |
| mAccountChanger.switchToDefaultInboxOrChangeAccount(mNextAccount); |
| mNextAccount = null; |
| } |
| } |
| }; |
| mDrawerObserver.initialize(accountController); |
| } |
| |
| if (mActivity.isFinishing()) { |
| // Activity is finishing, just bail. |
| return; |
| } |
| |
| setListAdapter(mCursorAdapter); |
| } |
| |
| /** |
| * Set the instance variables from the arguments provided here. |
| * @param args |
| */ |
| private void setInstanceFromBundle(Bundle args) { |
| if (args == null) { |
| return; |
| } |
| mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER); |
| final String folderUri = args.getString(ARG_FOLDER_LIST_URI); |
| if (folderUri == null) { |
| mFolderListUri = Uri.EMPTY; |
| } else { |
| mFolderListUri = Uri.parse(folderUri); |
| } |
| mIsSectioned = args.getBoolean(ARG_IS_SECTIONED); |
| mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES); |
| mType = args.getInt(ARG_TYPE); |
| mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedState) { |
| final Bundle args = getArguments(); |
| if (args != null) { |
| mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false); |
| } |
| final View rootView = inflater.inflate(R.layout.folder_list, null); |
| mListView = (ListView) rootView.findViewById(android.R.id.list); |
| mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); |
| 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 = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)); |
| mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE); |
| } else if (mParentFolder != null) { |
| mSelectedFolderUri = mParentFolder.uri; |
| // 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 (mCursorAdapter != null) { |
| mCursorAdapter.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); |
| } |
| |
| /** |
| * 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; |
| if (item instanceof DrawerItem) { |
| final DrawerItem drawerItem = (DrawerItem) item; |
| // Could be a folder or account. |
| final int itemType = mCursorAdapter.getItemType(drawerItem); |
| if (itemType == DrawerItem.VIEW_ACCOUNT) { |
| // Account, so switch. |
| folder = null; |
| final Account account = drawerItem.mAccount; |
| |
| if (account != null && account.settings.defaultInbox.equals(mSelectedFolderUri)) { |
| // We're already in the default inbox for the same account, just re-check item. |
| final int defaultInboxPosition = position + 1; |
| if (mListView.getChildAt(defaultInboxPosition) != null) { |
| mListView.setItemChecked(defaultInboxPosition, true); |
| } |
| } else { |
| // Switching accounts takes you to the inbox, which is always a system folder. |
| mSelectedFolderType = DrawerItem.FOLDER_SYSTEM; |
| mNextAccount = account; |
| mAccountChanger.closeDrawerForNewList(); |
| } |
| } else if (itemType == DrawerItem.VIEW_FOLDER) { |
| // Folder type, so change folders only. |
| folder = drawerItem.mFolder; |
| mSelectedFolderType = 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 if (item instanceof ObjectCursor){ |
| folder = ((ObjectCursor<Folder>) item).getModel(); |
| } else { |
| // Don't know how we got here. |
| LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item"); |
| folder = null; |
| } |
| if (folder != null) { |
| // Since we may be looking at hierarchical views, if we can |
| // determine the parent of the folder we have tapped, set it here. |
| // If we are looking at the folder we are already viewing, don't |
| // update its parent! |
| folder.parent = folder.equals(mParentFolder) ? null : mParentFolder; |
| // Go to the conversation list for this folder. |
| if (!folder.uri.equals(mSelectedFolderUri)) { |
| mNextFolder = folder; |
| mAccountChanger.closeDrawerForNewList(); |
| } |
| } |
| } |
| |
| @Override |
| public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) { |
| mListView.setEmptyView(null); |
| final Uri folderListUri; |
| if (mType == TYPE_TREE) { |
| // Folder trees, they specify a URI at construction time. |
| folderListUri = mFolderListUri; |
| } else if (mType == TYPE_DRAWER) { |
| // Drawers should have a valid account |
| if (mCurrentAccount != null) { |
| folderListUri = mCurrentAccount.folderListUri; |
| } else { |
| LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account"); |
| return null; |
| } |
| } 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 (mCursorAdapter != null) { |
| mCursorAdapter.setCursor(data); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) { |
| if (mCursorAdapter != null) { |
| mCursorAdapter.setCursor(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); |
| /** |
| * Given an item, find the type of the item, which should only be {@link |
| * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT} |
| * @return item the type of the item. |
| */ |
| int getItemType(DrawerItem item); |
| /** Get the folder associated with this item. **/ |
| Folder getFullFolder(DrawerItem item); |
| /** Notify that the all accounts changed. */ |
| void notifyAllAccountsChanged(); |
| /** 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 FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter { |
| |
| private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() { |
| @Override |
| public void onChanged() { |
| if (!isCursorInvalid(mCursor)) { |
| recalculateList(); |
| } |
| } |
| }; |
| /** 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 sectioned, false otherwise */ |
| private final boolean mIsSectioned; |
| /** All the items */ |
| private List<DrawerItem> mItemList = new ArrayList<DrawerItem>(); |
| /** Cursor into the folder list. This might be null. */ |
| private ObjectCursor<Folder> mCursor = null; |
| /** Watcher for tracking and receiving unread counts for mail */ |
| private FolderWatcher mFolderWatcher = null; |
| |
| /** |
| * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders. |
| * |
| * @param isSectioned true if folder list is flat, false if sectioned by label group |
| */ |
| public FolderListAdapter(boolean isSectioned) { |
| super(); |
| mIsSectioned = isSectioned; |
| final RecentFolderController controller = mActivity.getRecentFolderController(); |
| if (controller != null && mIsSectioned) { |
| mRecentFolders = mRecentFolderObserver.initialize(controller); |
| } else { |
| mRecentFolders = null; |
| } |
| mFolderWatcher = new FolderWatcher(mActivity, this); |
| mFolderWatcher.updateAccountList(getAllAccounts()); |
| } |
| |
| @Override |
| public void notifyAllAccountsChanged() { |
| mFolderWatcher.updateAccountList(getAllAccounts()); |
| recalculateList(); |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| final DrawerItem item = (DrawerItem) getItem(position); |
| final View view; |
| final int type = item.mType; |
| final boolean isSelected = |
| item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType); |
| if (type == DrawerItem.VIEW_FOLDER) { |
| mListView.setItemChecked(position, isSelected); |
| item.setSelected(isSelected); |
| } |
| view = item.getView(position, convertView, parent); |
| // 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); |
| } |
| LogUtils.d(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item); |
| 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)); |
| if (drawerItem == null) { |
| // If there is no item, return false as there's nothing there to be enabled |
| return false; |
| } else { |
| return drawerItem.isItemEnabled(); |
| } |
| } |
| |
| private Uri getCurrentAccountUri() { |
| return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri; |
| } |
| |
| @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 FolderListAdapter#mItemList} |
| */ |
| private void recalculateList() { |
| final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>(); |
| // Don't show accounts for single-account-based folder selection (i.e. widgets) |
| if (!mHideAccounts) { |
| recalculateListAccounts(newFolderList); |
| } |
| recalculateListFolders(newFolderList); |
| mItemList = newFolderList; |
| // Ask the list to invalidate its views. |
| notifyDataSetChanged(); |
| |
| } |
| |
| /** |
| * Recalculates the accounts if not null and adds them to the list. |
| * |
| * @param itemList List of drawer items to populate |
| */ |
| private void recalculateListAccounts(List<DrawerItem> itemList) { |
| 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); |
| itemList.add(DrawerItem.ofAccount( |
| mActivity, account, unreadCount, currentAccountUri.equals(account.uri))); |
| } |
| if (mCurrentAccount == null) { |
| LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account."); |
| } |
| } |
| |
| /** |
| * Recalculates the system, recent and user label lists. |
| * This method modifies all the three lists on every single invocation. |
| * |
| * @param itemList List of drawer items to populate |
| */ |
| private void recalculateListFolders(List<DrawerItem> itemList) { |
| // 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(mCursor)) { |
| if(!mCurrentAccount.isAccountReady()) { |
| itemList.add(DrawerItem.forWaitView(mActivity)); |
| } |
| return; |
| } |
| |
| if (!mIsSectioned) { |
| // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers. |
| do { |
| final Folder f = mCursor.getModel(); |
| if (!isFolderTypeExcluded(f)) { |
| itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER, |
| mCursor.getPosition())); |
| } |
| } while (mCursor.moveToNext()); |
| return; |
| } |
| |
| // Otherwise, this is an adapter for a sectioned 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_SYSTEM, mCursor.getPosition())); |
| } else { |
| allFoldersList.add(DrawerItem.ofFolder( |
| mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition())); |
| } |
| } |
| } while (mCursor.moveToNext()); |
| |
| // Add all inboxes (sectioned included) before recents. |
| addFolderSection(itemList, inboxFolders, R.string.inbox_folders_heading); |
| |
| // Add most recently folders (in alphabetical order) next. |
| addRecentsToList(itemList); |
| |
| // Add the remaining provider folders followed by all labels. |
| addFolderSection(itemList, allFoldersList, R.string.all_folders_heading); |
| } |
| |
| /** |
| * Given a list of folders as {@link DrawerItem}s, add them to the item |
| * list as needed. 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 FolderListAdapter#NO_HEADER_RESOURCE} if no header |
| * is required, or res-id otherwise |
| */ |
| private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source, |
| int headerStringResource) { |
| if (source.size() > 0) { |
| if(headerStringResource != NO_HEADER_RESOURCE) { |
| destination.add(DrawerItem.ofHeader(mActivity, headerStringResource)); |
| } |
| 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)); |
| // Recent folders are not queried for position. |
| final int position = -1; |
| for (Folder f : recentFolderList) { |
| destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT, |
| position)); |
| } |
| } |
| } |
| |
| /** |
| * Check if the cursor provided is valid. |
| * @param mCursor |
| * @return True if cursor is invalid, false otherwise |
| */ |
| private boolean isCursorInvalid(Cursor mCursor) { |
| return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0 |
| || !mCursor.moveToFirst(); |
| } |
| |
| @Override |
| public void setCursor(ObjectCursor<Folder> cursor) { |
| mCursor = cursor; |
| recalculateList(); |
| } |
| |
| @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(); |
| } |
| |
| @Override |
| public int getItemType(DrawerItem item) { |
| return item.mType; |
| } |
| |
| // TODO(viki): This is strange. We have the full folder and yet we create on from scratch. |
| @Override |
| public Folder getFullFolder(DrawerItem folderItem) { |
| if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) { |
| return folderItem.mFolder; |
| } else { |
| final int pos = folderItem.mPosition; |
| if (pos > -1 && mCursor != null && !mCursor.isClosed() |
| && mCursor.moveToPosition(folderItem.mPosition)) { |
| return mCursor.getModel(); |
| } else { |
| return null; |
| } |
| } |
| } |
| } |
| |
| private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder> |
| implements FolderListFragmentCursorAdapter{ |
| |
| private static final int PARENT = 0; |
| private static final int CHILD = 1; |
| private final Uri mParentUri; |
| private final Folder mParent; |
| private final FolderItemView.DropHandler mDropHandler; |
| private ObjectCursor<Folder> mCursor; |
| |
| public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) { |
| super(mActivity.getActivityContext(), R.layout.folder_item); |
| mDropHandler = mActivity; |
| mParent = parentFolder; |
| mParentUri = parentFolder.uri; |
| setCursor(c); |
| } |
| |
| @Override |
| public int getViewTypeCount() { |
| // Child and Parent |
| return 2; |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| final Folder f = getItem(position); |
| return f.uri.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.uri.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); |
| if (folder.uri.equals(mSelectedFolderUri)) { |
| getListView().setItemChecked(position, true); |
| folderItemView.setSelected(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) { |
| mCursor = cursor; |
| clear(); |
| if (mParent != null) { |
| add(mParent); |
| } |
| if (cursor != null && cursor.getCount() > 0) { |
| cursor.moveToFirst(); |
| do { |
| Folder f = cursor.getModel(); |
| f.parent = mParent; |
| add(f); |
| } while (cursor.moveToNext()); |
| } |
| } |
| |
| @Override |
| public void destroy() { |
| // Do nothing. |
| } |
| |
| @Override |
| public int getItemType(DrawerItem item) { |
| // Always returns folders for now. |
| return DrawerItem.VIEW_FOLDER; |
| } |
| |
| @Override |
| public Folder getFullFolder(DrawerItem folderItem) { |
| final int pos = folderItem.mPosition; |
| if (mCursor == null || mCursor.isClosed()) { |
| return null; |
| } |
| if (pos > -1 && mCursor != null && !mCursor.isClosed() |
| && mCursor.moveToPosition(folderItem.mPosition)) { |
| return mCursor.getModel(); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public void notifyAllAccountsChanged() { |
| // Do nothing. We don't care about changes to all accounts. |
| } |
| } |
| |
| public Folder getParentFolder() { |
| return mParentFolder; |
| } |
| |
| /** |
| * Sets the currently selected folder safely. |
| * @param folder |
| */ |
| private void setSelectedFolder(Folder folder) { |
| if (folder == null) { |
| mSelectedFolderUri = Uri.EMPTY; |
| mCurrentFolderForUnreadCheck = null; |
| LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!"); |
| return; |
| } |
| final boolean viewChanged = |
| !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck); |
| mCurrentFolderForUnreadCheck = folder; |
| mSelectedFolderUri = folder.uri; |
| setSelectedFolderType(folder); |
| if (mCursorAdapter != null && viewChanged) { |
| mCursorAdapter.notifyDataSetChanged(); |
| } |
| } |
| |
| /** |
| * Sets the selected folder type safely. |
| * @param folder folder to set to. |
| */ |
| private void setSelectedFolderType(Folder folder) { |
| if (mSelectedFolderType == DrawerItem.UNSET) { |
| mSelectedFolderType = folder.isInbox() ? DrawerItem.FOLDER_SYSTEM |
| : DrawerItem.FOLDER_USER; |
| } else if (folder.isInbox()) { |
| // Handle when backing up in case we need to manually set the type |
| mSelectedFolderType = DrawerItem.FOLDER_SYSTEM; |
| } |
| } |
| |
| /** |
| * 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 |
| mCursorAdapter.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_LOADER_ID); |
| manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this); |
| // An updated cursor causes the entire list to refresh. No need to refresh the list. |
| } 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_LOADER_ID); |
| } |
| } |
| |
| public interface FolderListSelectionListener { |
| public void onFolderSelected(Folder folder); |
| } |
| |
| /** |
| * Get whether the FolderListFragment is currently showing the hierarchy |
| * under a single parent. |
| */ |
| public boolean showingHierarchy() { |
| return mParentFolder != null; |
| } |
| |
| /** |
| * 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; |
| } |
| } |