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