FolderListFragment.java revision e06c114b3ca18ae26bcb08fa46717f44d621d8ef
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 266 final Folder selectedFolder; 267 if (mParentFolder != null) { 268 mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder); 269 selectedFolder = mActivity.getHierarchyFolder(); 270 } else { 271 mCursorAdapter = new FolderListAdapter(mIsSectioned); 272 selectedFolder = currentFolder; 273 } 274 // Is the selected folder fresher than the one we have restored from a bundle? 275 if (selectedFolder != null && !selectedFolder.uri.equals(mSelectedFolderUri)) { 276 setSelectedFolder(selectedFolder); 277 } 278 279 // Assign observers for current account & all accounts 280 final AccountController accountController = mActivity.getAccountController(); 281 mAccountObserver = new AccountObserver() { 282 @Override 283 public void onChanged(Account newAccount) { 284 setSelectedAccount(newAccount); 285 } 286 }; 287 if (accountController != null) { 288 // Current account and its observer. 289 setSelectedAccount(mAccountObserver.initialize(accountController)); 290 // List of all accounts and its observer. 291 mAllAccountsObserver = new AllAccountObserver(){ 292 @Override 293 public void onChanged(Account[] allAccounts) { 294 mCursorAdapter.notifyAllAccountsChanged(); 295 } 296 }; 297 mAllAccountsObserver.initialize(accountController); 298 mAccountChanger = accountController; 299 } 300 301 mFolderChanger = mActivity.getFolderListSelectionListener(); 302 if (mActivity.isFinishing()) { 303 // Activity is finishing, just bail. 304 return; 305 } 306 307 setListAdapter(mCursorAdapter); 308 } 309 310 /** 311 * Set the instance variables from the arguments provided here. 312 * @param args 313 */ 314 private void setInstanceFromBundle(Bundle args) { 315 if (args == null) { 316 return; 317 } 318 mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER); 319 final String folderUri = args.getString(ARG_FOLDER_LIST_URI); 320 if (folderUri == null) { 321 mFolderListUri = Uri.EMPTY; 322 } else { 323 mFolderListUri = Uri.parse(folderUri); 324 } 325 mIsSectioned = args.getBoolean(ARG_IS_SECTIONED); 326 mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES); 327 mType = args.getInt(ARG_TYPE); 328 } 329 330 @Override 331 public View onCreateView(LayoutInflater inflater, ViewGroup container, 332 Bundle savedState) { 333 setInstanceFromBundle(getArguments()); 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.i(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 mAccountChanger.changeAccount(account); 427 } else if (itemType == DrawerItem.VIEW_FOLDER) { 428 // Folder type, so change folders only. 429 folder = drawerItem.mFolder; 430 mSelectedFolderType = drawerItem.mFolderType; 431 LogUtils.i(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d", 432 folder, mSelectedFolderType); 433 } else { 434 // Do nothing. 435 LogUtils.i(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():" 436 + " Clicked on unset item in drawer. Offending item is " + item); 437 return; 438 } 439 } else if (item instanceof Folder) { 440 folder = (Folder) item; 441 } else if (item instanceof ObjectCursor){ 442 folder = ((ObjectCursor<Folder>) item).getModel(); 443 } else { 444 // Don't know how we got here. 445 LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item"); 446 folder = null; 447 } 448 if (folder != null) { 449 // Since we may be looking at hierarchical views, if we can 450 // determine the parent of the folder we have tapped, set it here. 451 // If we are looking at the folder we are already viewing, don't 452 // update its parent! 453 folder.parent = folder.equals(mParentFolder) ? null : mParentFolder; 454 // Go to the conversation list for this folder. 455 mFolderChanger.onFolderSelected(folder); 456 } 457 } 458 459 @Override 460 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) { 461 mListView.setEmptyView(null); 462 final Uri folderListUri; 463 if (mType == TYPE_TREE) { 464 // Folder trees, they specify a URI at construction time. 465 folderListUri = mFolderListUri; 466 } else if (mType == TYPE_DRAWER) { 467 // Drawers should have a valid account 468 if (mCurrentAccount != null) { 469 folderListUri = mCurrentAccount.folderListUri; 470 } else { 471 LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account"); 472 return null; 473 } 474 } else { 475 LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type"); 476 return null; 477 } 478 return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri, 479 UIProvider.FOLDERS_PROJECTION, Folder.FACTORY); 480 } 481 482 @Override 483 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) { 484 if (mCursorAdapter != null) { 485 mCursorAdapter.setCursor(data); 486 } 487 } 488 489 @Override 490 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) { 491 if (mCursorAdapter != null) { 492 mCursorAdapter.setCursor(null); 493 } 494 } 495 496 /** 497 * Returns the sorted list of accounts. The AAC always has the current list, sorted by 498 * frequency of use. 499 * @return a list of accounts, sorted by frequency of use 500 */ 501 private Account[] getAllAccounts() { 502 if (mAllAccountsObserver != null) { 503 return mAllAccountsObserver.getAllAccounts(); 504 } 505 return new Account[0]; 506 } 507 508 /** 509 * Interface for all cursor adapters that allow setting a cursor and being destroyed. 510 */ 511 private interface FolderListFragmentCursorAdapter extends ListAdapter { 512 /** Update the folder list cursor with the cursor given here. */ 513 void setCursor(ObjectCursor<Folder> cursor); 514 /** 515 * Given an item, find the type of the item, which should only be {@link 516 * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT} 517 * @return item the type of the item. 518 */ 519 int getItemType(DrawerItem item); 520 /** Get the folder associated with this item. **/ 521 Folder getFullFolder(DrawerItem item); 522 /** Notify that the all accounts changed. */ 523 void notifyAllAccountsChanged(); 524 /** Remove all observers and destroy the object. */ 525 void destroy(); 526 /** Notifies the adapter that the data has changed. */ 527 void notifyDataSetChanged(); 528 } 529 530 /** 531 * An adapter for flat folder lists. 532 */ 533 private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter { 534 535 private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() { 536 @Override 537 public void onChanged() { 538 recalculateList(); 539 } 540 }; 541 /** Database columns for email address -> photo_id query */ 542 private final String[] DATA_COLS = new String[] { Email.DATA, Email.PHOTO_ID }; 543 /** Database columns for photo_id -> photo query */ 544 private final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO }; 545 /** No resource used for string header in folder list */ 546 private static final int NO_HEADER_RESOURCE = -1; 547 /** Cache of most recently used folders */ 548 private final RecentFolderList mRecentFolders; 549 /** True if the list is sectioned, false otherwise */ 550 private final boolean mIsSectioned; 551 /** All the items */ 552 private List<DrawerItem> mItemList = new ArrayList<DrawerItem>(); 553 /** Cursor into the folder list. This might be null. */ 554 private ObjectCursor<Folder> mCursor = null; 555 /** Watcher for tracking and receiving unread counts for mail */ 556 private FolderWatcher mFolderWatcher = null; 557 /** 558 * DO NOT USE off the UI thread. Will cause ConcurrentModificationExceptions otherwise 559 * 560 * Email address -> Bitmap 561 * Caveat: at some point we will want this to be from URI to Bitmap. 562 */ 563 private final HashMap<String, Bitmap> mEmailToPhotoMap = new HashMap<String, Bitmap>(); 564 565 /** 566 * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders. 567 * 568 * @param isSectioned true if folder list is flat, false if sectioned by label group 569 */ 570 public FolderListAdapter(boolean isSectioned) { 571 super(); 572 mIsSectioned = isSectioned; 573 final RecentFolderController controller = mActivity.getRecentFolderController(); 574 if (controller != null && mIsSectioned) { 575 mRecentFolders = mRecentFolderObserver.initialize(controller); 576 } else { 577 mRecentFolders = null; 578 } 579 mFolderWatcher = new FolderWatcher(mActivity, this); 580 mFolderWatcher.updateAccountList(getAllAccounts()); 581 } 582 583 @Override 584 public void notifyAllAccountsChanged() { 585 mFolderWatcher.updateAccountList(getAllAccounts()); 586 retrieveContactPhotos(); 587 recalculateList(); 588 } 589 590 /** 591 * AsyncTask for loading all photos that populates the email address -> Bitmap Hash Map. 592 * Does the querying and loading of photos in the background along with creating 593 * default images in case contact photos aren't found. 594 * 595 * The task is of type <String, Void, HashMap<String, Bitmap>> which corresponds to 596 * the input being an array of String and the result being a HashMap that will get merged to 597 * {@link FolderListAdapter#mEmailToPhotoMap}. 598 */ 599 private class LoadPhotosTask extends AsyncTask<Account, Void, HashMap<String, Bitmap>> { 600 private final ContentResolver mResolver; 601 private final Context mContext; 602 private final int mImageSize; 603 604 /** 605 * Construct the async task for downloading the photos. 606 */ 607 public LoadPhotosTask(final Context context, final int imageSize) { 608 mResolver = context.getContentResolver(); 609 mContext = context; 610 mImageSize = imageSize; 611 } 612 613 /** 614 * Runs account photo retrieval in the background. Note, mEmailToPhotoMap should NOT be 615 * modified here since this is run on a background thread and not the UI thread. 616 * 617 * The {@link Account#accountFromAddresses} is used for letter tiles and is required 618 * in order to properly assign the tile to the respective account. 619 */ 620 @Override 621 protected HashMap<String, Bitmap> doInBackground(final Account... allAccounts) { 622 final HashMap<String, String> addressToDisplayNameMap = new HashMap< 623 String, String>(); 624 for (final Account account : allAccounts) { 625 addressToDisplayNameMap.put(account.name, account.accountFromAddresses); 626 } 627 628 return getAccountPhoto(addressToDisplayNameMap); 629 } 630 631 @Override 632 protected void onPostExecute(final HashMap<String, Bitmap> accountPhotos) { 633 mEmailToPhotoMap.putAll(accountPhotos); 634 } 635 636 /** 637 * Queries the database for the photos. First finds the corresponding photo_id and then 638 * proceeds to find the photo through subsequent queries for {photo_id, bytes}. If the 639 * photo is not found for the address at the end, creates a letter tile using the 640 * display name/email address and then adds that to the finished HashMap 641 * 642 * @param addresses array of email addresses (strings) 643 * @param addressToDisplayNameMap map of email addresses to display names used for 644 * letter tiles 645 * @return map of email addresses to the corresponding photos 646 */ 647 private HashMap<String, Bitmap> getAccountPhoto( 648 final HashMap<String, String> addressToDisplayNameMap) { 649 // Columns for email address, photo_id 650 final int DATA_EMAIL_COLUMN = 0; 651 final int DATA_PHOTO_COLUMN = 1; 652 final HashMap<String, Bitmap> photoMap = new HashMap<String, Bitmap>(); 653 final Set<String> addressSet = addressToDisplayNameMap.keySet(); 654 655 String address; 656 long photoId; 657 Cursor photoIdsCursor = null; 658 659 660 try { 661 // Build query for address -> photo_id 662 final StringBuilder query = new StringBuilder().append(Data.MIMETYPE) 663 .append("='").append(Email.CONTENT_ITEM_TYPE).append("' AND ") 664 .append(Email.DATA).append(" IN ("); 665 appendQuestionMarks(query, addressSet.size()); 666 query.append(')'); 667 photoIdsCursor = mResolver 668 .query(Data.CONTENT_URI, DATA_COLS, query.toString(), 669 addressSet.toArray(new String[addressSet.size()]), null); 670 671 // Iterate through cursor and attempt to find a matching photo_id 672 if (photoIdsCursor != null) { 673 while (photoIdsCursor.moveToNext()) { 674 // If photo_id is found, query for the encoded bitmap 675 if (!photoIdsCursor.isNull(DATA_PHOTO_COLUMN)) { 676 address = photoIdsCursor.getString(DATA_EMAIL_COLUMN); 677 photoId = photoIdsCursor.getLong(DATA_PHOTO_COLUMN); 678 final byte[] bitmapBytes = getPhotoForId(photoId); 679 if (bitmapBytes != null && photoMap.get(address) == null) { 680 final Bitmap contactPhoto = BitmapUtil.decodeBitmapFromBytes( 681 bitmapBytes, mImageSize, mImageSize); 682 photoMap.put(address, contactPhoto); 683 } 684 } 685 } 686 } 687 } finally { 688 if(photoIdsCursor != null) { 689 photoIdsCursor.close(); 690 } 691 } 692 693 // Finally, make sure that for any addresses in the original list for which 694 // we are unable to find contact photos, we're adding the LetterTiles 695 for(final String emailAddress : addressSet) { 696 if(!photoMap.containsKey(emailAddress)) { 697 final Bitmap letterTile = LetterTileUtils.generateLetterTile( 698 addressToDisplayNameMap.get(emailAddress), emailAddress, mContext, 699 mImageSize, mImageSize); 700 photoMap.put(emailAddress, letterTile); 701 } 702 } 703 704 return photoMap; 705 } 706 707 /** 708 * Find the photo by running a query on the photoId provided. 709 * 710 * @param resolver ContentResolver to query on 711 * @param photoId id corresponding to the photo (if found) 712 * @return array containing photo bytes 713 */ 714 private byte[] getPhotoForId(final long photoId) { 715 // Column for the photo blob 716 final int DATA_PHOTO_COLUMN = 1; 717 718 byte[] bitmapBytes = null; 719 // First try getting photos from Contacts 720 Cursor contactCursor = null; 721 try { 722 final String[] selectionArgs = { String.valueOf(photoId) }; 723 contactCursor = mResolver.query(Data.CONTENT_URI, PHOTO_COLS, 724 Photo._ID + " = ?", selectionArgs, null); 725 while (contactCursor.moveToNext()) { 726 if (!contactCursor.isNull(1)) { 727 bitmapBytes = contactCursor.getBlob(1); 728 break; 729 } 730 } 731 } finally { 732 if (contactCursor != null) { 733 contactCursor.close(); 734 } 735 } 736 737 // Photo not found in contacts, try profiles instead 738 if(bitmapBytes == null) { 739 if (ContactsContract.isProfileId(photoId)) { 740 Cursor profileCursor = null; 741 try { 742 profileCursor = mResolver.query( 743 ContentUris.withAppendedId(Data.CONTENT_URI, photoId), 744 PHOTO_COLS, null, null, null); 745 if (profileCursor != null && profileCursor.moveToFirst()) { 746 bitmapBytes = profileCursor.getBlob(DATA_PHOTO_COLUMN); 747 } 748 } finally { 749 if (profileCursor != null) { 750 profileCursor.close(); 751 } 752 } 753 } 754 } 755 return bitmapBytes; 756 } 757 758 /** 759 * Prepare the Selection clause for the given query by appending question marks 760 * followed by commas (Comma-delimited list of question marks as listed by 761 * the itemCount. 762 * 763 * @param query {@link StringBuilder} representing the query thus far 764 * @param itemCount number of selection arguments to add 765 */ 766 private void appendQuestionMarks(final StringBuilder query, final int itemCount) { 767 final String[] questionMarks = new String[itemCount]; 768 Arrays.fill(questionMarks, "?"); 769 final String selection = TextUtils.join(", ", questionMarks); 770 query.append(selection); 771 } 772 } 773 774 /** 775 * Retrieve photos for accounts that do not yet have a mapping in 776 * {@link FolderListAdapter#mEmailToPhotoMap} by querying over the database. Every account 777 * is guaranteed to have either the account contact photo, letter tile, or a default gray 778 * picture for non-English account names. 779 */ 780 public synchronized void retrieveContactPhotos() { 781 final Account[] allAccounts = getAllAccounts(); 782 if (allAccounts == null) { 783 return; 784 } 785 /** Fresh accounts that were recently added to the system. */ 786 final HashSet<Account> freshAccounts = new HashSet<Account>(); 787 /** All current account email addresses. */ 788 final HashSet<String> currentEmailList = new HashSet<String>(); 789 final Context context = mActivity.getActivityContext(); 790 final int imageSize = context.getResources().getDimensionPixelSize( 791 R.dimen.folder_list_item_minimum_height); 792 793 for (final Account account : allAccounts) { 794 final String email = account.name; 795 if (!mEmailToPhotoMap.containsKey(email)) { 796 freshAccounts.add(account); 797 // For multiple tasks running very closely together, make sure we don't end up 798 // loading pictures for an address more than once 799 mEmailToPhotoMap.put(email, null); 800 } 801 currentEmailList.add(email); 802 } 803 // Find all the stale accounts in our map, and remove them. 804 final Set<String> emails = ImmutableSet.copyOf(mEmailToPhotoMap.keySet()); 805 for (final String email : emails) { 806 if (!currentEmailList.contains(email)) { 807 mEmailToPhotoMap.remove(email); 808 } 809 } 810 // Fetch contact photos or letter tiles for each fresh account. 811 if (!freshAccounts.isEmpty()) { 812 new LoadPhotosTask(context, imageSize).execute( 813 freshAccounts.toArray(new Account[freshAccounts.size()])); 814 } 815 } 816 817 @Override 818 public View getView(int position, View convertView, ViewGroup parent) { 819 final DrawerItem item = (DrawerItem) getItem(position); 820 final View view = item.getView(position, convertView, parent); 821 final int type = item.mType; 822 if (mListView != null) { 823 final boolean isSelected = 824 item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType); 825 if (type == DrawerItem.VIEW_FOLDER) { 826 mListView.setItemChecked(position, isSelected); 827 } 828 // If this is the current folder, also check to verify that the unread count 829 // matches what the action bar shows. 830 if (type == DrawerItem.VIEW_FOLDER 831 && isSelected 832 && (mCurrentFolderForUnreadCheck != null) 833 && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) { 834 ((FolderItemView) view).overrideUnreadCount( 835 mCurrentFolderForUnreadCheck.unreadCount); 836 } 837 } 838 LogUtils.i(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item); 839 return view; 840 } 841 842 @Override 843 public int getViewTypeCount() { 844 // Accounts, headers, folders (all parts of drawer view types) 845 return DrawerItem.getViewTypes(); 846 } 847 848 @Override 849 public int getItemViewType(int position) { 850 return ((DrawerItem) getItem(position)).mType; 851 } 852 853 @Override 854 public int getCount() { 855 return mItemList.size(); 856 } 857 858 @Override 859 public boolean isEnabled(int position) { 860 return ((DrawerItem) getItem(position)).isItemEnabled(); 861 } 862 863 private Uri getCurrentAccountUri() { 864 return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri; 865 } 866 867 @Override 868 public boolean areAllItemsEnabled() { 869 // We have headers and thus some items are not enabled. 870 return false; 871 } 872 873 /** 874 * Returns all the recent folders from the list given here. Safe to call with a null list. 875 * @param recentList a list of all recently accessed folders. 876 * @return a valid list of folders, which are all recent folders. 877 */ 878 private List<Folder> getRecentFolders(RecentFolderList recentList) { 879 final List<Folder> folderList = new ArrayList<Folder>(); 880 if (recentList == null) { 881 return folderList; 882 } 883 // Get all recent folders, after removing system folders. 884 for (final Folder f : recentList.getRecentFolderList(null)) { 885 if (!f.isProviderFolder()) { 886 folderList.add(f); 887 } 888 } 889 return folderList; 890 } 891 892 /** 893 * Responsible for verifying mCursor, and ensuring any recalculate 894 * conditions are met. Also calls notifyDataSetChanged once it's finished 895 * populating {@link FolderListAdapter#mItemList} 896 */ 897 private void recalculateList() { 898 final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>(); 899 recalculateListAccounts(newFolderList); 900 recalculateListFolders(newFolderList); 901 mItemList = newFolderList; 902 // Ask the list to invalidate its views. 903 notifyDataSetChanged(); 904 } 905 906 /** 907 * Recalculates the accounts if not null and adds them to the list. 908 * 909 * @param itemList List of drawer items to populate 910 */ 911 private void recalculateListAccounts(List<DrawerItem> itemList) { 912 final Account[] allAccounts = getAllAccounts(); 913 // Add all accounts and then the current account 914 final Uri currentAccountUri = getCurrentAccountUri(); 915 for (final Account account : allAccounts) { 916 if (!currentAccountUri.equals(account.uri)) { 917 final int unreadCount = mFolderWatcher.getUnreadCount(account); 918 itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, false, 919 mEmailToPhotoMap.get(account.name))); 920 } 921 } 922 if (mCurrentAccount == null) { 923 LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account."); 924 } else { 925 // We don't show the unread count for the current account, so set this to zero. 926 itemList.add(DrawerItem.ofAccount(mActivity, mCurrentAccount, 0, true, 927 mEmailToPhotoMap.get(mCurrentAccount.name))); 928 } 929 } 930 931 /** 932 * Recalculates the system, recent and user label lists. 933 * This method modifies all the three lists on every single invocation. 934 * 935 * @param itemList List of drawer items to populate 936 */ 937 private void recalculateListFolders(List<DrawerItem> itemList) { 938 // If we are waiting for folder initialization, we don't have any kinds of folders, 939 // just the "Waiting for initialization" item. Note, this should only be done 940 // when we're waiting for account initialization or initial sync. 941 if (isCursorInvalid(mCursor)) { 942 if(!mCurrentAccount.isAccountReady()) { 943 itemList.add(DrawerItem.forWaitView(mActivity)); 944 } 945 return; 946 } 947 948 if (!mIsSectioned) { 949 // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers. 950 do { 951 final Folder f = mCursor.getModel(); 952 if (!isFolderTypeExcluded(f)) { 953 itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER, 954 mCursor.getPosition())); 955 } 956 } while (mCursor.moveToNext()); 957 return; 958 } 959 960 // Otherwise, this is an adapter for a sectioned list. 961 final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>(); 962 final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>(); 963 do { 964 final Folder f = mCursor.getModel(); 965 if (!isFolderTypeExcluded(f)) { 966 if (f.isProviderFolder() && f.isInbox()) { 967 inboxFolders.add(DrawerItem.ofFolder( 968 mActivity, f, DrawerItem.FOLDER_SYSTEM, mCursor.getPosition())); 969 } else { 970 allFoldersList.add(DrawerItem.ofFolder( 971 mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition())); 972 } 973 } 974 } while (mCursor.moveToNext()); 975 976 // Add all inboxes (sectioned included) before recents. 977 addFolderSection(itemList, inboxFolders, NO_HEADER_RESOURCE); 978 979 // Add most recently folders (in alphabetical order) next. 980 addRecentsToList(itemList); 981 982 // Add the remaining provider folders followed by all labels. 983 addFolderSection(itemList, allFoldersList, R.string.all_folders_heading); 984 } 985 986 /** 987 * Given a list of folders as {@link DrawerItem}s, add them to the item 988 * list as needed. Passing in a non-0 integer for the resource will 989 * enable a header 990 * 991 * @param destination List of drawer items to populate 992 * @param source List of drawer items representing folders to add to the drawer 993 * @param headerStringResource 994 * {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header 995 * is required, or res-id otherwise 996 */ 997 private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source, 998 int headerStringResource) { 999 if (source.size() > 0) { 1000 if(headerStringResource != NO_HEADER_RESOURCE) { 1001 destination.add(DrawerItem.ofHeader(mActivity, headerStringResource)); 1002 } 1003 destination.addAll(source); 1004 } 1005 } 1006 1007 /** 1008 * Add recent folders to the list in order as acquired by the {@link RecentFolderList}. 1009 * 1010 * @param destination List of drawer items to populate 1011 */ 1012 private void addRecentsToList(List<DrawerItem> destination) { 1013 // If there are recent folders, add them. 1014 final List<Folder> recentFolderList = getRecentFolders(mRecentFolders); 1015 1016 // Remove any excluded folder types 1017 if (mExcludedFolderTypes != null) { 1018 final Iterator<Folder> iterator = recentFolderList.iterator(); 1019 while (iterator.hasNext()) { 1020 if (isFolderTypeExcluded(iterator.next())) { 1021 iterator.remove(); 1022 } 1023 } 1024 } 1025 1026 if (recentFolderList.size() > 0) { 1027 destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading)); 1028 // Recent folders are not queried for position. 1029 final int position = -1; 1030 for (Folder f : recentFolderList) { 1031 destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT, 1032 position)); 1033 } 1034 } 1035 } 1036 1037 /** 1038 * Check if the cursor provided is valid. 1039 * @param mCursor 1040 * @return True if cursor is invalid, false otherwise 1041 */ 1042 private boolean isCursorInvalid(Cursor mCursor) { 1043 return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0 1044 || !mCursor.moveToFirst(); 1045 } 1046 1047 @Override 1048 public void setCursor(ObjectCursor<Folder> cursor) { 1049 mCursor = cursor; 1050 recalculateList(); 1051 } 1052 1053 @Override 1054 public Object getItem(int position) { 1055 return mItemList.get(position); 1056 } 1057 1058 @Override 1059 public long getItemId(int position) { 1060 return getItem(position).hashCode(); 1061 } 1062 1063 @Override 1064 public final void destroy() { 1065 mRecentFolderObserver.unregisterAndDestroy(); 1066 } 1067 1068 @Override 1069 public int getItemType(DrawerItem item) { 1070 return item.mType; 1071 } 1072 1073 // TODO(viki): This is strange. We have the full folder and yet we create on from scratch. 1074 @Override 1075 public Folder getFullFolder(DrawerItem folderItem) { 1076 if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) { 1077 return folderItem.mFolder; 1078 } else { 1079 final int pos = folderItem.mPosition; 1080 if (pos > -1 && mCursor != null && !mCursor.isClosed() 1081 && mCursor.moveToPosition(folderItem.mPosition)) { 1082 return mCursor.getModel(); 1083 } else { 1084 return null; 1085 } 1086 } 1087 } 1088 } 1089 1090 private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder> 1091 implements FolderListFragmentCursorAdapter{ 1092 1093 private static final int PARENT = 0; 1094 private static final int CHILD = 1; 1095 private final Uri mParentUri; 1096 private final Folder mParent; 1097 private final FolderItemView.DropHandler mDropHandler; 1098 private ObjectCursor<Folder> mCursor; 1099 1100 public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) { 1101 super(mActivity.getActivityContext(), R.layout.folder_item); 1102 mDropHandler = mActivity; 1103 mParent = parentFolder; 1104 mParentUri = parentFolder.uri; 1105 setCursor(c); 1106 } 1107 1108 @Override 1109 public int getViewTypeCount() { 1110 // Child and Parent 1111 return 2; 1112 } 1113 1114 @Override 1115 public int getItemViewType(int position) { 1116 final Folder f = getItem(position); 1117 return f.uri.equals(mParentUri) ? PARENT : CHILD; 1118 } 1119 1120 @Override 1121 public View getView(int position, View convertView, ViewGroup parent) { 1122 final FolderItemView folderItemView; 1123 final Folder folder = getItem(position); 1124 boolean isParent = folder.uri.equals(mParentUri); 1125 if (convertView != null) { 1126 folderItemView = (FolderItemView) convertView; 1127 } else { 1128 int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item; 1129 folderItemView = (FolderItemView) LayoutInflater.from( 1130 mActivity.getActivityContext()).inflate(resId, null); 1131 } 1132 folderItemView.bind(folder, mDropHandler); 1133 if (folder.uri.equals(mSelectedFolderUri)) { 1134 getListView().setItemChecked(position, true); 1135 // If this is the current folder, also check to verify that the unread count 1136 // matches what the action bar shows. 1137 final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null) 1138 && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount; 1139 if (unreadCountDiffers) { 1140 folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount); 1141 } 1142 } 1143 Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block)); 1144 Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon)); 1145 return folderItemView; 1146 } 1147 1148 @Override 1149 public void setCursor(ObjectCursor<Folder> cursor) { 1150 mCursor = cursor; 1151 clear(); 1152 if (mParent != null) { 1153 add(mParent); 1154 } 1155 if (cursor != null && cursor.getCount() > 0) { 1156 cursor.moveToFirst(); 1157 do { 1158 Folder f = cursor.getModel(); 1159 f.parent = mParent; 1160 add(f); 1161 } while (cursor.moveToNext()); 1162 } 1163 } 1164 1165 @Override 1166 public void destroy() { 1167 // Do nothing. 1168 } 1169 1170 @Override 1171 public int getItemType(DrawerItem item) { 1172 // Always returns folders for now. 1173 return DrawerItem.VIEW_FOLDER; 1174 } 1175 1176 @Override 1177 public Folder getFullFolder(DrawerItem folderItem) { 1178 final int pos = folderItem.mPosition; 1179 if (mCursor == null || mCursor.isClosed()) { 1180 return null; 1181 } 1182 if (pos > -1 && mCursor != null && !mCursor.isClosed() 1183 && mCursor.moveToPosition(folderItem.mPosition)) { 1184 return mCursor.getModel(); 1185 } else { 1186 return null; 1187 } 1188 } 1189 1190 @Override 1191 public void notifyAllAccountsChanged() { 1192 // Do nothing. We don't care about changes to all accounts. 1193 } 1194 } 1195 1196 public Folder getParentFolder() { 1197 return mParentFolder; 1198 } 1199 1200 /** 1201 * Sets the currently selected folder safely. 1202 * @param folder 1203 */ 1204 private void setSelectedFolder(Folder folder) { 1205 if (folder == null) { 1206 mSelectedFolderUri = Uri.EMPTY; 1207 LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!"); 1208 return; 1209 } 1210 mCurrentFolderForUnreadCheck = folder; 1211 mSelectedFolderUri = folder.uri; 1212 setSelectedFolderType(folder); 1213 final boolean viewChanged = 1214 !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck); 1215 if (mCursorAdapter != null && viewChanged) { 1216 mCursorAdapter.notifyDataSetChanged(); 1217 } 1218 } 1219 1220 /** 1221 * Sets the selected folder type safely. 1222 * @param folder folder to set to. 1223 */ 1224 private void setSelectedFolderType(Folder folder) { 1225 if (mSelectedFolderType == DrawerItem.UNSET) { 1226 mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM 1227 : DrawerItem.FOLDER_USER; 1228 } 1229 } 1230 1231 /** 1232 * Sets the current account to the one provided here. 1233 * @param account the current account to set to. 1234 */ 1235 private void setSelectedAccount(Account account){ 1236 final boolean changed = (account != null) && (mCurrentAccount == null 1237 || !mCurrentAccount.uri.equals(account.uri)); 1238 mCurrentAccount = account; 1239 if (changed) { 1240 // We no longer have proper folder objects. Let the new ones come in 1241 mCursorAdapter.setCursor(null); 1242 // If currentAccount is different from the one we set, restart the loader. Look at the 1243 // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we 1244 // don't just do restartLoader. 1245 final LoaderManager manager = getLoaderManager(); 1246 manager.destroyLoader(FOLDER_LOADER_ID); 1247 manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this); 1248 // An updated cursor causes the entire list to refresh. No need to refresh the list. 1249 } else if (account == null) { 1250 // This should never happen currently, but is a safeguard against a very incorrect 1251 // non-null account -> null account transition. 1252 LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader."); 1253 final LoaderManager manager = getLoaderManager(); 1254 manager.destroyLoader(FOLDER_LOADER_ID); 1255 } 1256 } 1257 1258 public interface FolderListSelectionListener { 1259 public void onFolderSelected(Folder folder); 1260 } 1261 1262 /** 1263 * Get whether the FolderListFragment is currently showing the hierarchy 1264 * under a single parent. 1265 */ 1266 public boolean showingHierarchy() { 1267 return mParentFolder != null; 1268 } 1269 1270 /** 1271 * Checks if the specified {@link Folder} is a type that we want to exclude from displaying. 1272 */ 1273 private boolean isFolderTypeExcluded(final Folder folder) { 1274 if (mExcludedFolderTypes == null) { 1275 return false; 1276 } 1277 1278 for (final int excludedType : mExcludedFolderTypes) { 1279 if (folder.isType(excludedType)) { 1280 return true; 1281 } 1282 } 1283 1284 return false; 1285 } 1286} 1287