/* * 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.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.app.Activity; import android.app.ListFragment; import android.app.LoaderManager; import android.content.Loader; import android.database.DataSetObserver; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.widget.DrawerLayout; 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.bitmap.BitmapCache; import com.android.bitmap.UnrefedBitmapCache; import com.android.mail.R; import com.android.mail.analytics.Analytics; import com.android.mail.bitmap.AccountAvatarDrawable; import com.android.mail.bitmap.ContactResolver; import com.android.mail.browse.MergedAdapter; import com.android.mail.content.ObjectCursor; import com.android.mail.content.ObjectCursorLoader; import com.android.mail.drawer.DrawerItem; import com.android.mail.drawer.FooterItem; import com.android.mail.providers.Account; import com.android.mail.providers.AccountObserver; import com.android.mail.providers.AllAccountObserver; 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 com.android.mail.utils.Utils; import com.google.common.collect.Lists; 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: * */ public class FolderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks>, FolderWatcher.UnreadCountChangedListener { private static final String LOG_TAG = LogTag.getLogTag(); // Duration to fade alpha from 0 to 1 and vice versa. private static final long DRAWER_FADE_VELOCITY_MS_PER_ALPHA = TwoPaneLayout.SLIDE_DURATION_MS; /** The parent activity */ protected 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 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) * and the footer should not show. */ protected boolean mIsFolderSelectionActivity = true; /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */ private ArrayList mExcludedFolderTypes; /** Object that changes folders on our behalf. */ private FolderSelector mFolderChanger; /** Object that changes accounts on our behalf */ private AccountController mAccountController; private DrawerController mDrawerController; /** 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_ITEM_TYPE = "flf-selected-item-type"; private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type"; private static final String BUNDLE_INBOX_PRESENT = "flf-inbox-present"; /** Number of avatars to we whould like to fit in the avatar cache */ private static final int IMAGE_CACHE_COUNT = 10; /** * This is the fractional portion of the total cache size above that's dedicated to non-pooled * bitmaps. (This is basically the portion of cache dedicated to GIFs.) */ private static final float AVATAR_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f; /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */ private static final int AVATAR_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100; /** Adapter used by the list that wraps both the folder adapter and the accounts adapter. */ private MergedAdapter 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; /** Adapter containing the Help and Feedback views */ private FooterAdapter mFooterAdapter; /** 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 to changes to selected folder or account */ private FolderOrAccountListener mFolderOrAccountListener = 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 mSelectedDrawerItemCategory = DrawerItem.UNSET; /** The FolderType of the selected folder {@link FolderType} */ private int mSelectedFolderType = FolderType.INBOX; /** The current account according to the controller */ protected 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; private final DrawerStateListener mDrawerListener = new DrawerStateListener(); private BitmapCache mImagesCache; private ContactResolver mContactResolver; private boolean mInboxPresent; private boolean mMiniDrawerEnabled; private boolean mIsMinimized; protected MiniDrawerView mMiniDrawerView; private MiniDrawerAccountsAdapter mMiniDrawerAccountsAdapter; // use the same dimen as AccountItemView to participate in recycling // TODO: but Material account switcher doesn't recycle... private int mMiniDrawerAvatarDecodeSize; private AnimatorListenerAdapter mMiniDrawerFadeOutListener; private AnimatorListenerAdapter mListViewFadeOutListener; private AnimatorListenerAdapter mMiniDrawerFadeInListener; private AnimatorListenerAdapter mListViewFadeInListener; /** * 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 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 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; mMiniDrawerAvatarDecodeSize = getResources().getDimensionPixelSize(R.dimen.account_avatar_dimension); final int avatarSize = getActivity().getResources().getDimensionPixelSize( R.dimen.account_avatar_dimension); mImagesCache = new UnrefedBitmapCache(Utils.isLowRamDevice(getActivity()) ? 0 : avatarSize * avatarSize * IMAGE_CACHE_COUNT, AVATAR_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION, AVATAR_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY); mContactResolver = new ContactResolver(getActivity().getContentResolver(), mImagesCache); if (mMiniDrawerEnabled) { setupMiniDrawerAccountsAdapter(); mMiniDrawerView.setController(this); // set up initial state setMinimized(isMinimized()); } else { mMiniDrawerView.setVisibility(View.GONE); } 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/hierarchical 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 = newAccountsAdapter(); mFooterAdapter = new FooterAdapter(); // 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) { mAccountController = accountController; // 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()); rebuildAccountList(); } }; mAllAccountsObserver.initialize(accountController); mFolderOrAccountListener = new FolderOrAccountListener(); mAccountController.registerFolderOrAccountChangedObserver(mFolderOrAccountListener); final DrawerController dc = mActivity.getDrawerController(); if (dc != null) { dc.registerDrawerListener(mDrawerListener); } } mDrawerController = mActivity.getDrawerController(); if (mActivity.isFinishing()) { // Activity is finishing, just bail. return; } mListView.setChoiceMode(getListViewChoiceMode()); mMergedAdapter = new MergedAdapter<>(); if (mAccountsAdapter != null) { mMergedAdapter.setAdapters(mAccountsAdapter, mFolderAdapter, mFooterAdapter); } else { mMergedAdapter.setAdapters(mFolderAdapter, mFooterAdapter); } mFolderWatcher = new FolderWatcher(mActivity, this); mFolderWatcher.updateAccountList(getAllAccounts()); setListAdapter(mMergedAdapter); } public BitmapCache getBitmapCache() { return mImagesCache; } public ContactResolver getContactResolver() { return mContactResolver; } public void toggleDrawerState() { if (mDrawerController != null) { mDrawerController.toggleDrawerState(); } } /** * 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, container, false); mListView = (ListView) rootView.findViewById(android.R.id.list); mListView.setEmptyView(null); mListView.setDivider(null); addListHeader(inflater, rootView, mListView); 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))); mSelectedDrawerItemCategory = savedState.getInt(BUNDLE_SELECTED_ITEM_TYPE); mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE); } else if (mParentFolder != null) { mSelectedFolderUri = mParentFolder.folderUri; // No selected folder type required for hierarchical lists. } if (savedState != null) { mInboxPresent = savedState.getBoolean(BUNDLE_INBOX_PRESENT, true); } else { mInboxPresent = true; } mMiniDrawerView = (MiniDrawerView) rootView.findViewById(R.id.mini_drawer); // Create default animator listeners mMiniDrawerFadeOutListener = new FadeAnimatorListener(mMiniDrawerView, true /* fadeOut */); mListViewFadeOutListener = new FadeAnimatorListener(mListView, true /* fadeOut */); mMiniDrawerFadeInListener = new FadeAnimatorListener(mMiniDrawerView, false /* fadeOut */); mListViewFadeInListener = new FadeAnimatorListener(mListView, false /* fadeOut */); return rootView; } protected void addListHeader(LayoutInflater inflater, View rootView, ListView list) { // Default impl does nothing } @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_ITEM_TYPE, mSelectedDrawerItemCategory); outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType); outState.putBoolean(BUNDLE_INBOX_PRESENT, mInboxPresent); } @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 (mFolderOrAccountListener != null && mAccountController != null) { mAccountController.unregisterFolderOrAccountChangedObserver(mFolderOrAccountListener); mFolderOrAccountListener = null; } super.onDestroyView(); if (mActivity != null) { final DrawerController dc = mActivity.getDrawerController(); if (dc != null) { dc.unregisterDrawerListener(mDrawerListener); } } } @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); } protected int getUnreadCount(Account account) { if (account == null || mFolderWatcher == null) { return 0; } return mFolderWatcher.getUnreadCount(account); } protected void changeAccount(final Account account) { // Switching accounts takes you to the default inbox for that account. mSelectedDrawerItemCategory = DrawerItem.FOLDER_INBOX; mSelectedFolderType = FolderType.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. */ protected void viewFolderOrChangeAccount(int position) { // Get the ListView's adapter final Object item = getListView().getAdapter().getItem(position); LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item); final Folder folder; @DrawerItem.DrawerItemCategory int itemCategory = DrawerItem.UNSET; if (item instanceof DrawerItem) { final DrawerItem drawerItem = (DrawerItem) item; // Could be a folder or account or footer final @DrawerItem.DrawerItemType int itemType = drawerItem.getType(); if (itemType == DrawerItem.VIEW_ACCOUNT) { // Account, so switch. folder = null; onAccountSelected(drawerItem.mAccount); } else if (itemType == DrawerItem.VIEW_FOLDER) { // Folder type, so change folders only. folder = drawerItem.mFolder; mSelectedDrawerItemCategory = itemCategory = drawerItem.mItemCategory; mSelectedFolderType = folder.type; LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d", folder, mSelectedDrawerItemCategory); } else if (itemType == DrawerItem.VIEW_FOOTER_HELP || itemType == DrawerItem.VIEW_FOOTER_SETTINGS) { folder = null; drawerItem.onClick(null /* unused */); } 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) { final String label = (itemCategory == DrawerItem.FOLDER_RECENT) ? "recent" : "normal"; onFolderSelected(folder, label); } } public void onFolderSelected(Folder folder, String analyticsLabel) { // Go to the conversation list for this folder. if (!folder.folderUri.equals(mSelectedFolderUri)) { mNextFolder = folder; mAccountController.closeDrawer(true /** hasNewFolderOrAccount */, null /** nextAccount */, folder /** nextFolder */); Analytics.getInstance().sendEvent("switch_folder", folder.getTypeDescription(), analyticsLabel, 0); } else { // Clicked on same folder, just close drawer mAccountController.closeDrawer(false /** hasNewFolderOrAccount */, null /** nextAccount */, folder /** nextFolder */); } } public void onAccountSelected(Account account) { // Only reset the cache if the account has changed. if (mCurrentAccount == null || account == null || !mCurrentAccount.getEmailAddress().equals(account.getEmailAddress())) { mActivity.resetSenderImageCache(); } 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); } } @Override public Loader> onCreateLoader(int id, Bundle args) { 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<>(mActivity.getActivityContext(), folderListUri, UIProvider.FOLDERS_PROJECTION, Folder.FACTORY); } @Override public void onLoadFinished(Loader> loader, ObjectCursor data) { if (mFolderAdapter != null) { if (loader.getId() == FOLDER_LIST_LOADER_ID) { mFolderAdapter.setCursor(data); if (mMiniDrawerEnabled) { mMiniDrawerView.refresh(); } } else if (loader.getId() == ALL_FOLDER_LIST_LOADER_ID) { mFolderAdapter.setAllFolderListCursor(data); } } } @Override public void onLoaderReset(Loader> 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 */ public Account[] getAllAccounts() { if (mAllAccountsObserver != null) { return mAllAccountsObserver.getAllAccounts(); } return new Account[0]; } protected AccountsAdapter newAccountsAdapter() { return new AccountsAdapter(); } @Override public void onUnreadCountChange() { if (mAccountsAdapter != null) { mAccountsAdapter.notifyDataSetChanged(); } } public boolean isMiniDrawerEnabled() { return mMiniDrawerEnabled; } public void setMiniDrawerEnabled(boolean enabled) { mMiniDrawerEnabled = enabled; setMinimized(isMinimized()); // init visual state } public boolean isMinimized() { return mMiniDrawerEnabled && mIsMinimized; } public void setMinimized(boolean minimized) { if (!mMiniDrawerEnabled) { return; } mIsMinimized = minimized; if (isMinimized()) { mMiniDrawerView.setVisibility(View.VISIBLE); mMiniDrawerView.setAlpha(1f); mListView.setVisibility(View.INVISIBLE); mListView.setAlpha(0f); } else { mMiniDrawerView.setVisibility(View.INVISIBLE); mMiniDrawerView.setAlpha(0f); mListView.setVisibility(View.VISIBLE); mListView.setAlpha(1f); } } public void animateMinimized(boolean minimized) { if (!mMiniDrawerEnabled) { return; } mIsMinimized = minimized; Utils.enableHardwareLayer(mMiniDrawerView); Utils.enableHardwareLayer(mListView); if (mIsMinimized) { // From the current state (either maximized or partially dragged) to minimized. final float startAlpha = mListView.getAlpha(); final long duration = (long) (startAlpha * DRAWER_FADE_VELOCITY_MS_PER_ALPHA); mMiniDrawerView.setVisibility(View.VISIBLE); // Animate the mini-drawer to fade in. mMiniDrawerView.animate() .alpha(1f) .setDuration(duration) .setListener(mMiniDrawerFadeInListener); // Animate the list view to fade out. mListView.animate() .alpha(0f) .setDuration(duration) .setListener(mListViewFadeOutListener); } else { // From the current state (either minimized or partially dragged) to maximized. final float startAlpha = mMiniDrawerView.getAlpha(); final long duration = (long) (startAlpha * DRAWER_FADE_VELOCITY_MS_PER_ALPHA); mListView.setVisibility(View.VISIBLE); mListView.requestFocus(); // Animate the mini-drawer to fade out. mMiniDrawerView.animate() .alpha(0f) .setDuration(duration) .setListener(mMiniDrawerFadeOutListener); // Animate the list view to fade in. mListView.animate() .alpha(1f) .setDuration(duration) .setListener(mListViewFadeInListener); } } public void onDrawerDragStarted() { Utils.enableHardwareLayer(mMiniDrawerView); Utils.enableHardwareLayer(mListView); // The drawer drag will always end with animating the drawers to their final states, so // the animation will remove the hardware layer upon completion. } public void onDrawerDrag(float percent) { mMiniDrawerView.setAlpha(1f - percent); mListView.setAlpha(percent); mMiniDrawerView.setVisibility(View.VISIBLE); mListView.setVisibility(View.VISIBLE); } /** * 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 cursor); ObjectCursor getCursor(); /** Update the all folder list cursor with the cursor given here. */ void setAllFolderListCursor(ObjectCursor 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 BLANK_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 mItemList = new ArrayList<>(); /** Cursor into the folder list. This might be null. */ private ObjectCursor mCursor = null; /** Cursor into the all folder list. This might be null. */ private ObjectCursor 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 @DrawerItem.DrawerItemType int type = item.getType(); final boolean isSelected = item.isHighlighted(mSelectedFolderUri, mSelectedDrawerItemCategory); if (type == DrawerItem.VIEW_FOLDER) { mListView.setItemChecked((mAccountsAdapter != null ? mAccountsAdapter.getCount() : 0) + position + mListView.getHeaderViewsCount(), 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.getViewTypeCount(); } @Override public int getItemViewType(int position) { return ((DrawerItem) getItem(position)).getType(); } @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 getRecentFolders(RecentFolderList recentList) { final List folderList = new ArrayList<>(); 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() { final boolean oldInboxPresent = mInboxPresent; mItemList = recalculateListFolders(); if (mAccountController != null && mInboxPresent && !oldInboxPresent) { // We didn't have an inbox folder before, but now we do. This can occur when // setting up a new account. We automatically create the "starred" virtual // virtual folder, but we won't create the inbox until it gets synced. // This means that we'll start out looking at the "starred" folder, and the // user will need to manually switch to the inbox. See b/13793316 mAccountController.switchToDefaultInboxOrChangeAccount(mCurrentAccount); } // 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 recalculateListFolders() { final List itemList = new ArrayList<>(); // 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)); } return itemList; } if (mIsDivided) { //Choose an adapter for a divided list with sections return recalculateDividedListFolders(itemList); } else { // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers. return recalculateFlatListFolders(itemList); } } // Recalculate folder list intended to be flat (no hearders or sections shown). // This is commonly used for the widget or other simple folder selections private List recalculateFlatListFolders(List itemList) { final List inboxFolders = new ArrayList<>(); final List allFoldersList = new ArrayList<>(); do { final Folder f = mCursor.getModel(); if (!isFolderTypeExcluded(f)) { // Prioritize inboxes if (f.isInbox()) { inboxFolders.add(DrawerItem.ofFolder( mActivity, f, DrawerItem.FOLDER_OTHER)); } else { allFoldersList.add( DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER)); } } } while (mCursor.moveToNext()); itemList.addAll(inboxFolders); itemList.addAll(allFoldersList); return itemList; } // Recalculate folder list divided by sections (inboxes, recents, all, etc...) // This is primarily used by the drawer private List recalculateDividedListFolders(List itemList) { final List allFoldersList = new ArrayList<>(); final List inboxFolders = new ArrayList<>(); do { final Folder f = mCursor.getModel(); if (!isFolderTypeExcluded(f)) { if (f.isInbox()) { inboxFolders.add(DrawerItem.ofFolder( mActivity, f, DrawerItem.FOLDER_INBOX)); } else { allFoldersList.add(DrawerItem.ofFolder( mActivity, f, DrawerItem.FOLDER_OTHER)); } } } 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()); } // The search folder will not be found here because it is excluded from the drawer. // Don't switch off from the current folder if it's search. if (!currentFolderFound && !Folder.isType(FolderType.SEARCH, mSelectedFolderType) && mSelectedFolderUri != FolderUri.EMPTY && mCurrentAccount != null && mAccountController != null && mAccountController.isDrawerPullEnabled()) { LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s", folderName, mCurrentAccount.getEmailAddress()); changeAccount(mCurrentAccount); } } mInboxPresent = (inboxFolders.size() > 0); // Add all inboxes (sectioned Inboxes included) before recent folders. addFolderDivision(itemList, inboxFolders, BLANK_HEADER_RESOURCE); // 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#BLANK_HEADER_RESOURCE} if no header text * is required, or res-id otherwise. The integer is interpreted as the string * for the header's title. */ private void addFolderDivision(List destination, List source, int headerStringResource) { if (source.size() > 0) { if(headerStringResource != BLANK_HEADER_RESOURCE) { destination.add(DrawerItem.ofHeader(mActivity, headerStringResource)); } else { destination.add(DrawerItem.ofBlankHeader(mActivity)); } 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 destination) { // If there are recent folders, add them. final List recentFolderList = getRecentFolders(mRecentFolders); // Remove any excluded folder types if (mExcludedFolderTypes != null) { final Iterator 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. for (Folder f : recentFolderList) { destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT)); } } } /** * 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 cursor) { mCursor = cursor; rebuildAccountList(); rebuildFolderList(); } @Override public ObjectCursor getCursor() { return mCursor; } @Override public void setAllFolderListCursor(final ObjectCursor cursor) { mAllFolderListCursor = cursor; 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 implements FolderListFragmentCursorAdapter { private static final int PARENT = 0; private static final int CHILD = 1; private final FolderUri mParentUri; private final Folder mParent; public HierarchicalFolderListAdapter(ObjectCursor c, Folder parentFolder) { super(mActivity.getActivityContext(), R.layout.folder_item); 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); if (convertView != null) { folderItemView = (FolderItemView) convertView; } else { folderItemView = (FolderItemView) LayoutInflater.from( mActivity.getActivityContext()).inflate(R.layout.folder_item, null); } folderItemView.bind(folder, mParentUri); if (folder.folderUri.equals(mSelectedFolderUri)) { final ListView listView = getListView(); listView.setItemChecked((mAccountsAdapter != null ? mAccountsAdapter.getCount() : 0) + position + listView.getHeaderViewsCount(), 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 cursor) { clear(); if (mParent != null) { add(mParent); } if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); do { add(cursor.getModel()); } while (cursor.moveToNext()); } } @Override public ObjectCursor getCursor() { throw new UnsupportedOperationException("drawers don't have hierarchical folders"); } @Override public void setAllFolderListCursor(final ObjectCursor cursor) { // Not necessary in HierarchicalFolderListAdapter } @Override public void destroy() { // Do nothing. } } public void rebuildAccountList() { if (!mIsFolderSelectionActivity) { if (mAccountsAdapter != null) { mAccountsAdapter.setAccounts(buildAccountListDrawerItems()); } if (mMiniDrawerAccountsAdapter != null) { mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount); } } } protected static class AccountsAdapter extends BaseAdapter { private List mAccounts; public AccountsAdapter() { mAccounts = new ArrayList<>(); } public void setAccounts(List accounts) { mAccounts = accounts; notifyDataSetChanged(); } @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); } } /** * Builds the drawer items for the list of accounts. */ private List buildAccountListDrawerItems() { final Account[] allAccounts = getAllAccounts(); final List accountList = new ArrayList<>(allAccounts.length); // Add all accounts and then the current account final Uri currentAccountUri = getCurrentAccountUri(); for (final Account account : allAccounts) { final int unreadCount = getUnreadCount(account); accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, currentAccountUri.equals(account.uri), mImagesCache, mContactResolver)); } if (mCurrentAccount == null) { LogUtils.wtf(LOG_TAG, "buildAccountListDrawerItems() with null current account."); } return accountList; } private Uri getCurrentAccountUri() { return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri; } protected String getCurrentAccountEmailAddress() { return mCurrentAccount == null ? "" : mCurrentAccount.getEmailAddress(); } protected MergedAdapter getMergedAdapter() { return mMergedAdapter; } public ObjectCursor getFoldersCursor() { return (mFolderAdapter != null) ? mFolderAdapter.getCursor() : null; } private class FooterAdapter extends BaseAdapter { private final List mFooterItems = Lists.newArrayList(); private FooterAdapter() { update(); } @Override public int getCount() { return mFooterItems.size(); } @Override public DrawerItem getItem(int position) { return mFooterItems.get(position); } @Override public long getItemId(int position) { return position; } @Override public int getViewTypeCount() { // Accounts, headers, folders (all parts of drawer view types) return DrawerItem.getViewTypeCount(); } @Override public int getItemViewType(int position) { return getItem(position).getType(); } /** * @param convertView a view, possibly null, to be recycled. * @param parent the parent hosting this view. * @return a view for the footer item displaying the given text and image. */ @Override public View getView(int position, View convertView, ViewGroup parent) { return getItem(position).getView(convertView, parent); } /** * Recomputes the footer drawer items depending on whether the current account * is populated with URIs that navigate to appropriate destinations. */ private void update() { // if the parent activity shows a drawer, these items should participate in that drawer // (if it shows a *pane* they should *not* participate in that pane) if (mIsFolderSelectionActivity) { return; } mFooterItems.clear(); if (mCurrentAccount != null) { mFooterItems.add(DrawerItem.ofSettingsItem(mActivity, mCurrentAccount, mDrawerListener)); } if (mCurrentAccount != null && !Utils.isEmpty(mCurrentAccount.helpIntentUri)) { mFooterItems.add(DrawerItem.ofHelpItem(mActivity, mCurrentAccount, mDrawerListener)); } if (!mFooterItems.isEmpty()) { mFooterItems.add(0, DrawerItem.ofBlankHeader(mActivity)); mFooterItems.add(DrawerItem.ofBottomSpace(mActivity)); } notifyDataSetChanged(); } } /** * 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 (mSelectedDrawerItemCategory == DrawerItem.UNSET || (mCurrentAccount != null && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) { mSelectedDrawerItemCategory = folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER; mSelectedFolderType = folder.type; } mCurrentFolderForUnreadCheck = folder; mSelectedFolderUri = folder.folderUri; if (viewChanged) { if (mFolderAdapter != null) { mFolderAdapter.notifyDataSetChanged(); } if (mMiniDrawerView != null) { mMiniDrawerView.refresh(); } } } public boolean isSelectedFolder(@NonNull Folder folder) { return folder.folderUri.equals(mSelectedFolderUri); } /** * 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) { // Verify that the new account supports sending application feedback updateFooterItems(); // 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; // also set/update the mini-drawer if (mMiniDrawerAccountsAdapter != null) { mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount); } } 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); } } private void updateFooterItems() { mFooterAdapter.update(); } /** * 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(); } /** * Drawer listener for footer functionality to react to drawer state. */ public class DrawerStateListener implements DrawerLayout.DrawerListener { private FooterItem mPendingFooterClick; public void setPendingFooterClick(FooterItem itemClicked) { mPendingFooterClick = itemClicked; } @Override public void onDrawerSlide(View drawerView, float slideOffset) {} @Override public void onDrawerOpened(View drawerView) {} @Override public void onDrawerClosed(View drawerView) { if (mPendingFooterClick != null) { mPendingFooterClick.onFooterClicked(); mPendingFooterClick = null; } } @Override public void onDrawerStateChanged(int newState) {} } private class FolderOrAccountListener extends DataSetObserver { @Override public void onChanged() { // 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; } } } @Override public ListAdapter getListAdapter() { // Ensures that we get the adapter with the header views. throw new UnsupportedOperationException("Use getListView().getAdapter() instead " + "which accounts for any header or footer views."); } protected class MiniDrawerAccountsAdapter extends BaseAdapter { private List mAccounts = new ArrayList<>(); public void setAccounts(Account[] accounts, Account currentAccount) { mAccounts.clear(); if (currentAccount == null) { notifyDataSetChanged(); return; } mAccounts.add(currentAccount); // TODO: sort by most recent accounts for (final Account account : accounts) { if (!account.getEmailAddress().equals(currentAccount.getEmailAddress())) { mAccounts.add(account); } } notifyDataSetChanged(); } @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 ImageView iv = convertView != null ? (ImageView) convertView : (ImageView) LayoutInflater.from(getActivity()).inflate( R.layout.mini_drawer_recent_account_item, parent, false /* attachToRoot */); final MiniDrawerAccountItem item = new MiniDrawerAccountItem(iv); item.setupDrawable(); item.setAccount(mAccounts.get(position)); iv.setTag(item); return iv; } private class MiniDrawerAccountItem implements View.OnClickListener { private Account mAccount; private AccountAvatarDrawable mDrawable; public final ImageView view; public MiniDrawerAccountItem(ImageView iv) { view = iv; view.setOnClickListener(this); } public void setupDrawable() { mDrawable = new AccountAvatarDrawable(getResources(), getBitmapCache(), getContactResolver()); mDrawable.setDecodeDimensions(mMiniDrawerAvatarDecodeSize, mMiniDrawerAvatarDecodeSize); view.setImageDrawable(mDrawable); } public void setAccount(Account acct) { mAccount = acct; mDrawable.bind(mAccount.getSenderName(), mAccount.getEmailAddress()); String contentDescription = mAccount.getDisplayName(); if (TextUtils.isEmpty(contentDescription)) { contentDescription = mAccount.getEmailAddress(); } view.setContentDescription(contentDescription); } @Override public void onClick(View v) { onAccountSelected(mAccount); } } } protected void setupMiniDrawerAccountsAdapter() { mMiniDrawerAccountsAdapter = new MiniDrawerAccountsAdapter(); } protected ListAdapter getMiniDrawerAccountsAdapter() { return mMiniDrawerAccountsAdapter; } private static class FadeAnimatorListener extends AnimatorListenerAdapter { private boolean mCanceled; private final View mView; private final boolean mFadeOut; FadeAnimatorListener(View v, boolean fadeOut) { mView = v; mFadeOut = fadeOut; } @Override public void onAnimationStart(Animator animation) { if (!mFadeOut) { mView.setVisibility(View.VISIBLE); } mCanceled = false; } @Override public void onAnimationCancel(Animator animation) { mCanceled = true; } @Override public void onAnimationEnd(Animator animation) { if (!mCanceled) { // Only need to set visibility to INVISIBLE for fade-out and not fade-in. if (mFadeOut) { mView.setVisibility(View.INVISIBLE); } // If the animation is canceled, then the next animation onAnimationEnd will disable // the hardware layer. mView.setLayerType(View.LAYER_TYPE_NONE, null); } } } }