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