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