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