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