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