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