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