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