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