FolderListFragment.java revision e716c856d59cc6741e9d9fa16a77bbccd12530ab
1/* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mail.ui; 19 20import android.app.Activity; 21import android.app.ListFragment; 22import android.app.LoaderManager; 23import android.content.CursorLoader; 24import android.content.Loader; 25import android.database.Cursor; 26import android.database.DataSetObserver; 27import android.net.Uri; 28import android.os.Bundle; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.ArrayAdapter; 33import android.widget.BaseAdapter; 34import android.widget.ImageView; 35import android.widget.ListAdapter; 36import android.widget.ListView; 37import android.widget.TextView; 38 39import com.android.mail.R; 40import com.android.mail.providers.Folder; 41import com.android.mail.providers.RecentFolderObserver; 42import com.android.mail.providers.UIProvider; 43import com.android.mail.utils.LogTag; 44import com.android.mail.utils.LogUtils; 45import com.android.mail.utils.Utils; 46 47import java.util.ArrayList; 48import java.util.List; 49 50/** 51 * The folder list UI component. 52 */ 53public final class FolderListFragment extends ListFragment implements 54 LoaderManager.LoaderCallbacks<Cursor> { 55 private static final String LOG_TAG = LogTag.getLogTag(); 56 /** The parent activity */ 57 private ControllableActivity mActivity; 58 /** The underlying list view */ 59 private ListView mListView; 60 /** URI that points to the list of folders for the current account. */ 61 private Uri mFolderListUri; 62 /** True if you want a sectioned FolderList, false otherwise. */ 63 private boolean mIsSectioned; 64 /** Callback into the parent */ 65 private FolderListSelectionListener mListener; 66 67 /** The currently selected folder (the folder being viewed). This is never null. */ 68 private Uri mSelectedFolderUri = Uri.EMPTY; 69 /** Parent of the current folder, or null if the current folder is not a child. */ 70 private Folder mParentFolder; 71 72 private static final int FOLDER_LOADER_ID = 0; 73 public static final int MODE_DEFAULT = 0; 74 public static final int MODE_PICK = 1; 75 /** Key to store {@link #mParentFolder}. */ 76 private static final String ARG_PARENT_FOLDER = "arg-parent-folder"; 77 /** Key to store {@link #mFolderListUri}. */ 78 private static final String ARG_FOLDER_URI = "arg-folder-list-uri"; 79 /** Key to store {@link #mIsSectioned} */ 80 private static final String ARG_IS_SECTIONED = "arg-is-sectioned"; 81 82 private static final String BUNDLE_LIST_STATE = "flf-list-state"; 83 private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder"; 84 85 private FolderListFragmentCursorAdapter mCursorAdapter; 86 /** View that we show while we are waiting for the folder list to load */ 87 private View mEmptyView; 88 /** Observer to wait for changes to the current folder so we can change the selected folder */ 89 private FolderObserver mFolderObserver = null; 90 91 // Listen to folder changes from the controller and update state accordingly. 92 private class FolderObserver extends DataSetObserver { 93 @Override 94 public void onChanged() { 95 if (mActivity == null) { 96 return; 97 } 98 final FolderController controller = mActivity.getFolderController(); 99 if (controller == null) { 100 return; 101 } 102 final Folder folder = controller.getFolder(); 103 if (folder == null) { 104 return; 105 } 106 mSelectedFolderUri = folder.uri; 107 } 108 } 109 110 /** 111 * Constructor needs to be public to handle orientation changes and activity lifecycle events. 112 */ 113 public FolderListFragment() { 114 super(); 115 } 116 117 @Override 118 public void onResume() { 119 Utils.dumpLayoutRequests("FLF(" + this + ").onResume()", getView()); 120 121 super.onResume(); 122 // Hacky workaround for http://b/6946182 123 Utils.fixSubTreeLayoutIfOrphaned(getView(), "FolderListFragment"); 124 } 125 /** 126 * Creates a new instance of {@link ConversationListFragment}, initialized 127 * to display conversation list context. 128 * @param isSectioned TODO(viki): 129 */ 130 public static FolderListFragment newInstance(Folder parentFolder, Uri folderUri, 131 boolean isSectioned) { 132 final FolderListFragment fragment = new FolderListFragment(); 133 final Bundle args = new Bundle(); 134 if (parentFolder != null) { 135 args.putParcelable(ARG_PARENT_FOLDER, parentFolder); 136 } 137 args.putString(ARG_FOLDER_URI, folderUri.toString()); 138 args.putBoolean(ARG_IS_SECTIONED, isSectioned); 139 fragment.setArguments(args); 140 return fragment; 141 } 142 143 @Override 144 public void onActivityCreated(Bundle savedState) { 145 super.onActivityCreated(savedState); 146 // Strictly speaking, we get back an android.app.Activity from getActivity. However, the 147 // only activity creating a ConversationListContext is a MailActivity which is of type 148 // ControllableActivity, so this cast should be safe. If this cast fails, some other 149 // activity is creating ConversationListFragments. This activity must be of type 150 // ControllableActivity. 151 final Activity activity = getActivity(); 152 if (! (activity instanceof ControllableActivity)){ 153 LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" + 154 "create it. Cannot proceed."); 155 } 156 mActivity = (ControllableActivity) activity; 157 final FolderController controller = mActivity.getFolderController(); 158 // Listen to folder changes in the future 159 mFolderObserver = new FolderObserver(); 160 if (controller != null) { 161 // Only register for selected folder updates if we have a controller. 162 controller.registerFolderObserver(mFolderObserver); 163 } 164 165 mListener = mActivity.getFolderListSelectionListener(); 166 if (mActivity.isFinishing()) { 167 // Activity is finishing, just bail. 168 return; 169 } 170 171 if (mParentFolder != null) { 172 mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder); 173 } else { 174 mCursorAdapter = new FolderListAdapter(R.layout.folder_item, mIsSectioned); 175 } 176 setListAdapter(mCursorAdapter); 177 178 selectInitialFolder(mActivity.getHierarchyFolder()); 179 getLoaderManager().initLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this); 180 } 181 182 @Override 183 public View onCreateView(LayoutInflater inflater, ViewGroup container, 184 Bundle savedState) { 185 final Bundle args = getArguments(); 186 mFolderListUri = Uri.parse(args.getString(ARG_FOLDER_URI)); 187 mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER); 188 mIsSectioned = args.getBoolean(ARG_IS_SECTIONED); 189 final View rootView = inflater.inflate(R.layout.folder_list, null); 190 mListView = (ListView) rootView.findViewById(android.R.id.list); 191 mListView.setHeaderDividersEnabled(false); 192 mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 193 mListView.setEmptyView(null); 194 if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) { 195 mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE)); 196 } 197 mEmptyView = rootView.findViewById(R.id.empty_view); 198 if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) { 199 mSelectedFolderUri = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER)); 200 } else if (mParentFolder != null) { 201 mSelectedFolderUri = mParentFolder.uri; 202 } 203 Utils.dumpLayoutRequests("FLF(" + this + ").onCreateView()", rootView); 204 205 return rootView; 206 } 207 208 @Override 209 public void onStart() { 210 Utils.dumpLayoutRequests("FLF(" + this + ").onStart()", getView()); 211 super.onStart(); 212 } 213 214 @Override 215 public void onStop() { 216 Utils.dumpLayoutRequests("FLF(" + this + ").onStop()", getView()); 217 super.onStop(); 218 } 219 220 @Override 221 public void onPause() { 222 Utils.dumpLayoutRequests("FLF(" + this + ").onPause()", getView()); 223 super.onPause(); 224 } 225 226 @Override 227 public void onSaveInstanceState(Bundle outState) { 228 super.onSaveInstanceState(outState); 229 if (mListView != null) { 230 outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState()); 231 } 232 if (mSelectedFolderUri != null) { 233 outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString()); 234 } 235 outState.putBoolean(ARG_IS_SECTIONED, mIsSectioned); 236 } 237 238 @Override 239 public void onDestroyView() { 240 Utils.dumpLayoutRequests("FLF(" + this + ").onDestoryView()", getView()); 241 if (mCursorAdapter != null) { 242 mCursorAdapter.destroy(); 243 } 244 // Clear the adapter. 245 setListAdapter(null); 246 if (mFolderObserver != null) { 247 FolderController controller = mActivity.getFolderController(); 248 if (controller != null) { 249 controller.unregisterFolderObserver(mFolderObserver); 250 mFolderObserver = null; 251 } 252 } 253 super.onDestroyView(); 254 } 255 256 @Override 257 public void onListItemClick(ListView l, View v, int position, long id) { 258 viewFolder(position); 259 } 260 261 /** 262 * Display the conversation list from the folder at the position given. 263 * @param position 264 */ 265 private void viewFolder(int position) { 266 Object item = getListAdapter().getItem(position); 267 final Folder folder; 268 if (item instanceof FolderListAdapter.Item) { 269 FolderListAdapter.Item folderItem = (FolderListAdapter.Item) item; 270 folder = folderItem.mFolder; 271 ((FolderListAdapter) getListAdapter()).setSelectedType(folderItem.mFolderType); 272 } else if (item instanceof Folder) { 273 folder = (Folder) item; 274 } else { 275 folder = new Folder((Cursor) item); 276 } 277 if (folder != null) { 278 // Since we may be looking at hierarchical views, if we can 279 // determine the parent of the folder we have tapped, set it here. 280 // If we are looking at the folder we are already viewing, don't 281 // update its parent! 282 folder.parent = folder.equals(mParentFolder) ? null : mParentFolder; 283 // Go to the conversation list for this folder. 284 mListener.onFolderSelected(folder); 285 } 286 } 287 288 @Override 289 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 290 mListView.setEmptyView(null); 291 mEmptyView.setVisibility(View.GONE); 292 return new CursorLoader(mActivity.getActivityContext(), mFolderListUri, 293 UIProvider.FOLDERS_PROJECTION, null, null, null); 294 } 295 296 @Override 297 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 298 mCursorAdapter.setCursor(data); 299 if (data == null || data.getCount() == 0) { 300 mEmptyView.setVisibility(View.VISIBLE); 301 mListView.setEmptyView(mEmptyView); 302 } 303 } 304 305 @Override 306 public void onLoaderReset(Loader<Cursor> loader) { 307 mCursorAdapter.setCursor(null); 308 } 309 310 /** 311 * Interface for all cursor adpaters that allow setting a cursor and being destroyed. 312 */ 313 private interface FolderListFragmentCursorAdapter extends ListAdapter { 314 /** Update the folder list cursor with the cursor given here. */ 315 void setCursor(Cursor cursor); 316 /** Remove all observers and destroy the object. */ 317 void destroy(); 318 } 319 320 /** 321 * An adapter for flat folder lists. 322 */ 323 private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter { 324 325 private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() { 326 @Override 327 public void onChanged() { 328 recalculateList(); 329 } 330 }; 331 332 private final RecentFolderList mRecentFolders; 333 /** True if the list is sectioned, false otherwise */ 334 private final boolean mIsSectioned; 335 private final LayoutInflater mInflater; 336 /** All the items */ 337 private final List<Item> mItemList = new ArrayList<Item>(); 338 /** Cursor into the folder list. This might be null. */ 339 private Cursor mCursor = null; 340 /** 341 * Type of currently selected folder: {@link Item#FOLDER_SYSTEM}, {@link Item#FOLDER_RECENT} 342 * or {@link Item#FOLDER_USER} 343 */ 344 private int mSelectedFolderType; 345 346 /** A union of either a folder or a resource string */ 347 private class Item { 348 public final Folder mFolder; 349 public final int mResource; 350 /** Either {@link #VIEW_FOLDER} or {@link #VIEW_HEADER} */ 351 public final int mType; 352 /** A normal folder, also a child, if a parent is specified. */ 353 private static final int VIEW_FOLDER = 0; 354 /** A text-label which serves as a header in sectioned lists. */ 355 private static final int VIEW_HEADER = 1; 356 357 /** 358 * Either {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or {@link #FOLDER_USER} when 359 * {@link #mType} is {@link #VIEW_FOLDER}, and {@link #NOT_A_FOLDER} otherwise. 360 */ 361 public final int mFolderType; 362 private static final int NOT_A_FOLDER = 0; 363 private static final int FOLDER_SYSTEM = 1; 364 private static final int FOLDER_RECENT = 2; 365 private static final int FOLDER_USER = 3; 366 367 /** 368 * Create a folder item with the given type. 369 * @param folder 370 * @param folderType one of {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or 371 * {@link #FOLDER_USER} 372 */ 373 private Item(Folder folder, int folderType) { 374 mFolder = folder; 375 mResource = -1; 376 mType = VIEW_FOLDER; 377 mFolderType = folderType; 378 } 379 /** 380 * Create a header item with a string resource. 381 * @param resource the string resource: R.string.all_folders_heading 382 */ 383 private Item(int resource) { 384 mFolder = null; 385 mResource = resource; 386 mType = VIEW_HEADER; 387 mFolderType = NOT_A_FOLDER; 388 } 389 390 private final View getView(int position, View convertView, ViewGroup parent) { 391 if (mType == VIEW_FOLDER) { 392 return getFolderView(position, convertView, parent); 393 } else { 394 return getHeaderView(position, convertView, parent); 395 } 396 } 397 398 /** 399 * Returns a text divider between sections. 400 * @param convertView 401 * @param parent 402 * @return a text header at the given position. 403 */ 404 private final View getHeaderView(int position, View convertView, ViewGroup parent) { 405 final TextView headerView; 406 if (convertView != null) { 407 headerView = (TextView) convertView; 408 } else { 409 headerView = (TextView) mInflater.inflate( 410 R.layout.folder_list_header, parent, false); 411 } 412 headerView.setText(mResource); 413 return headerView; 414 } 415 416 /** 417 * Return a folder: either a parent folder or a normal (child or flat) 418 * folder. 419 * @param position 420 * @param convertView 421 * @param parent 422 * @return a view showing a folder at the given position. 423 */ 424 private final View getFolderView(int position, View convertView, ViewGroup parent) { 425 final FolderItemView folderItemView; 426 if (convertView != null) { 427 folderItemView = (FolderItemView) convertView; 428 } else { 429 folderItemView = 430 (FolderItemView) mInflater.inflate(R.layout.folder_item, null, false); 431 } 432 folderItemView.bind(mFolder, mActivity, false); 433 if (mListView != null) { 434 final boolean isSelected = (mFolderType == mSelectedFolderType) 435 && mFolder.uri.equals(mSelectedFolderUri); 436 mListView.setItemChecked(position, isSelected); 437 } 438 Folder.setFolderBlockColor(mFolder, folderItemView.findViewById(R.id.color_block)); 439 Folder.setIcon(mFolder, (ImageView) folderItemView.findViewById(R.id.folder_box)); 440 return folderItemView; 441 } 442 } 443 444 /** 445 * Creates a {@link FolderListAdapter}.This is a flat folder list of all the folders for the 446 * given account. 447 * @param layout 448 * @param isSectioned TODO(viki): 449 */ 450 public FolderListAdapter(int layout, boolean isSectioned) { 451 super(); 452 mInflater = LayoutInflater.from(mActivity.getActivityContext()); 453 mIsSectioned = isSectioned; 454 final RecentFolderController controller = mActivity.getRecentFolderController(); 455 if (controller != null && mIsSectioned) { 456 mRecentFolders = mRecentFolderObserver.initialize(controller); 457 } else { 458 mRecentFolders = null; 459 } 460 } 461 /** 462 * Sets the currently selected folder's type to the type given here. 463 */ 464 public void setSelectedType(int type) { 465 mSelectedFolderType = type; 466 } 467 468 @Override 469 public View getView(int position, View convertView, ViewGroup parent) { 470 return ((Item) getItem(position)).getView(position, convertView, parent); 471 } 472 473 @Override 474 public int getViewTypeCount() { 475 // Headers and folders 476 return 2; 477 } 478 479 @Override 480 public int getItemViewType(int position) { 481 return ((Item) getItem(position)).mType; 482 } 483 484 @Override 485 public int getCount() { 486 return mItemList.size(); 487 } 488 489 @Override 490 public boolean isEnabled(int position) { 491 // We disallow taps on headers 492 return ((Item) getItem(position)).mType != Item.VIEW_HEADER; 493 } 494 495 @Override 496 public boolean areAllItemsEnabled() { 497 // The headers are not enabled. 498 return false; 499 } 500 501 /** 502 * Returns all the recent folders from the list given here. Safe to call with a null list. 503 * @param recentList 504 * @return a valid list of folders, which are all recent folders. 505 */ 506 private final List<Folder> getRecentFolders(RecentFolderList recentList) { 507 final List<Folder> folderList = new ArrayList<Folder>(); 508 if (recentList == null) { 509 return folderList; 510 } 511 // Get all recent folders, after removing system folders. 512 for (final Folder f : recentList.getRecentFolderList(null)) { 513 if (!f.isProviderFolder()) { 514 folderList.add(f); 515 } 516 } 517 return folderList; 518 } 519 520 /** 521 * Recalculates the system, recent and user label lists. Notifies that the data has changed. 522 * This method modifies all the three lists on every single invocation. 523 */ 524 private void recalculateList() { 525 if (mCursor == null || mCursor.getCount() <= 0 || !mCursor.moveToFirst()) { 526 return; 527 } 528 mItemList.clear(); 529 if (!mIsSectioned) { 530 // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers. 531 do { 532 final Folder f = new Folder(mCursor); 533 mItemList.add(new Item(f, Item.FOLDER_USER)); 534 } while (mCursor.moveToNext()); 535 // Ask the list to invalidate its views. 536 notifyDataSetChanged(); 537 return; 538 } 539 540 // Otherwise, this is an adapter for a sectioned list. 541 // First add all the system folders. 542 final List<Folder> userFolderList = new ArrayList<Folder>(); 543 do { 544 final Folder f = new Folder(mCursor); 545 if (f.isProviderFolder()) { 546 mItemList.add(new Item(f, Item.FOLDER_SYSTEM)); 547 } else { 548 userFolderList.add(f); 549 } 550 } while (mCursor.moveToNext()); 551 // If there are recent folders, add them and a header. 552 final List<Folder> recentFolderList = getRecentFolders(mRecentFolders); 553 if (recentFolderList.size() > 0) { 554 mItemList.add(new Item(R.string.recent_folders_heading)); 555 for (Folder f : recentFolderList) { 556 mItemList.add(new Item(f, Item.FOLDER_RECENT)); 557 } 558 } 559 // If there are user folders, add them and a header. 560 if (userFolderList.size() > 0) { 561 mItemList.add(new Item(R.string.all_folders_heading)); 562 for (final Folder f : userFolderList) { 563 mItemList.add(new Item(f, Item.FOLDER_USER)); 564 } 565 } 566 // Ask the list to invalidate its views. 567 notifyDataSetChanged(); 568 } 569 570 @Override 571 public void setCursor(Cursor cursor) { 572 mCursor = cursor; 573 recalculateList(); 574 } 575 576 @Override 577 public Object getItem(int position) { 578 return mItemList.get(position); 579 } 580 581 @Override 582 public long getItemId(int position) { 583 return getItem(position).hashCode(); 584 } 585 586 @Override 587 public final void destroy() { 588 mRecentFolderObserver.unregisterAndDestroy(); 589 } 590 } 591 592 private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder> 593 implements FolderListFragmentCursorAdapter{ 594 595 private static final int PARENT = 0; 596 private static final int CHILD = 1; 597 private final Uri mParentUri; 598 private final Folder mParent; 599 private final FolderItemView.DropHandler mDropHandler; 600 601 public HierarchicalFolderListAdapter(Cursor c, Folder parentFolder) { 602 super(mActivity.getActivityContext(), R.layout.folder_item); 603 mDropHandler = mActivity; 604 mParent = parentFolder; 605 mParentUri = parentFolder.uri; 606 setCursor(c); 607 } 608 609 @Override 610 public int getViewTypeCount() { 611 // Child and Parent 612 return 2; 613 } 614 615 @Override 616 public int getItemViewType(int position) { 617 Folder f = getItem(position); 618 return f.uri.equals(mParentUri) ? PARENT : CHILD; 619 } 620 621 @Override 622 public View getView(int position, View convertView, ViewGroup parent) { 623 FolderItemView folderItemView; 624 Folder folder = getItem(position); 625 boolean isParent = folder.uri.equals(mParentUri); 626 if (convertView != null) { 627 folderItemView = (FolderItemView) convertView; 628 } else { 629 int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item; 630 folderItemView = (FolderItemView) LayoutInflater.from( 631 mActivity.getActivityContext()).inflate(resId, null); 632 } 633 folderItemView.bind(folder, mDropHandler, isParent); 634 if (folder.uri.equals(mSelectedFolderUri)) { 635 getListView().setItemChecked(position, true); 636 } 637 Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.folder_box)); 638 return folderItemView; 639 } 640 641 @Override 642 public void setCursor(Cursor cursor) { 643 clear(); 644 if (mParent != null) { 645 add(mParent); 646 } 647 if (cursor != null && cursor.getCount() > 0) { 648 cursor.moveToFirst(); 649 do { 650 Folder f = new Folder(cursor); 651 f.parent = mParent; 652 add(f); 653 } while (cursor.moveToNext()); 654 } 655 } 656 657 @Override 658 public void destroy() { 659 // Do nothing. 660 } 661 } 662 663 public void selectInitialFolder(Folder folder) { 664 if (folder == null) { 665 mSelectedFolderUri = Uri.EMPTY; 666 return; 667 } 668 mSelectedFolderUri = folder.uri; 669 } 670 671 public interface FolderListSelectionListener { 672 public void onFolderSelected(Folder folder); 673 } 674 675 /** 676 * Get whether the FolderListFragment is currently showing the hierarchy 677 * under a single parent. 678 */ 679 public boolean showingHierarchy() { 680 return mParentFolder != null; 681 } 682} 683