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