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