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