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