FolderListFragment.java revision 4c91a8ce54b7874309ff1d4cb8c9c439fe308375
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 onDrawerDragStarted() { 846 Utils.enableHardwareLayer(mMiniDrawerView); 847 Utils.enableHardwareLayer(mListView); 848 // The drawer drag will always end with animating the drawers to their final states, so 849 // the animation will remove the hardware layer upon completion. 850 } 851 852 public void onDrawerDrag(float percent) { 853 mMiniDrawerView.setAlpha(1f - percent); 854 mListView.setAlpha(percent); 855 mMiniDrawerView.setVisibility(View.VISIBLE); 856 mListView.setVisibility(View.VISIBLE); 857 } 858 859 /** 860 * Interface for all cursor adapters that allow setting a cursor and being destroyed. 861 */ 862 private interface FolderListFragmentCursorAdapter extends ListAdapter { 863 /** Update the folder list cursor with the cursor given here. */ 864 void setCursor(ObjectCursor<Folder> cursor); 865 ObjectCursor<Folder> getCursor(); 866 /** Update the all folder list cursor with the cursor given here. */ 867 void setAllFolderListCursor(ObjectCursor<Folder> cursor); 868 /** Remove all observers and destroy the object. */ 869 void destroy(); 870 /** Notifies the adapter that the data has changed. */ 871 void notifyDataSetChanged(); 872 } 873 874 /** 875 * An adapter for flat folder lists. 876 */ 877 private class FolderAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter { 878 879 private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() { 880 @Override 881 public void onChanged() { 882 if (!isCursorInvalid()) { 883 rebuildFolderList(); 884 } 885 } 886 }; 887 /** No resource used for string header in folder list */ 888 private static final int BLANK_HEADER_RESOURCE = -1; 889 /** Cache of most recently used folders */ 890 private final RecentFolderList mRecentFolders; 891 /** True if the list is divided, false otherwise. See the comment on 892 * {@link FolderListFragment#mIsDivided} for more information */ 893 private final boolean mIsDivided; 894 /** All the items */ 895 private List<DrawerItem> mItemList = new ArrayList<>(); 896 /** Cursor into the folder list. This might be null. */ 897 private ObjectCursor<Folder> mCursor = null; 898 /** Cursor into the all folder list. This might be null. */ 899 private ObjectCursor<Folder> mAllFolderListCursor = null; 900 901 /** 902 * Creates a {@link FolderAdapter}. This is a list of all the accounts and folders. 903 * 904 * @param isDivided true if folder list is flat, false if divided by label group. See 905 * the comments on {@link #mIsDivided} for more information 906 */ 907 public FolderAdapter(boolean isDivided) { 908 super(); 909 mIsDivided = isDivided; 910 final RecentFolderController controller = mActivity.getRecentFolderController(); 911 if (controller != null && mIsDivided) { 912 mRecentFolders = mRecentFolderObserver.initialize(controller); 913 } else { 914 mRecentFolders = null; 915 } 916 } 917 918 @Override 919 public View getView(int position, View convertView, ViewGroup parent) { 920 final DrawerItem item = (DrawerItem) getItem(position); 921 final View view = item.getView(convertView, parent); 922 final @DrawerItem.DrawerItemType int type = item.getType(); 923 final boolean isSelected = 924 item.isHighlighted(mSelectedFolderUri, mSelectedDrawerItemCategory); 925 if (type == DrawerItem.VIEW_FOLDER) { 926 mListView.setItemChecked((mAccountsAdapter != null ? 927 mAccountsAdapter.getCount() : 0) + 928 position + mListView.getHeaderViewsCount(), isSelected); 929 } 930 // If this is the current folder, also check to verify that the unread count 931 // matches what the action bar shows. 932 if (type == DrawerItem.VIEW_FOLDER 933 && isSelected 934 && (mCurrentFolderForUnreadCheck != null) 935 && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) { 936 ((FolderItemView) view).overrideUnreadCount( 937 mCurrentFolderForUnreadCheck.unreadCount); 938 } 939 return view; 940 } 941 942 @Override 943 public int getViewTypeCount() { 944 // Accounts, headers, folders (all parts of drawer view types) 945 return DrawerItem.getViewTypeCount(); 946 } 947 948 @Override 949 public int getItemViewType(int position) { 950 return ((DrawerItem) getItem(position)).getType(); 951 } 952 953 @Override 954 public int getCount() { 955 return mItemList.size(); 956 } 957 958 @Override 959 public boolean isEnabled(int position) { 960 final DrawerItem drawerItem = ((DrawerItem) getItem(position)); 961 return drawerItem != null && drawerItem.isItemEnabled(); 962 } 963 964 @Override 965 public boolean areAllItemsEnabled() { 966 // We have headers and thus some items are not enabled. 967 return false; 968 } 969 970 /** 971 * Returns all the recent folders from the list given here. Safe to call with a null list. 972 * @param recentList a list of all recently accessed folders. 973 * @return a valid list of folders, which are all recent folders. 974 */ 975 private List<Folder> getRecentFolders(RecentFolderList recentList) { 976 final List<Folder> folderList = new ArrayList<>(); 977 if (recentList == null) { 978 return folderList; 979 } 980 // Get all recent folders, after removing system folders. 981 for (final Folder f : recentList.getRecentFolderList(null)) { 982 if (!f.isProviderFolder()) { 983 folderList.add(f); 984 } 985 } 986 return folderList; 987 } 988 989 /** 990 * Responsible for verifying mCursor, and ensuring any recalculate 991 * conditions are met. Also calls notifyDataSetChanged once it's finished 992 * populating {@link com.android.mail.ui.FolderListFragment.FolderAdapter#mItemList} 993 */ 994 private void rebuildFolderList() { 995 final boolean oldInboxPresent = mInboxPresent; 996 mItemList = recalculateListFolders(); 997 if (mAccountController != null && mInboxPresent && !oldInboxPresent) { 998 // We didn't have an inbox folder before, but now we do. This can occur when 999 // setting up a new account. We automatically create the "starred" virtual 1000 // virtual folder, but we won't create the inbox until it gets synced. 1001 // This means that we'll start out looking at the "starred" folder, and the 1002 // user will need to manually switch to the inbox. See b/13793316 1003 mAccountController.switchToDefaultInboxOrChangeAccount(mCurrentAccount); 1004 } 1005 // Ask the list to invalidate its views. 1006 notifyDataSetChanged(); 1007 } 1008 1009 /** 1010 * Recalculates the system, recent and user label lists. 1011 * This method modifies all the three lists on every single invocation. 1012 */ 1013 private List<DrawerItem> recalculateListFolders() { 1014 final List<DrawerItem> itemList = new ArrayList<>(); 1015 // If we are waiting for folder initialization, we don't have any kinds of folders, 1016 // just the "Waiting for initialization" item. Note, this should only be done 1017 // when we're waiting for account initialization or initial sync. 1018 if (isCursorInvalid()) { 1019 if(!mCurrentAccount.isAccountReady()) { 1020 itemList.add(DrawerItem.ofWaitView(mActivity)); 1021 } 1022 return itemList; 1023 } 1024 if (mIsDivided) { 1025 //Choose an adapter for a divided list with sections 1026 return recalculateDividedListFolders(itemList); 1027 } else { 1028 // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers. 1029 return recalculateFlatListFolders(itemList); 1030 } 1031 } 1032 1033 // Recalculate folder list intended to be flat (no hearders or sections shown). 1034 // This is commonly used for the widget or other simple folder selections 1035 private List<DrawerItem> recalculateFlatListFolders(List<DrawerItem> itemList) { 1036 final List<DrawerItem> inboxFolders = new ArrayList<>(); 1037 final List<DrawerItem> allFoldersList = new ArrayList<>(); 1038 do { 1039 final Folder f = mCursor.getModel(); 1040 if (!isFolderTypeExcluded(f)) { 1041 // Prioritize inboxes 1042 if (f.isInbox()) { 1043 inboxFolders.add(DrawerItem.ofFolder( 1044 mActivity, f, DrawerItem.FOLDER_OTHER)); 1045 } else { 1046 allFoldersList.add( 1047 DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_OTHER)); 1048 } 1049 } 1050 } while (mCursor.moveToNext()); 1051 itemList.addAll(inboxFolders); 1052 itemList.addAll(allFoldersList); 1053 return itemList; 1054 } 1055 1056 // Recalculate folder list divided by sections (inboxes, recents, all, etc...) 1057 // This is primarily used by the drawer 1058 private List<DrawerItem> recalculateDividedListFolders(List<DrawerItem> itemList) { 1059 final List<DrawerItem> allFoldersList = new ArrayList<>(); 1060 final List<DrawerItem> inboxFolders = new ArrayList<>(); 1061 do { 1062 final Folder f = mCursor.getModel(); 1063 if (!isFolderTypeExcluded(f)) { 1064 if (f.isInbox()) { 1065 inboxFolders.add(DrawerItem.ofFolder( 1066 mActivity, f, DrawerItem.FOLDER_INBOX)); 1067 } else { 1068 allFoldersList.add(DrawerItem.ofFolder( 1069 mActivity, f, DrawerItem.FOLDER_OTHER)); 1070 } 1071 } 1072 } while (mCursor.moveToNext()); 1073 1074 // If we have the all folder list, verify that the current folder exists 1075 boolean currentFolderFound = false; 1076 if (mAllFolderListCursor != null) { 1077 final String folderName = mSelectedFolderUri.toString(); 1078 LogUtils.d(LOG_TAG, "Checking if all folder list contains %s", folderName); 1079 1080 if (mAllFolderListCursor.moveToFirst()) { 1081 LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName); 1082 do { 1083 final Folder f = mAllFolderListCursor.getModel(); 1084 if (!isFolderTypeExcluded(f)) { 1085 if (f.folderUri.equals(mSelectedFolderUri)) { 1086 LogUtils.d(LOG_TAG, "Found %s !", folderName); 1087 currentFolderFound = true; 1088 } 1089 } 1090 } while (!currentFolderFound && mAllFolderListCursor.moveToNext()); 1091 } 1092 1093 // The search folder will not be found here because it is excluded from the drawer. 1094 // Don't switch off from the current folder if it's search. 1095 if (!currentFolderFound && !Folder.isType(FolderType.SEARCH, mSelectedFolderType) 1096 && mSelectedFolderUri != FolderUri.EMPTY 1097 && mCurrentAccount != null && mAccountController != null 1098 && mAccountController.isDrawerPullEnabled()) { 1099 LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s", 1100 folderName, mCurrentAccount.getEmailAddress()); 1101 changeAccount(mCurrentAccount); 1102 } 1103 } 1104 1105 mInboxPresent = (inboxFolders.size() > 0); 1106 1107 // Add all inboxes (sectioned Inboxes included) before recent folders. 1108 addFolderDivision(itemList, inboxFolders, BLANK_HEADER_RESOURCE); 1109 1110 // Add recent folders next. 1111 addRecentsToList(itemList); 1112 1113 // Add the remaining folders. 1114 addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading); 1115 1116 return itemList; 1117 } 1118 1119 /** 1120 * Given a list of folders as {@link DrawerItem}s, add them as a group. 1121 * Passing in a non-0 integer for the resource will enable a header. 1122 * 1123 * @param destination List of drawer items to populate 1124 * @param source List of drawer items representing folders to add to the drawer 1125 * @param headerStringResource 1126 * {@link FolderAdapter#BLANK_HEADER_RESOURCE} if no header text 1127 * is required, or res-id otherwise. The integer is interpreted as the string 1128 * for the header's title. 1129 */ 1130 private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source, 1131 int headerStringResource) { 1132 if (source.size() > 0) { 1133 if(headerStringResource != BLANK_HEADER_RESOURCE) { 1134 destination.add(DrawerItem.ofHeader(mActivity, headerStringResource)); 1135 } else { 1136 destination.add(DrawerItem.ofBlankHeader(mActivity)); 1137 } 1138 destination.addAll(source); 1139 } 1140 } 1141 1142 /** 1143 * Add recent folders to the list in order as acquired by the {@link RecentFolderList}. 1144 * 1145 * @param destination List of drawer items to populate 1146 */ 1147 private void addRecentsToList(List<DrawerItem> destination) { 1148 // If there are recent folders, add them. 1149 final List<Folder> recentFolderList = getRecentFolders(mRecentFolders); 1150 1151 // Remove any excluded folder types 1152 if (mExcludedFolderTypes != null) { 1153 final Iterator<Folder> iterator = recentFolderList.iterator(); 1154 while (iterator.hasNext()) { 1155 if (isFolderTypeExcluded(iterator.next())) { 1156 iterator.remove(); 1157 } 1158 } 1159 } 1160 1161 if (recentFolderList.size() > 0) { 1162 destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading)); 1163 // Recent folders are not queried for position. 1164 for (Folder f : recentFolderList) { 1165 destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT)); 1166 } 1167 } 1168 } 1169 1170 /** 1171 * Check if the cursor provided is valid. 1172 * @return True if cursor is invalid, false otherwise 1173 */ 1174 private boolean isCursorInvalid() { 1175 return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0 1176 || !mCursor.moveToFirst(); 1177 } 1178 1179 @Override 1180 public void setCursor(ObjectCursor<Folder> cursor) { 1181 mCursor = cursor; 1182 rebuildAccountList(); 1183 rebuildFolderList(); 1184 } 1185 1186 @Override 1187 public ObjectCursor<Folder> getCursor() { 1188 return mCursor; 1189 } 1190 1191 @Override 1192 public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) { 1193 mAllFolderListCursor = cursor; 1194 rebuildAccountList(); 1195 rebuildFolderList(); 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 >= mItemList.size()) { 1202 return null; 1203 } else { 1204 return mItemList.get(position); 1205 } 1206 } 1207 1208 @Override 1209 public long getItemId(int position) { 1210 return getItem(position).hashCode(); 1211 } 1212 1213 @Override 1214 public final void destroy() { 1215 mRecentFolderObserver.unregisterAndDestroy(); 1216 } 1217 } 1218 1219 private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder> 1220 implements FolderListFragmentCursorAdapter { 1221 1222 private static final int PARENT = 0; 1223 private static final int CHILD = 1; 1224 private final FolderUri mParentUri; 1225 private final Folder mParent; 1226 1227 public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) { 1228 super(mActivity.getActivityContext(), R.layout.folder_item); 1229 mParent = parentFolder; 1230 mParentUri = parentFolder.folderUri; 1231 setCursor(c); 1232 } 1233 1234 @Override 1235 public int getViewTypeCount() { 1236 // Child and Parent 1237 return 2; 1238 } 1239 1240 @Override 1241 public int getItemViewType(int position) { 1242 final Folder f = getItem(position); 1243 return f.folderUri.equals(mParentUri) ? PARENT : CHILD; 1244 } 1245 1246 @Override 1247 public View getView(int position, View convertView, ViewGroup parent) { 1248 final FolderItemView folderItemView; 1249 final Folder folder = getItem(position); 1250 1251 if (convertView != null) { 1252 folderItemView = (FolderItemView) convertView; 1253 } else { 1254 folderItemView = (FolderItemView) LayoutInflater.from( 1255 mActivity.getActivityContext()).inflate(R.layout.folder_item, null); 1256 } 1257 folderItemView.bind(folder, mParentUri); 1258 1259 if (folder.folderUri.equals(mSelectedFolderUri)) { 1260 final ListView listView = getListView(); 1261 listView.setItemChecked((mAccountsAdapter != null ? 1262 mAccountsAdapter.getCount() : 0) + 1263 position + listView.getHeaderViewsCount(), true); 1264 // If this is the current folder, also check to verify that the unread count 1265 // matches what the action bar shows. 1266 final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null) 1267 && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount; 1268 if (unreadCountDiffers) { 1269 folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount); 1270 } 1271 } 1272 Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block)); 1273 Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon)); 1274 return folderItemView; 1275 } 1276 1277 @Override 1278 public void setCursor(ObjectCursor<Folder> cursor) { 1279 clear(); 1280 if (mParent != null) { 1281 add(mParent); 1282 } 1283 if (cursor != null && cursor.getCount() > 0) { 1284 cursor.moveToFirst(); 1285 do { 1286 add(cursor.getModel()); 1287 } while (cursor.moveToNext()); 1288 } 1289 } 1290 1291 @Override 1292 public ObjectCursor<Folder> getCursor() { 1293 throw new UnsupportedOperationException("drawers don't have hierarchical folders"); 1294 } 1295 1296 @Override 1297 public void setAllFolderListCursor(final ObjectCursor<Folder> cursor) { 1298 // Not necessary in HierarchicalFolderListAdapter 1299 } 1300 1301 @Override 1302 public void destroy() { 1303 // Do nothing. 1304 } 1305 } 1306 1307 public void rebuildAccountList() { 1308 if (!mIsFolderSelectionActivity) { 1309 if (mAccountsAdapter != null) { 1310 mAccountsAdapter.setAccounts(buildAccountListDrawerItems()); 1311 } 1312 if (mMiniDrawerAccountsAdapter != null) { 1313 mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount); 1314 } 1315 } 1316 } 1317 1318 protected static class AccountsAdapter extends BaseAdapter { 1319 1320 private List<DrawerItem> mAccounts; 1321 1322 public AccountsAdapter() { 1323 mAccounts = new ArrayList<>(); 1324 } 1325 1326 public void setAccounts(List<DrawerItem> accounts) { 1327 mAccounts = accounts; 1328 notifyDataSetChanged(); 1329 } 1330 1331 @Override 1332 public int getCount() { 1333 return mAccounts.size(); 1334 } 1335 1336 @Override 1337 public Object getItem(int position) { 1338 // Is there an attempt made to access outside of the drawer item list? 1339 if (position >= mAccounts.size()) { 1340 return null; 1341 } else { 1342 return mAccounts.get(position); 1343 } 1344 } 1345 1346 @Override 1347 public long getItemId(int position) { 1348 return getItem(position).hashCode(); 1349 } 1350 1351 @Override 1352 public View getView(int position, View convertView, ViewGroup parent) { 1353 final DrawerItem item = (DrawerItem) getItem(position); 1354 return item.getView(convertView, parent); 1355 } 1356 } 1357 1358 /** 1359 * Builds the drawer items for the list of accounts. 1360 */ 1361 private List<DrawerItem> buildAccountListDrawerItems() { 1362 final Account[] allAccounts = getAllAccounts(); 1363 final List<DrawerItem> accountList = new ArrayList<>(allAccounts.length); 1364 // Add all accounts and then the current account 1365 final Uri currentAccountUri = getCurrentAccountUri(); 1366 for (final Account account : allAccounts) { 1367 final int unreadCount = getUnreadCount(account); 1368 accountList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, 1369 currentAccountUri.equals(account.uri), mImagesCache, mContactResolver)); 1370 } 1371 if (mCurrentAccount == null) { 1372 LogUtils.wtf(LOG_TAG, "buildAccountListDrawerItems() with null current account."); 1373 } 1374 return accountList; 1375 } 1376 1377 private Uri getCurrentAccountUri() { 1378 return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri; 1379 } 1380 1381 protected String getCurrentAccountEmailAddress() { 1382 return mCurrentAccount == null ? "" : mCurrentAccount.getEmailAddress(); 1383 } 1384 1385 protected MergedAdapter<ListAdapter> getMergedAdapter() { 1386 return mMergedAdapter; 1387 } 1388 1389 public ObjectCursor<Folder> getFoldersCursor() { 1390 return (mFolderAdapter != null) ? mFolderAdapter.getCursor() : null; 1391 } 1392 1393 private class FooterAdapter extends BaseAdapter { 1394 1395 private final List<DrawerItem> mFooterItems = Lists.newArrayList(); 1396 1397 private FooterAdapter() { 1398 update(); 1399 } 1400 1401 @Override 1402 public int getCount() { 1403 return mFooterItems.size(); 1404 } 1405 1406 @Override 1407 public DrawerItem getItem(int position) { 1408 return mFooterItems.get(position); 1409 } 1410 1411 @Override 1412 public long getItemId(int position) { 1413 return position; 1414 } 1415 1416 @Override 1417 public int getViewTypeCount() { 1418 // Accounts, headers, folders (all parts of drawer view types) 1419 return DrawerItem.getViewTypeCount(); 1420 } 1421 1422 @Override 1423 public int getItemViewType(int position) { 1424 return getItem(position).getType(); 1425 } 1426 1427 /** 1428 * @param convertView a view, possibly null, to be recycled. 1429 * @param parent the parent hosting this view. 1430 * @return a view for the footer item displaying the given text and image. 1431 */ 1432 @Override 1433 public View getView(int position, View convertView, ViewGroup parent) { 1434 return getItem(position).getView(convertView, parent); 1435 } 1436 1437 /** 1438 * Recomputes the footer drawer items depending on whether the current account 1439 * is populated with URIs that navigate to appropriate destinations. 1440 */ 1441 private void update() { 1442 // if the parent activity shows a drawer, these items should participate in that drawer 1443 // (if it shows a *pane* they should *not* participate in that pane) 1444 if (mIsFolderSelectionActivity) { 1445 return; 1446 } 1447 1448 mFooterItems.clear(); 1449 1450 if (mCurrentAccount != null) { 1451 mFooterItems.add(DrawerItem.ofSettingsItem(mActivity, mCurrentAccount, 1452 mDrawerListener)); 1453 } 1454 1455 if (mCurrentAccount != null && !Utils.isEmpty(mCurrentAccount.helpIntentUri)) { 1456 mFooterItems.add(DrawerItem.ofHelpItem(mActivity, mCurrentAccount, 1457 mDrawerListener)); 1458 } 1459 1460 if (!mFooterItems.isEmpty()) { 1461 mFooterItems.add(0, DrawerItem.ofBlankHeader(mActivity)); 1462 mFooterItems.add(DrawerItem.ofBottomSpace(mActivity)); 1463 } 1464 1465 notifyDataSetChanged(); 1466 } 1467 } 1468 1469 /** 1470 * Sets the currently selected folder safely. 1471 * @param folder the folder to change to. It is an error to pass null here. 1472 */ 1473 private void setSelectedFolder(Folder folder) { 1474 if (folder == null) { 1475 mSelectedFolderUri = FolderUri.EMPTY; 1476 mCurrentFolderForUnreadCheck = null; 1477 LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!"); 1478 return; 1479 } 1480 1481 final boolean viewChanged = 1482 !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck); 1483 1484 // There are two cases in which the folder type is not set by this class. 1485 // 1. The activity starts up: from notification/widget/shortcut/launcher. Then we have a 1486 // folder but its type was never set. 1487 // 2. The user backs into the default inbox. Going 'back' from the conversation list of 1488 // any folder will take you to the default inbox for that account. (If you are in the 1489 // default inbox already, back exits the app.) 1490 // In both these cases, the selected folder type is not set, and must be set. 1491 if (mSelectedDrawerItemCategory == DrawerItem.UNSET || (mCurrentAccount != null 1492 && folder.folderUri.equals(mCurrentAccount.settings.defaultInbox))) { 1493 mSelectedDrawerItemCategory = 1494 folder.isInbox() ? DrawerItem.FOLDER_INBOX : DrawerItem.FOLDER_OTHER; 1495 mSelectedFolderType = folder.type; 1496 } 1497 1498 mCurrentFolderForUnreadCheck = folder; 1499 mSelectedFolderUri = folder.folderUri; 1500 if (viewChanged) { 1501 if (mFolderAdapter != null) { 1502 mFolderAdapter.notifyDataSetChanged(); 1503 } 1504 if (mMiniDrawerView != null) { 1505 mMiniDrawerView.refresh(); 1506 } 1507 } 1508 } 1509 1510 public boolean isSelectedFolder(@NonNull Folder folder) { 1511 return folder.folderUri.equals(mSelectedFolderUri); 1512 } 1513 1514 /** 1515 * Sets the current account to the one provided here. 1516 * @param account the current account to set to. 1517 */ 1518 private void setSelectedAccount(Account account) { 1519 final boolean changed = (account != null) && (mCurrentAccount == null 1520 || !mCurrentAccount.uri.equals(account.uri)); 1521 mCurrentAccount = account; 1522 if (changed) { 1523 // Verify that the new account supports sending application feedback 1524 updateFooterItems(); 1525 // We no longer have proper folder objects. Let the new ones come in 1526 mFolderAdapter.setCursor(null); 1527 // If currentAccount is different from the one we set, restart the loader. Look at the 1528 // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we 1529 // don't just do restartLoader. 1530 final LoaderManager manager = getLoaderManager(); 1531 manager.destroyLoader(FOLDER_LIST_LOADER_ID); 1532 manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this); 1533 manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID); 1534 manager.restartLoader(ALL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this); 1535 // An updated cursor causes the entire list to refresh. No need to refresh the list. 1536 // But we do need to blank out the current folder, since the account might not be 1537 // synced. 1538 mSelectedFolderUri = FolderUri.EMPTY; 1539 mCurrentFolderForUnreadCheck = null; 1540 1541 // also set/update the mini-drawer 1542 if (mMiniDrawerAccountsAdapter != null) { 1543 mMiniDrawerAccountsAdapter.setAccounts(getAllAccounts(), mCurrentAccount); 1544 } 1545 1546 } else if (account == null) { 1547 // This should never happen currently, but is a safeguard against a very incorrect 1548 // non-null account -> null account transition. 1549 LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader."); 1550 final LoaderManager manager = getLoaderManager(); 1551 manager.destroyLoader(FOLDER_LIST_LOADER_ID); 1552 manager.destroyLoader(ALL_FOLDER_LIST_LOADER_ID); 1553 } 1554 } 1555 1556 private void updateFooterItems() { 1557 mFooterAdapter.update(); 1558 } 1559 1560 /** 1561 * Checks if the specified {@link Folder} is a type that we want to exclude from displaying. 1562 */ 1563 private boolean isFolderTypeExcluded(final Folder folder) { 1564 if (mExcludedFolderTypes == null) { 1565 return false; 1566 } 1567 1568 for (final int excludedType : mExcludedFolderTypes) { 1569 if (folder.isType(excludedType)) { 1570 return true; 1571 } 1572 } 1573 1574 return false; 1575 } 1576 1577 /** 1578 * @return the choice mode to use for the {@link ListView} 1579 */ 1580 protected int getListViewChoiceMode() { 1581 return mAccountController.getFolderListViewChoiceMode(); 1582 } 1583 1584 1585 /** 1586 * Drawer listener for footer functionality to react to drawer state. 1587 */ 1588 public class DrawerStateListener implements DrawerLayout.DrawerListener { 1589 1590 private FooterItem mPendingFooterClick; 1591 1592 public void setPendingFooterClick(FooterItem itemClicked) { 1593 mPendingFooterClick = itemClicked; 1594 } 1595 1596 @Override 1597 public void onDrawerSlide(View drawerView, float slideOffset) {} 1598 1599 @Override 1600 public void onDrawerOpened(View drawerView) {} 1601 1602 @Override 1603 public void onDrawerClosed(View drawerView) { 1604 if (mPendingFooterClick != null) { 1605 mPendingFooterClick.onFooterClicked(); 1606 mPendingFooterClick = null; 1607 } 1608 } 1609 1610 @Override 1611 public void onDrawerStateChanged(int newState) {} 1612 1613 } 1614 1615 private class FolderOrAccountListener extends DataSetObserver { 1616 1617 @Override 1618 public void onChanged() { 1619 // First, check if there's a folder to change to 1620 if (mNextFolder != null) { 1621 mFolderChanger.onFolderSelected(mNextFolder); 1622 mNextFolder = null; 1623 } 1624 // Next, check if there's an account to change to 1625 if (mNextAccount != null) { 1626 mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount); 1627 mNextAccount = null; 1628 } 1629 } 1630 } 1631 1632 @Override 1633 public ListAdapter getListAdapter() { 1634 // Ensures that we get the adapter with the header views. 1635 throw new UnsupportedOperationException("Use getListView().getAdapter() instead " 1636 + "which accounts for any header or footer views."); 1637 } 1638 1639 protected class MiniDrawerAccountsAdapter extends BaseAdapter { 1640 1641 private List<Account> mAccounts = new ArrayList<>(); 1642 1643 public void setAccounts(Account[] accounts, Account currentAccount) { 1644 mAccounts.clear(); 1645 if (currentAccount == null) { 1646 notifyDataSetChanged(); 1647 return; 1648 } 1649 mAccounts.add(currentAccount); 1650 // TODO: sort by most recent accounts 1651 for (final Account account : accounts) { 1652 if (!account.getEmailAddress().equals(currentAccount.getEmailAddress())) { 1653 mAccounts.add(account); 1654 } 1655 } 1656 notifyDataSetChanged(); 1657 } 1658 1659 @Override 1660 public int getCount() { 1661 return mAccounts.size(); 1662 } 1663 1664 @Override 1665 public Object getItem(int position) { 1666 // Is there an attempt made to access outside of the drawer item list? 1667 if (position >= mAccounts.size()) { 1668 return null; 1669 } else { 1670 return mAccounts.get(position); 1671 } 1672 } 1673 1674 @Override 1675 public long getItemId(int position) { 1676 return getItem(position).hashCode(); 1677 } 1678 1679 @Override 1680 public View getView(int position, View convertView, ViewGroup parent) { 1681 final ImageView iv = convertView != null ? (ImageView) convertView : 1682 (ImageView) LayoutInflater.from(getActivity()).inflate( 1683 R.layout.mini_drawer_recent_account_item, parent, false /* attachToRoot */); 1684 final MiniDrawerAccountItem item = new MiniDrawerAccountItem(iv); 1685 item.setupDrawable(); 1686 item.setAccount(mAccounts.get(position)); 1687 iv.setTag(item); 1688 return iv; 1689 } 1690 1691 private class MiniDrawerAccountItem implements View.OnClickListener { 1692 private Account mAccount; 1693 private AccountAvatarDrawable mDrawable; 1694 public final ImageView view; 1695 1696 public MiniDrawerAccountItem(ImageView iv) { 1697 view = iv; 1698 view.setOnClickListener(this); 1699 } 1700 1701 public void setupDrawable() { 1702 mDrawable = new AccountAvatarDrawable(getResources(), getBitmapCache(), 1703 getContactResolver()); 1704 mDrawable.setDecodeDimensions(mMiniDrawerAvatarDecodeSize, 1705 mMiniDrawerAvatarDecodeSize); 1706 view.setImageDrawable(mDrawable); 1707 } 1708 1709 public void setAccount(Account acct) { 1710 mAccount = acct; 1711 mDrawable.bind(mAccount.getSenderName(), mAccount.getEmailAddress()); 1712 String contentDescription = mAccount.getDisplayName(); 1713 if (TextUtils.isEmpty(contentDescription)) { 1714 contentDescription = mAccount.getEmailAddress(); 1715 } 1716 view.setContentDescription(contentDescription); 1717 } 1718 1719 @Override 1720 public void onClick(View v) { 1721 onAccountSelected(mAccount); 1722 } 1723 } 1724 } 1725 1726 protected void setupMiniDrawerAccountsAdapter() { 1727 mMiniDrawerAccountsAdapter = new MiniDrawerAccountsAdapter(); 1728 } 1729 1730 protected ListAdapter getMiniDrawerAccountsAdapter() { 1731 return mMiniDrawerAccountsAdapter; 1732 } 1733 1734 private static class FadeAnimatorListener extends AnimatorListenerAdapter { 1735 private boolean mCanceled; 1736 private final View mView; 1737 private final boolean mFadeOut; 1738 1739 FadeAnimatorListener(View v, boolean fadeOut) { 1740 mView = v; 1741 mFadeOut = fadeOut; 1742 } 1743 1744 @Override 1745 public void onAnimationStart(Animator animation) { 1746 if (!mFadeOut) { 1747 mView.setVisibility(View.VISIBLE); 1748 } 1749 mCanceled = false; 1750 } 1751 1752 @Override 1753 public void onAnimationCancel(Animator animation) { 1754 mCanceled = true; 1755 } 1756 1757 @Override 1758 public void onAnimationEnd(Animator animation) { 1759 if (!mCanceled) { 1760 // Only need to set visibility to INVISIBLE for fade-out and not fade-in. 1761 if (mFadeOut) { 1762 mView.setVisibility(View.INVISIBLE); 1763 } 1764 // If the animation is canceled, then the next animation onAnimationEnd will disable 1765 // the hardware layer. 1766 mView.setLayerType(View.LAYER_TYPE_NONE, null); 1767 } 1768 } 1769 } 1770 1771} 1772