FolderListFragment.java revision b9c23da3c3d0eebf980552a0f95bbee4e11368af
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.app.Activity; 21import android.app.ListFragment; 22import android.app.LoaderManager; 23import android.content.ContentResolver; 24import android.content.ContentUris; 25import android.content.Context; 26import android.content.Loader; 27import android.database.Cursor; 28import android.graphics.Bitmap; 29import android.net.Uri; 30import android.os.AsyncTask; 31import android.os.Bundle; 32import android.provider.ContactsContract; 33import android.provider.ContactsContract.Data; 34import android.provider.ContactsContract.CommonDataKinds.Email; 35import android.provider.ContactsContract.Contacts.Photo; 36import android.text.TextUtils; 37import android.view.LayoutInflater; 38import android.view.View; 39import android.view.ViewGroup; 40import android.widget.ArrayAdapter; 41import android.widget.BaseAdapter; 42import android.widget.ImageView; 43import android.widget.ListAdapter; 44import android.widget.ListView; 45 46import com.android.mail.R; 47import com.android.mail.adapter.DrawerItem; 48import com.android.mail.browse.LetterTileUtils; 49import com.android.mail.content.ObjectCursor; 50import com.android.mail.content.ObjectCursorLoader; 51import com.android.mail.photomanager.BitmapUtil; 52import com.android.mail.providers.Account; 53import com.android.mail.providers.AccountObserver; 54import com.android.mail.providers.AllAccountObserver; 55import com.android.mail.providers.Folder; 56import com.android.mail.providers.FolderObserver; 57import com.android.mail.providers.FolderWatcher; 58import com.android.mail.providers.RecentFolderObserver; 59import com.android.mail.providers.UIProvider; 60import com.android.mail.providers.UIProvider.FolderType; 61import com.android.mail.utils.LogTag; 62import com.android.mail.utils.LogUtils; 63import com.google.common.collect.ImmutableSet; 64import com.android.mail.utils.Utils; 65 66import java.util.ArrayList; 67import java.util.Arrays; 68import java.util.HashMap; 69import java.util.HashSet; 70import java.util.Iterator; 71import java.util.List; 72import java.util.Set; 73 74/** 75 * The folder list UI component. 76 */ 77public class FolderListFragment extends ListFragment implements 78 LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> { 79 private static final String LOG_TAG = LogTag.getLogTag(); 80 /** The parent activity */ 81 private ControllableActivity mActivity; 82 /** The underlying list view */ 83 private ListView mListView; 84 /** URI that points to the list of folders for the current account. */ 85 private Uri mFolderListUri; 86 /** True if you want a sectioned FolderList, false otherwise. */ 87 protected boolean mIsSectioned; 88 /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */ 89 private ArrayList<Integer> mExcludedFolderTypes; 90 /** Object that changes folders on our behalf. */ 91 private FolderListSelectionListener mFolderChanger; 92 /** Object that changes accounts on our behalf */ 93 private AccountController mAccountChanger; 94 95 /** The currently selected folder (the folder being viewed). This is never null. */ 96 private Uri mSelectedFolderUri = Uri.EMPTY; 97 /** 98 * The current folder from the controller. This is meant only to check when the unread count 99 * goes out of sync and fixing it. 100 */ 101 private Folder mCurrentFolderForUnreadCheck; 102 /** Parent of the current folder, or null if the current folder is not a child. */ 103 private Folder mParentFolder; 104 105 private static final int FOLDER_LOADER_ID = 0; 106 /** Key to store {@link #mParentFolder}. */ 107 private static final String ARG_PARENT_FOLDER = "arg-parent-folder"; 108 /** Key to store {@link #mIsSectioned} */ 109 private static final String ARG_IS_SECTIONED = "arg-is-sectioned"; 110 /** Key to store {@link #mFolderListUri}. */ 111 private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri"; 112 /** Key to store {@link #mExcludedFolderTypes} */ 113 private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types"; 114 /** Key to store {@link #mType} */ 115 private static final String ARG_TYPE = "arg-flf-type"; 116 117 /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */ 118 private int mType; 119 /** This fragment is a drawer */ 120 private static final int TYPE_DRAWER = 0; 121 /** This fragment is a folder tree */ 122 private static final int TYPE_TREE = 1; 123 124 private static final String BUNDLE_LIST_STATE = "flf-list-state"; 125 private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder"; 126 private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type"; 127 128 private FolderListFragmentCursorAdapter mCursorAdapter; 129 /** Observer to wait for changes to the current folder so we can change the selected folder */ 130 private FolderObserver mFolderObserver = null; 131 /** Listen for account changes. */ 132 private AccountObserver mAccountObserver = null; 133 134 /** Listen to changes to list of all accounts */ 135 private AllAccountObserver mAllAccountsObserver = null; 136 /** 137 * Type of currently selected folder: {@link DrawerItem#FOLDER_SYSTEM}, 138 * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_USER}. 139 */ 140 // Setting to INERT_HEADER = leaving uninitialized. 141 private int mSelectedFolderType = DrawerItem.UNSET; 142 /** The current account according to the controller */ 143 private Account mCurrentAccount; 144 145 /** 146 * Constructor needs to be public to handle orientation changes and activity lifecycle events. 147 */ 148 public FolderListFragment() { 149 super(); 150 } 151 152 @Override 153 public String toString() { 154 final StringBuilder sb = new StringBuilder(super.toString()); 155 sb.setLength(sb.length() - 1); 156 sb.append(" folder="); 157 sb.append(mFolderListUri); 158 sb.append(" parent="); 159 sb.append(mParentFolder); 160 sb.append(" adapterCount="); 161 sb.append(mCursorAdapter != null ? mCursorAdapter.getCount() : -1); 162 sb.append("}"); 163 return sb.toString(); 164 } 165 166 /** 167 * Creates a new instance of {@link FolderListFragment}. Gets the current account and current 168 * folder through observers. 169 */ 170 public static FolderListFragment ofDrawer() { 171 final FolderListFragment fragment = new FolderListFragment(); 172 // The drawer is always sectioned 173 final boolean isSectioned = true; 174 fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null)); 175 return fragment; 176 } 177 178 /** 179 * Creates a new instance of {@link FolderListFragment}, initialized 180 * to display the folder and its immediate children. 181 * @param folder parent folder whose children are shown 182 * 183 */ 184 public static FolderListFragment ofTree(Folder folder) { 185 final FolderListFragment fragment = new FolderListFragment(); 186 // Trees are never sectioned. 187 final boolean isSectioned = false; 188 fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri, 189 isSectioned, null)); 190 return fragment; 191 } 192 193 /** 194 * Creates a new instance of {@link FolderListFragment}, initialized 195 * to display the folder and its immediate children. 196 * @param folderListUri the URI which contains all the list of folders 197 * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying 198 */ 199 public static FolderListFragment ofTopLevelTree(Uri folderListUri, 200 final ArrayList<Integer> excludedFolderTypes) { 201 final FolderListFragment fragment = new FolderListFragment(); 202 // Trees are never sectioned. 203 final boolean isSectioned = false; 204 fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri, 205 isSectioned, excludedFolderTypes)); 206 return fragment; 207 } 208 209 /** 210 * Construct a bundle that represents the state of this fragment. 211 * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE} 212 * @param parentFolder non-null for trees, the parent of this list 213 * @param isSectioned true if this drawer is sectioned, false otherwise 214 * @param folderListUri the URI which contains all the list of folders 215 * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists. 216 * @return Bundle containing parentFolder, sectioned list boolean and 217 * excluded folder types 218 */ 219 private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri, 220 boolean isSectioned, final ArrayList<Integer> excludedFolderTypes) { 221 final Bundle args = new Bundle(); 222 args.putInt(ARG_TYPE, type); 223 if (parentFolder != null) { 224 args.putParcelable(ARG_PARENT_FOLDER, parentFolder); 225 } 226 if (folderListUri != null) { 227 args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString()); 228 } 229 args.putBoolean(ARG_IS_SECTIONED, isSectioned); 230 if (excludedFolderTypes != null) { 231 args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes); 232 } 233 return args; 234 } 235 236 @Override 237 public void onActivityCreated(Bundle savedState) { 238 super.onActivityCreated(savedState); 239 // Strictly speaking, we get back an android.app.Activity from getActivity. However, the 240 // only activity creating a ConversationListContext is a MailActivity which is of type 241 // ControllableActivity, so this cast should be safe. If this cast fails, some other 242 // activity is creating ConversationListFragments. This activity must be of type 243 // ControllableActivity. 244 final Activity activity = getActivity(); 245 Folder currentFolder = null; 246 if (! (activity instanceof ControllableActivity)){ 247 LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" + 248 "create it. Cannot proceed."); 249 } 250 mActivity = (ControllableActivity) activity; 251 final FolderController controller = mActivity.getFolderController(); 252 // Listen to folder changes in the future 253 mFolderObserver = new FolderObserver() { 254 @Override 255 public void onChanged(Folder newFolder) { 256 setSelectedFolder(newFolder); 257 } 258 }; 259 if (controller != null) { 260 // Only register for selected folder updates if we have a controller. 261 currentFolder = mFolderObserver.initialize(controller); 262 mCurrentFolderForUnreadCheck = currentFolder; 263 } 264 265 // Initialize adapter for folder/heirarchical list. Note this relies on 266 // mActivity being initialized. 267 final Folder selectedFolder; 268 if (mParentFolder != null) { 269 mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder); 270 selectedFolder = mActivity.getHierarchyFolder(); 271 } else { 272 mCursorAdapter = new FolderListAdapter(mIsSectioned); 273 selectedFolder = currentFolder; 274 } 275 // Is the selected folder fresher than the one we have restored from a bundle? 276 if (selectedFolder != null && !selectedFolder.uri.equals(mSelectedFolderUri)) { 277 setSelectedFolder(selectedFolder); 278 } 279 280 // Assign observers for current account & all accounts 281 final AccountController accountController = mActivity.getAccountController(); 282 mAccountObserver = new AccountObserver() { 283 @Override 284 public void onChanged(Account newAccount) { 285 setSelectedAccount(newAccount); 286 } 287 }; 288 if (accountController != null) { 289 // Current account and its observer. 290 setSelectedAccount(mAccountObserver.initialize(accountController)); 291 // List of all accounts and its observer. 292 mAllAccountsObserver = new AllAccountObserver(){ 293 @Override 294 public void onChanged(Account[] allAccounts) { 295 mCursorAdapter.notifyAllAccountsChanged(); 296 } 297 }; 298 mAllAccountsObserver.initialize(accountController); 299 mAccountChanger = accountController; 300 } 301 302 mFolderChanger = mActivity.getFolderListSelectionListener(); 303 if (mActivity.isFinishing()) { 304 // Activity is finishing, just bail. 305 return; 306 } 307 308 setListAdapter(mCursorAdapter); 309 } 310 311 /** 312 * Set the instance variables from the arguments provided here. 313 * @param args 314 */ 315 private void setInstanceFromBundle(Bundle args) { 316 if (args == null) { 317 return; 318 } 319 mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER); 320 final String folderUri = args.getString(ARG_FOLDER_LIST_URI); 321 if (folderUri == null) { 322 mFolderListUri = Uri.EMPTY; 323 } else { 324 mFolderListUri = Uri.parse(folderUri); 325 } 326 mIsSectioned = args.getBoolean(ARG_IS_SECTIONED); 327 mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES); 328 mType = args.getInt(ARG_TYPE); 329 } 330 331 @Override 332 public View onCreateView(LayoutInflater inflater, ViewGroup container, 333 Bundle savedState) { 334 final View rootView = inflater.inflate(R.layout.folder_list, null); 335 mListView = (ListView) rootView.findViewById(android.R.id.list); 336 mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 337 mListView.setEmptyView(null); 338 // If we're not using tablet UI, set the background correctly 339 if (!Utils.useTabletUI(getResources())) { 340 mListView.setBackgroundResource(R.color.list_background_color); 341 } 342 if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) { 343 mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE)); 344 } 345 if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) { 346 mSelectedFolderUri = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)); 347 mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE); 348 } else if (mParentFolder != null) { 349 mSelectedFolderUri = mParentFolder.uri; 350 // No selected folder type required for hierarchical lists. 351 } 352 353 return rootView; 354 } 355 356 @Override 357 public void onStart() { 358 super.onStart(); 359 } 360 361 @Override 362 public void onStop() { 363 super.onStop(); 364 } 365 366 @Override 367 public void onPause() { 368 super.onPause(); 369 } 370 371 @Override 372 public void onSaveInstanceState(Bundle outState) { 373 super.onSaveInstanceState(outState); 374 if (mListView != null) { 375 outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState()); 376 } 377 if (mSelectedFolderUri != null) { 378 outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString()); 379 } 380 outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType); 381 } 382 383 @Override 384 public void onDestroyView() { 385 if (mCursorAdapter != null) { 386 mCursorAdapter.destroy(); 387 } 388 // Clear the adapter. 389 setListAdapter(null); 390 if (mFolderObserver != null) { 391 mFolderObserver.unregisterAndDestroy(); 392 mFolderObserver = null; 393 } 394 if (mAccountObserver != null) { 395 mAccountObserver.unregisterAndDestroy(); 396 mAccountObserver = null; 397 } 398 if (mAllAccountsObserver != null) { 399 mAllAccountsObserver.unregisterAndDestroy(); 400 mAllAccountsObserver = null; 401 } 402 super.onDestroyView(); 403 } 404 405 @Override 406 public void onListItemClick(ListView l, View v, int position, long id) { 407 viewFolderOrChangeAccount(position); 408 } 409 410 /** 411 * Display the conversation list from the folder at the position given. 412 * @param position a zero indexed position into the list. 413 */ 414 private void viewFolderOrChangeAccount(int position) { 415 final Object item = getListAdapter().getItem(position); 416 LogUtils.d(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item); 417 final Folder folder; 418 if (item instanceof DrawerItem) { 419 final DrawerItem drawerItem = (DrawerItem) item; 420 // Could be a folder or account. 421 final int itemType = mCursorAdapter.getItemType(drawerItem); 422 if (itemType == DrawerItem.VIEW_ACCOUNT) { 423 // Account, so switch. 424 folder = null; 425 final Account account = drawerItem.mAccount; 426 // Switching accounts takes you to the inbox, which is always a system folder. 427 mSelectedFolderType = DrawerItem.FOLDER_SYSTEM; 428 mAccountChanger.changeAccount(account); 429 } else if (itemType == DrawerItem.VIEW_FOLDER) { 430 // Folder type, so change folders only. 431 folder = drawerItem.mFolder; 432 mSelectedFolderType = drawerItem.mFolderType; 433 LogUtils.d(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d", 434 folder, mSelectedFolderType); 435 } else { 436 // Do nothing. 437 LogUtils.d(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():" 438 + " Clicked on unset item in drawer. Offending item is " + item); 439 return; 440 } 441 } else if (item instanceof Folder) { 442 folder = (Folder) item; 443 } else if (item instanceof ObjectCursor){ 444 folder = ((ObjectCursor<Folder>) item).getModel(); 445 } else { 446 // Don't know how we got here. 447 LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item"); 448 folder = null; 449 } 450 if (folder != null) { 451 // Since we may be looking at hierarchical views, if we can 452 // determine the parent of the folder we have tapped, set it here. 453 // If we are looking at the folder we are already viewing, don't 454 // update its parent! 455 folder.parent = folder.equals(mParentFolder) ? null : mParentFolder; 456 // Go to the conversation list for this folder. 457 mFolderChanger.onFolderSelected(folder); 458 } 459 } 460 461 @Override 462 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) { 463 mListView.setEmptyView(null); 464 final Uri folderListUri; 465 if (mType == TYPE_TREE) { 466 // Folder trees, they specify a URI at construction time. 467 folderListUri = mFolderListUri; 468 } else if (mType == TYPE_DRAWER) { 469 // Drawers should have a valid account 470 if (mCurrentAccount != null) { 471 folderListUri = mCurrentAccount.folderListUri; 472 } else { 473 LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account"); 474 return null; 475 } 476 } else { 477 LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type"); 478 return null; 479 } 480 return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri, 481 UIProvider.FOLDERS_PROJECTION, Folder.FACTORY); 482 } 483 484 @Override 485 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) { 486 if (mCursorAdapter != null) { 487 mCursorAdapter.setCursor(data); 488 } 489 } 490 491 @Override 492 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) { 493 if (mCursorAdapter != null) { 494 mCursorAdapter.setCursor(null); 495 } 496 } 497 498 /** 499 * Returns the sorted list of accounts. The AAC always has the current list, sorted by 500 * frequency of use. 501 * @return a list of accounts, sorted by frequency of use 502 */ 503 private Account[] getAllAccounts() { 504 if (mAllAccountsObserver != null) { 505 return mAllAccountsObserver.getAllAccounts(); 506 } 507 return new Account[0]; 508 } 509 510 /** 511 * Interface for all cursor adapters that allow setting a cursor and being destroyed. 512 */ 513 private interface FolderListFragmentCursorAdapter extends ListAdapter { 514 /** Update the folder list cursor with the cursor given here. */ 515 void setCursor(ObjectCursor<Folder> cursor); 516 /** 517 * Given an item, find the type of the item, which should only be {@link 518 * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT} 519 * @return item the type of the item. 520 */ 521 int getItemType(DrawerItem item); 522 /** Get the folder associated with this item. **/ 523 Folder getFullFolder(DrawerItem item); 524 /** Notify that the all accounts changed. */ 525 void notifyAllAccountsChanged(); 526 /** Remove all observers and destroy the object. */ 527 void destroy(); 528 /** Notifies the adapter that the data has changed. */ 529 void notifyDataSetChanged(); 530 } 531 532 /** 533 * An adapter for flat folder lists. 534 */ 535 private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter { 536 537 private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() { 538 @Override 539 public void onChanged() { 540 recalculateList(); 541 } 542 }; 543 /** Database columns for email address -> photo_id query */ 544 private final String[] DATA_COLS = new String[] { Email.DATA, Email.PHOTO_ID }; 545 /** Database columns for photo_id -> photo query */ 546 private final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO }; 547 /** No resource used for string header in folder list */ 548 private static final int NO_HEADER_RESOURCE = -1; 549 /** Cache of most recently used folders */ 550 private final RecentFolderList mRecentFolders; 551 /** True if the list is sectioned, false otherwise */ 552 private final boolean mIsSectioned; 553 /** All the items */ 554 private List<DrawerItem> mItemList = new ArrayList<DrawerItem>(); 555 /** Cursor into the folder list. This might be null. */ 556 private ObjectCursor<Folder> mCursor = null; 557 /** Watcher for tracking and receiving unread counts for mail */ 558 private FolderWatcher mFolderWatcher = null; 559 /** 560 * DO NOT USE off the UI thread. Will cause ConcurrentModificationExceptions otherwise 561 * 562 * Email address -> Bitmap 563 * Caveat: at some point we will want this to be from URI to Bitmap. 564 */ 565 private final HashMap<String, Bitmap> mEmailToPhotoMap = new HashMap<String, Bitmap>(); 566 567 /** 568 * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders. 569 * 570 * @param isSectioned true if folder list is flat, false if sectioned by label group 571 */ 572 public FolderListAdapter(boolean isSectioned) { 573 super(); 574 mIsSectioned = isSectioned; 575 final RecentFolderController controller = mActivity.getRecentFolderController(); 576 if (controller != null && mIsSectioned) { 577 mRecentFolders = mRecentFolderObserver.initialize(controller); 578 } else { 579 mRecentFolders = null; 580 } 581 mFolderWatcher = new FolderWatcher(mActivity, this); 582 mFolderWatcher.updateAccountList(getAllAccounts()); 583 } 584 585 @Override 586 public void notifyAllAccountsChanged() { 587 mFolderWatcher.updateAccountList(getAllAccounts()); 588 retrieveContactPhotos(); 589 recalculateList(); 590 } 591 592 /** 593 * AsyncTask for loading all photos that populates the email address -> Bitmap Hash Map. 594 * Does the querying and loading of photos in the background along with creating 595 * default images in case contact photos aren't found. 596 * 597 * The task is of type <String, Void, HashMap<String, Bitmap>> which corresponds to 598 * the input being an array of String and the result being a HashMap that will get merged to 599 * {@link FolderListAdapter#mEmailToPhotoMap}. 600 */ 601 private class LoadPhotosTask extends AsyncTask<Account, Void, HashMap<String, Bitmap>> { 602 private final ContentResolver mResolver; 603 private final Context mContext; 604 private final int mImageSize; 605 606 /** 607 * Construct the async task for downloading the photos. 608 */ 609 public LoadPhotosTask(final Context context, final int imageSize) { 610 mResolver = context.getContentResolver(); 611 mContext = context; 612 mImageSize = imageSize; 613 } 614 615 /** 616 * Runs account photo retrieval in the background. Note, mEmailToPhotoMap should NOT be 617 * modified here since this is run on a background thread and not the UI thread. 618 * 619 * The {@link Account#accountFromAddresses} is used for letter tiles and is required 620 * in order to properly assign the tile to the respective account. 621 */ 622 @Override 623 protected HashMap<String, Bitmap> doInBackground(final Account... allAccounts) { 624 final HashMap<String, String> addressToDisplayNameMap = new HashMap< 625 String, String>(); 626 for (final Account account : allAccounts) { 627 addressToDisplayNameMap.put(account.name, account.accountFromAddresses); 628 } 629 630 return getAccountPhoto(addressToDisplayNameMap); 631 } 632 633 @Override 634 protected void onPostExecute(final HashMap<String, Bitmap> accountPhotos) { 635 mEmailToPhotoMap.putAll(accountPhotos); 636 if (!accountPhotos.isEmpty()) { 637 recalculateList(); 638 } 639 } 640 641 /** 642 * Queries the database for the photos. First finds the corresponding photo_id and then 643 * proceeds to find the photo through subsequent queries for {photo_id, bytes}. If the 644 * photo is not found for the address at the end, creates a letter tile using the 645 * display name/email address and then adds that to the finished HashMap 646 * 647 * @param addresses array of email addresses (strings) 648 * @param addressToDisplayNameMap map of email addresses to display names used for 649 * letter tiles 650 * @return map of email addresses to the corresponding photos 651 */ 652 private HashMap<String, Bitmap> getAccountPhoto( 653 final HashMap<String, String> addressToDisplayNameMap) { 654 // Columns for email address, photo_id 655 final int DATA_EMAIL_COLUMN = 0; 656 final int DATA_PHOTO_COLUMN = 1; 657 final HashMap<String, Bitmap> photoMap = new HashMap<String, Bitmap>(); 658 final Set<String> addressSet = addressToDisplayNameMap.keySet(); 659 660 String address; 661 long photoId; 662 Cursor photoIdsCursor = null; 663 664 665 try { 666 // Build query for address -> photo_id 667 final StringBuilder query = new StringBuilder().append(Data.MIMETYPE) 668 .append("='").append(Email.CONTENT_ITEM_TYPE).append("' AND ") 669 .append(Email.DATA).append(" IN ("); 670 appendQuestionMarks(query, addressSet.size()); 671 query.append(')'); 672 photoIdsCursor = mResolver 673 .query(Data.CONTENT_URI, DATA_COLS, query.toString(), 674 addressSet.toArray(new String[addressSet.size()]), null); 675 676 // Iterate through cursor and attempt to find a matching photo_id 677 if (photoIdsCursor != null) { 678 while (photoIdsCursor.moveToNext()) { 679 // If photo_id is found, query for the encoded bitmap 680 if (!photoIdsCursor.isNull(DATA_PHOTO_COLUMN)) { 681 address = photoIdsCursor.getString(DATA_EMAIL_COLUMN); 682 photoId = photoIdsCursor.getLong(DATA_PHOTO_COLUMN); 683 final byte[] bitmapBytes = getPhotoForId(photoId); 684 if (bitmapBytes != null && photoMap.get(address) == null) { 685 final Bitmap contactPhoto = BitmapUtil.decodeBitmapFromBytes( 686 bitmapBytes, mImageSize, mImageSize); 687 photoMap.put(address, contactPhoto); 688 } 689 } 690 } 691 } 692 } finally { 693 if(photoIdsCursor != null) { 694 photoIdsCursor.close(); 695 } 696 } 697 698 // Finally, make sure that for any addresses in the original list for which 699 // we are unable to find contact photos, we're adding the LetterTiles 700 for(final String emailAddress : addressSet) { 701 if(!photoMap.containsKey(emailAddress)) { 702 final Bitmap letterTile = LetterTileUtils.generateLetterTile( 703 addressToDisplayNameMap.get(emailAddress), emailAddress, mContext, 704 mImageSize, mImageSize); 705 photoMap.put(emailAddress, letterTile); 706 } 707 } 708 709 return photoMap; 710 } 711 712 /** 713 * Find the photo by running a query on the photoId provided. 714 * 715 * @param resolver ContentResolver to query on 716 * @param photoId id corresponding to the photo (if found) 717 * @return array containing photo bytes 718 */ 719 private byte[] getPhotoForId(final long photoId) { 720 // Column for the photo blob 721 final int DATA_PHOTO_COLUMN = 1; 722 723 byte[] bitmapBytes = null; 724 // First try getting photos from Contacts 725 Cursor contactCursor = null; 726 try { 727 final String[] selectionArgs = { String.valueOf(photoId) }; 728 contactCursor = mResolver.query(Data.CONTENT_URI, PHOTO_COLS, 729 Photo._ID + " = ?", selectionArgs, null); 730 while (contactCursor.moveToNext()) { 731 if (!contactCursor.isNull(1)) { 732 bitmapBytes = contactCursor.getBlob(1); 733 break; 734 } 735 } 736 } finally { 737 if (contactCursor != null) { 738 contactCursor.close(); 739 } 740 } 741 742 // Photo not found in contacts, try profiles instead 743 if(bitmapBytes == null) { 744 if (ContactsContract.isProfileId(photoId)) { 745 Cursor profileCursor = null; 746 try { 747 profileCursor = mResolver.query( 748 ContentUris.withAppendedId(Data.CONTENT_URI, photoId), 749 PHOTO_COLS, null, null, null); 750 if (profileCursor != null && profileCursor.moveToFirst()) { 751 bitmapBytes = profileCursor.getBlob(DATA_PHOTO_COLUMN); 752 } 753 } finally { 754 if (profileCursor != null) { 755 profileCursor.close(); 756 } 757 } 758 } 759 } 760 return bitmapBytes; 761 } 762 763 /** 764 * Prepare the Selection clause for the given query by appending question marks 765 * followed by commas (Comma-delimited list of question marks as listed by 766 * the itemCount. 767 * 768 * @param query {@link StringBuilder} representing the query thus far 769 * @param itemCount number of selection arguments to add 770 */ 771 private void appendQuestionMarks(final StringBuilder query, final int itemCount) { 772 final String[] questionMarks = new String[itemCount]; 773 Arrays.fill(questionMarks, "?"); 774 final String selection = TextUtils.join(", ", questionMarks); 775 query.append(selection); 776 } 777 } 778 779 /** 780 * Retrieve photos for accounts that do not yet have a mapping in 781 * {@link FolderListAdapter#mEmailToPhotoMap} by querying over the database. Every account 782 * is guaranteed to have either the account contact photo, letter tile, or a default gray 783 * picture for non-English account names. 784 */ 785 public synchronized void retrieveContactPhotos() { 786 final Account[] allAccounts = getAllAccounts(); 787 if (allAccounts == null) { 788 return; 789 } 790 /** Fresh accounts that were recently added to the system. */ 791 final HashSet<Account> freshAccounts = new HashSet<Account>(); 792 /** All current account email addresses. */ 793 final HashSet<String> currentEmailList = new HashSet<String>(); 794 final Context context = mActivity.getActivityContext(); 795 final int imageSize = context.getResources().getDimensionPixelSize( 796 R.dimen.folder_list_item_minimum_height); 797 798 for (final Account account : allAccounts) { 799 final String email = account.name; 800 if (!mEmailToPhotoMap.containsKey(email)) { 801 freshAccounts.add(account); 802 // For multiple tasks running very closely together, make sure we don't end up 803 // loading pictures for an address more than once 804 mEmailToPhotoMap.put(email, null); 805 } 806 currentEmailList.add(email); 807 } 808 // Find all the stale accounts in our map, and remove them. 809 final Set<String> emails = ImmutableSet.copyOf(mEmailToPhotoMap.keySet()); 810 for (final String email : emails) { 811 if (!currentEmailList.contains(email)) { 812 mEmailToPhotoMap.remove(email); 813 } 814 } 815 // Fetch contact photos or letter tiles for each fresh account. 816 if (!freshAccounts.isEmpty()) { 817 new LoadPhotosTask(context, imageSize).execute( 818 freshAccounts.toArray(new Account[freshAccounts.size()])); 819 } 820 } 821 822 @Override 823 public View getView(int position, View convertView, ViewGroup parent) { 824 final DrawerItem item = (DrawerItem) getItem(position); 825 final View view = item.getView(position, convertView, parent); 826 final int type = item.mType; 827 if (mListView != null) { 828 final boolean isSelected = 829 item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType); 830 if (type == DrawerItem.VIEW_FOLDER) { 831 mListView.setItemChecked(position, isSelected); 832 } 833 // If this is the current folder, also check to verify that the unread count 834 // matches what the action bar shows. 835 if (type == DrawerItem.VIEW_FOLDER 836 && isSelected 837 && (mCurrentFolderForUnreadCheck != null) 838 && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) { 839 ((FolderItemView) view).overrideUnreadCount( 840 mCurrentFolderForUnreadCheck.unreadCount); 841 } 842 } 843 LogUtils.d(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item); 844 return view; 845 } 846 847 @Override 848 public int getViewTypeCount() { 849 // Accounts, headers, folders (all parts of drawer view types) 850 return DrawerItem.getViewTypes(); 851 } 852 853 @Override 854 public int getItemViewType(int position) { 855 return ((DrawerItem) getItem(position)).mType; 856 } 857 858 @Override 859 public int getCount() { 860 return mItemList.size(); 861 } 862 863 @Override 864 public boolean isEnabled(int position) { 865 return ((DrawerItem) getItem(position)).isItemEnabled(); 866 } 867 868 private Uri getCurrentAccountUri() { 869 return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri; 870 } 871 872 @Override 873 public boolean areAllItemsEnabled() { 874 // We have headers and thus some items are not enabled. 875 return false; 876 } 877 878 /** 879 * Returns all the recent folders from the list given here. Safe to call with a null list. 880 * @param recentList a list of all recently accessed folders. 881 * @return a valid list of folders, which are all recent folders. 882 */ 883 private List<Folder> getRecentFolders(RecentFolderList recentList) { 884 final List<Folder> folderList = new ArrayList<Folder>(); 885 if (recentList == null) { 886 return folderList; 887 } 888 // Get all recent folders, after removing system folders. 889 for (final Folder f : recentList.getRecentFolderList(null)) { 890 if (!f.isProviderFolder()) { 891 folderList.add(f); 892 } 893 } 894 return folderList; 895 } 896 897 /** 898 * Responsible for verifying mCursor, and ensuring any recalculate 899 * conditions are met. Also calls notifyDataSetChanged once it's finished 900 * populating {@link FolderListAdapter#mItemList} 901 */ 902 private void recalculateList() { 903 final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>(); 904 recalculateListAccounts(newFolderList); 905 recalculateListFolders(newFolderList); 906 mItemList = newFolderList; 907 // Ask the list to invalidate its views. 908 notifyDataSetChanged(); 909 } 910 911 /** 912 * Recalculates the accounts if not null and adds them to the list. 913 * 914 * @param itemList List of drawer items to populate 915 */ 916 private void recalculateListAccounts(List<DrawerItem> itemList) { 917 final Account[] allAccounts = getAllAccounts(); 918 // Add all accounts and then the current account 919 final Uri currentAccountUri = getCurrentAccountUri(); 920 for (final Account account : allAccounts) { 921 if (!currentAccountUri.equals(account.uri)) { 922 final int unreadCount = mFolderWatcher.getUnreadCount(account); 923 itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, false, 924 mEmailToPhotoMap.get(account.name))); 925 } 926 } 927 if (mCurrentAccount == null) { 928 LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account."); 929 } else { 930 // We don't show the unread count for the current account, so set this to zero. 931 itemList.add(DrawerItem.ofAccount(mActivity, mCurrentAccount, 0, true, 932 mEmailToPhotoMap.get(mCurrentAccount.name))); 933 } 934 } 935 936 /** 937 * Recalculates the system, recent and user label lists. 938 * This method modifies all the three lists on every single invocation. 939 * 940 * @param itemList List of drawer items to populate 941 */ 942 private void recalculateListFolders(List<DrawerItem> itemList) { 943 // If we are waiting for folder initialization, we don't have any kinds of folders, 944 // just the "Waiting for initialization" item. Note, this should only be done 945 // when we're waiting for account initialization or initial sync. 946 if (isCursorInvalid(mCursor)) { 947 if(!mCurrentAccount.isAccountReady()) { 948 itemList.add(DrawerItem.forWaitView(mActivity)); 949 } 950 return; 951 } 952 953 if (!mIsSectioned) { 954 // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers. 955 do { 956 final Folder f = mCursor.getModel(); 957 if (!isFolderTypeExcluded(f)) { 958 itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER, 959 mCursor.getPosition())); 960 } 961 } while (mCursor.moveToNext()); 962 return; 963 } 964 965 // Otherwise, this is an adapter for a sectioned list. 966 final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>(); 967 final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>(); 968 do { 969 final Folder f = mCursor.getModel(); 970 if (!isFolderTypeExcluded(f)) { 971 if (f.isProviderFolder() && f.isInbox()) { 972 inboxFolders.add(DrawerItem.ofFolder( 973 mActivity, f, DrawerItem.FOLDER_SYSTEM, mCursor.getPosition())); 974 } else { 975 allFoldersList.add(DrawerItem.ofFolder( 976 mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition())); 977 } 978 } 979 } while (mCursor.moveToNext()); 980 981 // Add all inboxes (sectioned included) before recents. 982 addFolderSection(itemList, inboxFolders, NO_HEADER_RESOURCE); 983 984 // Add most recently folders (in alphabetical order) next. 985 addRecentsToList(itemList); 986 987 // Add the remaining provider folders followed by all labels. 988 addFolderSection(itemList, allFoldersList, R.string.all_folders_heading); 989 } 990 991 /** 992 * Given a list of folders as {@link DrawerItem}s, add them to the item 993 * list as needed. Passing in a non-0 integer for the resource will 994 * enable a header 995 * 996 * @param destination List of drawer items to populate 997 * @param source List of drawer items representing folders to add to the drawer 998 * @param headerStringResource 999 * {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header 1000 * is required, or res-id otherwise 1001 */ 1002 private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source, 1003 int headerStringResource) { 1004 if (source.size() > 0) { 1005 if(headerStringResource != NO_HEADER_RESOURCE) { 1006 destination.add(DrawerItem.ofHeader(mActivity, headerStringResource)); 1007 } 1008 destination.addAll(source); 1009 } 1010 } 1011 1012 /** 1013 * Add recent folders to the list in order as acquired by the {@link RecentFolderList}. 1014 * 1015 * @param destination List of drawer items to populate 1016 */ 1017 private void addRecentsToList(List<DrawerItem> destination) { 1018 // If there are recent folders, add them. 1019 final List<Folder> recentFolderList = getRecentFolders(mRecentFolders); 1020 1021 // Remove any excluded folder types 1022 if (mExcludedFolderTypes != null) { 1023 final Iterator<Folder> iterator = recentFolderList.iterator(); 1024 while (iterator.hasNext()) { 1025 if (isFolderTypeExcluded(iterator.next())) { 1026 iterator.remove(); 1027 } 1028 } 1029 } 1030 1031 if (recentFolderList.size() > 0) { 1032 destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading)); 1033 // Recent folders are not queried for position. 1034 final int position = -1; 1035 for (Folder f : recentFolderList) { 1036 destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT, 1037 position)); 1038 } 1039 } 1040 } 1041 1042 /** 1043 * Check if the cursor provided is valid. 1044 * @param mCursor 1045 * @return True if cursor is invalid, false otherwise 1046 */ 1047 private boolean isCursorInvalid(Cursor mCursor) { 1048 return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0 1049 || !mCursor.moveToFirst(); 1050 } 1051 1052 @Override 1053 public void setCursor(ObjectCursor<Folder> cursor) { 1054 mCursor = cursor; 1055 recalculateList(); 1056 } 1057 1058 @Override 1059 public Object getItem(int position) { 1060 return mItemList.get(position); 1061 } 1062 1063 @Override 1064 public long getItemId(int position) { 1065 return getItem(position).hashCode(); 1066 } 1067 1068 @Override 1069 public final void destroy() { 1070 mRecentFolderObserver.unregisterAndDestroy(); 1071 } 1072 1073 @Override 1074 public int getItemType(DrawerItem item) { 1075 return item.mType; 1076 } 1077 1078 // TODO(viki): This is strange. We have the full folder and yet we create on from scratch. 1079 @Override 1080 public Folder getFullFolder(DrawerItem folderItem) { 1081 if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) { 1082 return folderItem.mFolder; 1083 } else { 1084 final int pos = folderItem.mPosition; 1085 if (pos > -1 && mCursor != null && !mCursor.isClosed() 1086 && mCursor.moveToPosition(folderItem.mPosition)) { 1087 return mCursor.getModel(); 1088 } else { 1089 return null; 1090 } 1091 } 1092 } 1093 } 1094 1095 private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder> 1096 implements FolderListFragmentCursorAdapter{ 1097 1098 private static final int PARENT = 0; 1099 private static final int CHILD = 1; 1100 private final Uri mParentUri; 1101 private final Folder mParent; 1102 private final FolderItemView.DropHandler mDropHandler; 1103 private ObjectCursor<Folder> mCursor; 1104 1105 public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) { 1106 super(mActivity.getActivityContext(), R.layout.folder_item); 1107 mDropHandler = mActivity; 1108 mParent = parentFolder; 1109 mParentUri = parentFolder.uri; 1110 setCursor(c); 1111 } 1112 1113 @Override 1114 public int getViewTypeCount() { 1115 // Child and Parent 1116 return 2; 1117 } 1118 1119 @Override 1120 public int getItemViewType(int position) { 1121 final Folder f = getItem(position); 1122 return f.uri.equals(mParentUri) ? PARENT : CHILD; 1123 } 1124 1125 @Override 1126 public View getView(int position, View convertView, ViewGroup parent) { 1127 final FolderItemView folderItemView; 1128 final Folder folder = getItem(position); 1129 boolean isParent = folder.uri.equals(mParentUri); 1130 if (convertView != null) { 1131 folderItemView = (FolderItemView) convertView; 1132 } else { 1133 int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item; 1134 folderItemView = (FolderItemView) LayoutInflater.from( 1135 mActivity.getActivityContext()).inflate(resId, null); 1136 } 1137 folderItemView.bind(folder, mDropHandler); 1138 if (folder.uri.equals(mSelectedFolderUri)) { 1139 getListView().setItemChecked(position, true); 1140 // If this is the current folder, also check to verify that the unread count 1141 // matches what the action bar shows. 1142 final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null) 1143 && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount; 1144 if (unreadCountDiffers) { 1145 folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount); 1146 } 1147 } 1148 Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block)); 1149 Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon)); 1150 return folderItemView; 1151 } 1152 1153 @Override 1154 public void setCursor(ObjectCursor<Folder> cursor) { 1155 mCursor = cursor; 1156 clear(); 1157 if (mParent != null) { 1158 add(mParent); 1159 } 1160 if (cursor != null && cursor.getCount() > 0) { 1161 cursor.moveToFirst(); 1162 do { 1163 Folder f = cursor.getModel(); 1164 f.parent = mParent; 1165 add(f); 1166 } while (cursor.moveToNext()); 1167 } 1168 } 1169 1170 @Override 1171 public void destroy() { 1172 // Do nothing. 1173 } 1174 1175 @Override 1176 public int getItemType(DrawerItem item) { 1177 // Always returns folders for now. 1178 return DrawerItem.VIEW_FOLDER; 1179 } 1180 1181 @Override 1182 public Folder getFullFolder(DrawerItem folderItem) { 1183 final int pos = folderItem.mPosition; 1184 if (mCursor == null || mCursor.isClosed()) { 1185 return null; 1186 } 1187 if (pos > -1 && mCursor != null && !mCursor.isClosed() 1188 && mCursor.moveToPosition(folderItem.mPosition)) { 1189 return mCursor.getModel(); 1190 } else { 1191 return null; 1192 } 1193 } 1194 1195 @Override 1196 public void notifyAllAccountsChanged() { 1197 // Do nothing. We don't care about changes to all accounts. 1198 } 1199 } 1200 1201 public Folder getParentFolder() { 1202 return mParentFolder; 1203 } 1204 1205 /** 1206 * Sets the currently selected folder safely. 1207 * @param folder 1208 */ 1209 private void setSelectedFolder(Folder folder) { 1210 if (folder == null) { 1211 mSelectedFolderUri = Uri.EMPTY; 1212 mCurrentFolderForUnreadCheck = null; 1213 LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!"); 1214 return; 1215 } 1216 final boolean viewChanged = 1217 !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck); 1218 mCurrentFolderForUnreadCheck = folder; 1219 mSelectedFolderUri = folder.uri; 1220 setSelectedFolderType(folder); 1221 if (mCursorAdapter != null && viewChanged) { 1222 mCursorAdapter.notifyDataSetChanged(); 1223 } 1224 } 1225 1226 /** 1227 * Sets the selected folder type safely. 1228 * @param folder folder to set to. 1229 */ 1230 private void setSelectedFolderType(Folder folder) { 1231 if (mSelectedFolderType == DrawerItem.UNSET) { 1232 mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM 1233 : DrawerItem.FOLDER_USER; 1234 } 1235 } 1236 1237 /** 1238 * Sets the current account to the one provided here. 1239 * @param account the current account to set to. 1240 */ 1241 private void setSelectedAccount(Account account){ 1242 final boolean changed = (account != null) && (mCurrentAccount == null 1243 || !mCurrentAccount.uri.equals(account.uri)); 1244 mCurrentAccount = account; 1245 if (changed) { 1246 // We no longer have proper folder objects. Let the new ones come in 1247 mCursorAdapter.setCursor(null); 1248 // If currentAccount is different from the one we set, restart the loader. Look at the 1249 // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we 1250 // don't just do restartLoader. 1251 final LoaderManager manager = getLoaderManager(); 1252 manager.destroyLoader(FOLDER_LOADER_ID); 1253 manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this); 1254 // An updated cursor causes the entire list to refresh. No need to refresh the list. 1255 } else if (account == null) { 1256 // This should never happen currently, but is a safeguard against a very incorrect 1257 // non-null account -> null account transition. 1258 LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader."); 1259 final LoaderManager manager = getLoaderManager(); 1260 manager.destroyLoader(FOLDER_LOADER_ID); 1261 } 1262 } 1263 1264 public interface FolderListSelectionListener { 1265 public void onFolderSelected(Folder folder); 1266 } 1267 1268 /** 1269 * Get whether the FolderListFragment is currently showing the hierarchy 1270 * under a single parent. 1271 */ 1272 public boolean showingHierarchy() { 1273 return mParentFolder != null; 1274 } 1275 1276 /** 1277 * Checks if the specified {@link Folder} is a type that we want to exclude from displaying. 1278 */ 1279 private boolean isFolderTypeExcluded(final Folder folder) { 1280 if (mExcludedFolderTypes == null) { 1281 return false; 1282 } 1283 1284 for (final int excludedType : mExcludedFolderTypes) { 1285 if (folder.isType(excludedType)) { 1286 return true; 1287 } 1288 } 1289 1290 return false; 1291 } 1292} 1293