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