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