DirectoryFragment.java revision 1feeddf518d31a7ff5d28000a631857442cd7161
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.documentsui; 18 19import static com.android.documentsui.DocumentsActivity.TAG; 20import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE; 21import static com.android.documentsui.DocumentsActivity.State.MODE_GRID; 22import static com.android.documentsui.DocumentsActivity.State.MODE_LIST; 23import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN; 24import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN; 25import static com.android.documentsui.model.DocumentInfo.getCursorInt; 26import static com.android.documentsui.model.DocumentInfo.getCursorLong; 27import static com.android.documentsui.model.DocumentInfo.getCursorString; 28 29import android.app.Fragment; 30import android.app.FragmentManager; 31import android.app.FragmentTransaction; 32import android.app.LoaderManager.LoaderCallbacks; 33import android.content.ContentResolver; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Intent; 37import android.content.Loader; 38import android.database.Cursor; 39import android.graphics.Bitmap; 40import android.graphics.Point; 41import android.graphics.drawable.Drawable; 42import android.graphics.drawable.InsetDrawable; 43import android.net.Uri; 44import android.os.AsyncTask; 45import android.os.Bundle; 46import android.os.CancellationSignal; 47import android.os.Parcelable; 48import android.provider.DocumentsContract; 49import android.provider.DocumentsContract.Document; 50import android.text.format.DateUtils; 51import android.text.format.Formatter; 52import android.text.format.Time; 53import android.util.Log; 54import android.util.SparseArray; 55import android.util.SparseBooleanArray; 56import android.view.ActionMode; 57import android.view.LayoutInflater; 58import android.view.Menu; 59import android.view.MenuItem; 60import android.view.View; 61import android.view.ViewGroup; 62import android.widget.AbsListView; 63import android.widget.AbsListView.MultiChoiceModeListener; 64import android.widget.AbsListView.RecyclerListener; 65import android.widget.AdapterView; 66import android.widget.AdapterView.OnItemClickListener; 67import android.widget.BaseAdapter; 68import android.widget.FrameLayout; 69import android.widget.GridView; 70import android.widget.ImageView; 71import android.widget.ListView; 72import android.widget.TextView; 73import android.widget.Toast; 74 75import com.android.documentsui.DocumentsActivity.State; 76import com.android.documentsui.RecentsProvider.StateColumns; 77import com.android.documentsui.model.DocumentInfo; 78import com.android.documentsui.model.RootInfo; 79import com.android.internal.util.Predicate; 80import com.google.android.collect.Lists; 81 82import java.util.ArrayList; 83import java.util.List; 84import java.util.concurrent.atomic.AtomicInteger; 85 86/** 87 * Display the documents inside a single directory. 88 */ 89public class DirectoryFragment extends Fragment { 90 91 private View mEmptyView; 92 private ListView mListView; 93 private GridView mGridView; 94 95 private AbsListView mCurrentView; 96 97 private Predicate<DocumentInfo> mFilter; 98 99 public static final int TYPE_NORMAL = 1; 100 public static final int TYPE_SEARCH = 2; 101 public static final int TYPE_RECENT_OPEN = 3; 102 103 public static final int ANIM_NONE = 1; 104 public static final int ANIM_SIDE = 2; 105 public static final int ANIM_DOWN = 3; 106 public static final int ANIM_UP = 4; 107 108 private int mType = TYPE_NORMAL; 109 private String mStateKey; 110 111 private int mLastMode = MODE_UNKNOWN; 112 private int mLastSortOrder = SORT_ORDER_UNKNOWN; 113 private boolean mLastShowSize = false; 114 115 private boolean mHideGridTitles = false; 116 117 private Point mThumbSize; 118 119 private DocumentsAdapter mAdapter; 120 private LoaderCallbacks<DirectoryResult> mCallbacks; 121 122 private static final String EXTRA_TYPE = "type"; 123 private static final String EXTRA_ROOT = "root"; 124 private static final String EXTRA_DOC = "doc"; 125 private static final String EXTRA_QUERY = "query"; 126 private static final String EXTRA_IGNORE_STATE = "ignoreState"; 127 128 private static AtomicInteger sLoaderId = new AtomicInteger(4000); 129 130 private final int mLoaderId = sLoaderId.incrementAndGet(); 131 132 public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) { 133 show(fm, TYPE_NORMAL, root, doc, null, anim); 134 } 135 136 public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) { 137 show(fm, TYPE_SEARCH, root, null, query, anim); 138 } 139 140 public static void showRecentsOpen(FragmentManager fm, int anim) { 141 show(fm, TYPE_RECENT_OPEN, null, null, null, anim); 142 } 143 144 private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc, 145 String query, int anim) { 146 final Bundle args = new Bundle(); 147 args.putInt(EXTRA_TYPE, type); 148 args.putParcelable(EXTRA_ROOT, root); 149 args.putParcelable(EXTRA_DOC, doc); 150 args.putString(EXTRA_QUERY, query); 151 152 final FragmentTransaction ft = fm.beginTransaction(); 153 switch (anim) { 154 case ANIM_SIDE: 155 args.putBoolean(EXTRA_IGNORE_STATE, true); 156 break; 157 case ANIM_DOWN: 158 args.putBoolean(EXTRA_IGNORE_STATE, true); 159 ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen); 160 break; 161 case ANIM_UP: 162 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up); 163 break; 164 } 165 166 final DirectoryFragment fragment = new DirectoryFragment(); 167 fragment.setArguments(args); 168 169 ft.replace(R.id.container_directory, fragment); 170 ft.commitAllowingStateLoss(); 171 } 172 173 private static String buildStateKey(RootInfo root, DocumentInfo doc) { 174 final StringBuilder builder = new StringBuilder(); 175 builder.append(root != null ? root.authority : "null").append(';'); 176 builder.append(root != null ? root.rootId : "null").append(';'); 177 builder.append(doc != null ? doc.documentId : "null"); 178 return builder.toString(); 179 } 180 181 public static DirectoryFragment get(FragmentManager fm) { 182 // TODO: deal with multiple directories shown at once 183 return (DirectoryFragment) fm.findFragmentById(R.id.container_directory); 184 } 185 186 @Override 187 public View onCreateView( 188 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 189 final Context context = inflater.getContext(); 190 final View view = inflater.inflate(R.layout.fragment_directory, container, false); 191 192 mEmptyView = view.findViewById(android.R.id.empty); 193 194 mListView = (ListView) view.findViewById(R.id.list); 195 mListView.setOnItemClickListener(mItemListener); 196 mListView.setMultiChoiceModeListener(mMultiListener); 197 mListView.setRecyclerListener(mRecycleListener); 198 199 mGridView = (GridView) view.findViewById(R.id.grid); 200 mGridView.setOnItemClickListener(mItemListener); 201 mGridView.setMultiChoiceModeListener(mMultiListener); 202 mGridView.setRecyclerListener(mRecycleListener); 203 204 return view; 205 } 206 207 @Override 208 public void onActivityCreated(Bundle savedInstanceState) { 209 super.onActivityCreated(savedInstanceState); 210 211 final Context context = getActivity(); 212 final State state = getDisplayState(DirectoryFragment.this); 213 214 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); 215 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); 216 217 mAdapter = new DocumentsAdapter(); 218 mType = getArguments().getInt(EXTRA_TYPE); 219 mStateKey = buildStateKey(root, doc); 220 221 if (mType == TYPE_RECENT_OPEN) { 222 // Hide titles when showing recents for picking images/videos 223 mHideGridTitles = MimePredicate.mimeMatches( 224 MimePredicate.VISUAL_MIMES, state.acceptMimes); 225 } else { 226 mHideGridTitles = (doc != null) && doc.isGridTitlesHidden(); 227 } 228 229 mCallbacks = new LoaderCallbacks<DirectoryResult>() { 230 @Override 231 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { 232 final String query = getArguments().getString(EXTRA_QUERY); 233 234 Uri contentsUri; 235 switch (mType) { 236 case TYPE_NORMAL: 237 contentsUri = DocumentsContract.buildChildDocumentsUri( 238 doc.authority, doc.documentId); 239 if (state.action == ACTION_MANAGE) { 240 contentsUri = DocumentsContract.setManageMode(contentsUri); 241 } 242 return new DirectoryLoader( 243 context, mType, root, doc, contentsUri, state.userSortOrder); 244 case TYPE_SEARCH: 245 contentsUri = DocumentsContract.buildSearchDocumentsUri( 246 root.authority, root.rootId, query); 247 if (state.action == ACTION_MANAGE) { 248 contentsUri = DocumentsContract.setManageMode(contentsUri); 249 } 250 return new DirectoryLoader( 251 context, mType, root, doc, contentsUri, state.userSortOrder); 252 case TYPE_RECENT_OPEN: 253 final RootsCache roots = DocumentsApplication.getRootsCache(context); 254 return new RecentLoader(context, roots, state); 255 default: 256 throw new IllegalStateException("Unknown type " + mType); 257 } 258 } 259 260 @Override 261 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { 262 if (!isAdded()) return; 263 264 mAdapter.swapCursor(result.cursor); 265 266 // Push latest state up to UI 267 // TODO: if mode change was racing with us, don't overwrite it 268 if (result.mode != MODE_UNKNOWN) { 269 state.derivedMode = result.mode; 270 } 271 state.derivedSortOrder = result.sortOrder; 272 ((DocumentsActivity) context).onStateChanged(); 273 274 updateDisplayState(); 275 276 // Restore any previous instance state 277 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); 278 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { 279 getView().restoreHierarchyState(container); 280 } else if (mLastSortOrder != state.derivedSortOrder) { 281 mListView.smoothScrollToPosition(0); 282 mGridView.smoothScrollToPosition(0); 283 } 284 285 mLastSortOrder = state.derivedSortOrder; 286 } 287 288 @Override 289 public void onLoaderReset(Loader<DirectoryResult> loader) { 290 mAdapter.swapCursor(null); 291 } 292 }; 293 294 // Kick off loader at least once 295 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); 296 297 updateDisplayState(); 298 } 299 300 @Override 301 public void onStop() { 302 super.onStop(); 303 304 // Remember last scroll location 305 final SparseArray<Parcelable> container = new SparseArray<Parcelable>(); 306 getView().saveHierarchyState(container); 307 final State state = getDisplayState(this); 308 state.dirState.put(mStateKey, container); 309 } 310 311 @Override 312 public void onResume() { 313 super.onResume(); 314 updateDisplayState(); 315 } 316 317 public void onUserSortOrderChanged() { 318 // Sort order change always triggers reload; we'll trigger state change 319 // on the flip side. 320 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); 321 } 322 323 public void onUserModeChanged() { 324 final ContentResolver resolver = getActivity().getContentResolver(); 325 final State state = getDisplayState(this); 326 327 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); 328 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); 329 330 if (root != null && doc != null) { 331 final Uri stateUri = RecentsProvider.buildState( 332 root.authority, root.rootId, doc.documentId); 333 final ContentValues values = new ContentValues(); 334 values.put(StateColumns.MODE, state.userMode); 335 336 new AsyncTask<Void, Void, Void>() { 337 @Override 338 protected Void doInBackground(Void... params) { 339 resolver.insert(stateUri, values); 340 return null; 341 } 342 }.execute(); 343 } 344 345 // Mode change is just visual change; no need to kick loader, and 346 // deliver change event immediately. 347 state.derivedMode = state.userMode; 348 ((DocumentsActivity) getActivity()).onStateChanged(); 349 350 updateDisplayState(); 351 } 352 353 private void updateDisplayState() { 354 final State state = getDisplayState(this); 355 356 mFilter = new MimePredicate(state.acceptMimes); 357 358 if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return; 359 mLastMode = state.derivedMode; 360 mLastShowSize = state.showSize; 361 362 mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE); 363 mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE); 364 365 final int choiceMode; 366 if (state.allowMultiple) { 367 choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; 368 } else { 369 choiceMode = ListView.CHOICE_MODE_NONE; 370 } 371 372 final int thumbSize; 373 if (state.derivedMode == MODE_GRID) { 374 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); 375 mListView.setAdapter(null); 376 mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); 377 mGridView.setAdapter(mAdapter); 378 mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width)); 379 mGridView.setNumColumns(GridView.AUTO_FIT); 380 mGridView.setChoiceMode(choiceMode); 381 mCurrentView = mGridView; 382 } else if (state.derivedMode == MODE_LIST) { 383 thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size); 384 mGridView.setAdapter(null); 385 mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE); 386 mListView.setAdapter(mAdapter); 387 mListView.setChoiceMode(choiceMode); 388 mCurrentView = mListView; 389 } else { 390 throw new IllegalStateException("Unknown state " + state.derivedMode); 391 } 392 393 mThumbSize = new Point(thumbSize, thumbSize); 394 } 395 396 private OnItemClickListener mItemListener = new OnItemClickListener() { 397 @Override 398 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 399 final Cursor cursor = mAdapter.getItem(position); 400 if (cursor != null) { 401 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 402 if (mFilter.apply(doc)) { 403 ((DocumentsActivity) getActivity()).onDocumentPicked(doc); 404 } 405 } 406 } 407 }; 408 409 private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() { 410 @Override 411 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 412 mode.getMenuInflater().inflate(R.menu.mode_directory, menu); 413 return true; 414 } 415 416 @Override 417 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 418 final State state = getDisplayState(DirectoryFragment.this); 419 420 final MenuItem open = menu.findItem(R.id.menu_open); 421 final MenuItem share = menu.findItem(R.id.menu_share); 422 final MenuItem delete = menu.findItem(R.id.menu_delete); 423 424 final boolean manageMode = state.action == ACTION_MANAGE; 425 open.setVisible(!manageMode); 426 share.setVisible(manageMode); 427 delete.setVisible(manageMode); 428 429 return true; 430 } 431 432 @Override 433 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 434 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); 435 final ArrayList<DocumentInfo> docs = Lists.newArrayList(); 436 final int size = checked.size(); 437 for (int i = 0; i < size; i++) { 438 if (checked.valueAt(i)) { 439 final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); 440 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 441 docs.add(doc); 442 } 443 } 444 445 final int id = item.getItemId(); 446 if (id == R.id.menu_open) { 447 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); 448 mode.finish(); 449 return true; 450 451 } else if (id == R.id.menu_share) { 452 onShareDocuments(docs); 453 mode.finish(); 454 return true; 455 456 } else if (id == R.id.menu_delete) { 457 onDeleteDocuments(docs); 458 mode.finish(); 459 return true; 460 461 } else { 462 return false; 463 } 464 } 465 466 @Override 467 public void onDestroyActionMode(ActionMode mode) { 468 // ignored 469 } 470 471 @Override 472 public void onItemCheckedStateChanged( 473 ActionMode mode, int position, long id, boolean checked) { 474 if (checked) { 475 // Directories and footer items cannot be checked 476 boolean valid = false; 477 478 final Cursor cursor = mAdapter.getItem(position); 479 if (cursor != null) { 480 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 481 482 // Only valid if non-directory matches filter 483 final State state = getDisplayState(DirectoryFragment.this); 484 valid = !Document.MIME_TYPE_DIR.equals(docMimeType) 485 && MimePredicate.mimeMatches(state.acceptMimes, docMimeType); 486 } 487 488 if (!valid) { 489 mCurrentView.setItemChecked(position, false); 490 } 491 } 492 493 mode.setTitle(getResources() 494 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 495 } 496 }; 497 498 private RecyclerListener mRecycleListener = new RecyclerListener() { 499 @Override 500 public void onMovedToScrapHeap(View view) { 501 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb); 502 if (iconThumb != null) { 503 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); 504 if (oldTask != null) { 505 oldTask.reallyCancel(); 506 iconThumb.setTag(null); 507 } 508 } 509 } 510 }; 511 512 private void onShareDocuments(List<DocumentInfo> docs) { 513 Intent intent; 514 if (docs.size() == 1) { 515 final DocumentInfo doc = docs.get(0); 516 517 intent = new Intent(Intent.ACTION_SEND); 518 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 519 intent.addCategory(Intent.CATEGORY_DEFAULT); 520 intent.setType(doc.mimeType); 521 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri); 522 523 } else if (docs.size() > 1) { 524 intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 525 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 526 intent.addCategory(Intent.CATEGORY_DEFAULT); 527 528 final ArrayList<String> mimeTypes = Lists.newArrayList(); 529 final ArrayList<Uri> uris = Lists.newArrayList(); 530 for (DocumentInfo doc : docs) { 531 mimeTypes.add(doc.mimeType); 532 uris.add(doc.derivedUri); 533 } 534 535 intent.setType(findCommonMimeType(mimeTypes)); 536 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 537 538 } else { 539 return; 540 } 541 542 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); 543 startActivity(intent); 544 } 545 546 private void onDeleteDocuments(List<DocumentInfo> docs) { 547 final Context context = getActivity(); 548 final ContentResolver resolver = context.getContentResolver(); 549 550 boolean hadTrouble = false; 551 for (DocumentInfo doc : docs) { 552 if (!doc.isDeleteSupported()) { 553 Log.w(TAG, "Skipping " + doc); 554 hadTrouble = true; 555 continue; 556 } 557 558 if (!DocumentsContract.deleteDocument(resolver, doc.derivedUri)) { 559 Log.w(TAG, "Failed to delete " + doc); 560 hadTrouble = true; 561 } 562 } 563 564 if (hadTrouble) { 565 Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show(); 566 } 567 } 568 569 private static State getDisplayState(Fragment fragment) { 570 return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); 571 } 572 573 private static abstract class Footer { 574 private final int mItemViewType; 575 576 public Footer(int itemViewType) { 577 mItemViewType = itemViewType; 578 } 579 580 public abstract View getView(View convertView, ViewGroup parent); 581 582 public int getItemViewType() { 583 return mItemViewType; 584 } 585 } 586 587 private class LoadingFooter extends Footer { 588 public LoadingFooter() { 589 super(1); 590 } 591 592 @Override 593 public View getView(View convertView, ViewGroup parent) { 594 final Context context = parent.getContext(); 595 final State state = getDisplayState(DirectoryFragment.this); 596 597 if (convertView == null) { 598 final LayoutInflater inflater = LayoutInflater.from(context); 599 if (state.derivedMode == MODE_LIST) { 600 convertView = inflater.inflate(R.layout.item_loading_list, parent, false); 601 } else if (state.derivedMode == MODE_GRID) { 602 convertView = inflater.inflate(R.layout.item_loading_grid, parent, false); 603 } else { 604 throw new IllegalStateException(); 605 } 606 } 607 608 return convertView; 609 } 610 } 611 612 private class MessageFooter extends Footer { 613 private final int mIcon; 614 private final String mMessage; 615 616 public MessageFooter(int itemViewType, int icon, String message) { 617 super(itemViewType); 618 mIcon = icon; 619 mMessage = message; 620 } 621 622 @Override 623 public View getView(View convertView, ViewGroup parent) { 624 final Context context = parent.getContext(); 625 final State state = getDisplayState(DirectoryFragment.this); 626 627 if (convertView == null) { 628 final LayoutInflater inflater = LayoutInflater.from(context); 629 if (state.derivedMode == MODE_LIST) { 630 convertView = inflater.inflate(R.layout.item_message_list, parent, false); 631 } else if (state.derivedMode == MODE_GRID) { 632 convertView = inflater.inflate(R.layout.item_message_grid, parent, false); 633 } else { 634 throw new IllegalStateException(); 635 } 636 } 637 638 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 639 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 640 icon.setImageResource(mIcon); 641 title.setText(mMessage); 642 return convertView; 643 } 644 } 645 646 private class DocumentsAdapter extends BaseAdapter { 647 private Cursor mCursor; 648 private int mCursorCount; 649 650 private List<Footer> mFooters = Lists.newArrayList(); 651 652 public void swapCursor(Cursor cursor) { 653 mCursor = cursor; 654 mCursorCount = cursor != null ? cursor.getCount() : 0; 655 656 mFooters.clear(); 657 658 final Bundle extras = cursor != null ? cursor.getExtras() : null; 659 if (extras != null) { 660 final String info = extras.getString(DocumentsContract.EXTRA_INFO); 661 if (info != null) { 662 mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info)); 663 } 664 final String error = extras.getString(DocumentsContract.EXTRA_ERROR); 665 if (error != null) { 666 mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error)); 667 } 668 if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) { 669 mFooters.add(new LoadingFooter()); 670 } 671 } 672 673 if (isEmpty()) { 674 mEmptyView.setVisibility(View.VISIBLE); 675 } else { 676 mEmptyView.setVisibility(View.GONE); 677 } 678 679 notifyDataSetChanged(); 680 } 681 682 @Override 683 public View getView(int position, View convertView, ViewGroup parent) { 684 if (position < mCursorCount) { 685 return getDocumentView(position, convertView, parent); 686 } else { 687 position -= mCursorCount; 688 convertView = mFooters.get(position).getView(convertView, parent); 689 // Only the view itself is disabled; contents inside shouldn't 690 // be dimmed. 691 convertView.setEnabled(false); 692 return convertView; 693 } 694 } 695 696 private View getDocumentView(int position, View convertView, ViewGroup parent) { 697 final Context context = parent.getContext(); 698 final State state = getDisplayState(DirectoryFragment.this); 699 700 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); 701 702 final RootsCache roots = DocumentsApplication.getRootsCache(context); 703 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 704 context, mThumbSize); 705 706 if (convertView == null) { 707 final LayoutInflater inflater = LayoutInflater.from(context); 708 if (state.derivedMode == MODE_LIST) { 709 convertView = inflater.inflate(R.layout.item_doc_list, parent, false); 710 } else if (state.derivedMode == MODE_GRID) { 711 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); 712 713 // Apply padding to grid items 714 final FrameLayout grid = (FrameLayout) convertView; 715 final int gridPadding = getResources() 716 .getDimensionPixelSize(R.dimen.grid_padding); 717 grid.setForeground(new InsetDrawable(grid.getForeground(), gridPadding)); 718 grid.setBackground(new InsetDrawable(grid.getBackground(), gridPadding)); 719 } else { 720 throw new IllegalStateException(); 721 } 722 } 723 724 final Cursor cursor = getItem(position); 725 726 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); 727 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); 728 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); 729 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 730 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 731 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); 732 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); 733 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 734 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); 735 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); 736 737 final View line1 = convertView.findViewById(R.id.line1); 738 final View line2 = convertView.findViewById(R.id.line2); 739 740 final View icon = convertView.findViewById(android.R.id.icon); 741 final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime); 742 final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb); 743 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 744 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); 745 final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2); 746 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 747 final TextView date = (TextView) convertView.findViewById(R.id.date); 748 final TextView size = (TextView) convertView.findViewById(R.id.size); 749 750 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); 751 if (oldTask != null) { 752 oldTask.reallyCancel(); 753 iconThumb.setTag(null); 754 } 755 756 iconMime.animate().cancel(); 757 iconThumb.animate().cancel(); 758 759 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0; 760 final boolean allowThumbnail = (state.derivedMode == MODE_GRID) 761 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType); 762 final boolean showThumbnail = supportsThumbnail && allowThumbnail; 763 764 boolean cacheHit = false; 765 if (showThumbnail) { 766 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); 767 final Bitmap cachedResult = thumbs.get(uri); 768 if (cachedResult != null) { 769 iconThumb.setImageBitmap(cachedResult); 770 cacheHit = true; 771 } else { 772 iconThumb.setImageDrawable(null); 773 final ThumbnailAsyncTask task = new ThumbnailAsyncTask( 774 uri, iconMime, iconThumb, mThumbSize); 775 iconThumb.setTag(task); 776 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 777 } 778 } 779 780 // Always throw MIME icon into place, even when a thumbnail is being 781 // loaded in background. 782 if (cacheHit) { 783 iconMime.setAlpha(0f); 784 iconThumb.setAlpha(1f); 785 } else { 786 iconMime.setAlpha(1f); 787 iconThumb.setAlpha(0f); 788 if (docIcon != 0) { 789 iconMime.setImageDrawable( 790 IconUtils.loadPackageIcon(context, docAuthority, docIcon)); 791 } else { 792 iconMime.setImageDrawable(IconUtils.loadMimeIcon( 793 context, docMimeType, docAuthority, docId, state.derivedMode)); 794 } 795 } 796 797 boolean hasLine1 = false; 798 boolean hasLine2 = false; 799 800 final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles; 801 if (!hideTitle) { 802 title.setText(docDisplayName); 803 hasLine1 = true; 804 } 805 806 Drawable iconDrawable = null; 807 if (mType == TYPE_RECENT_OPEN) { 808 // We've already had to enumerate roots before any results can 809 // be shown, so this will never block. 810 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId); 811 iconDrawable = root.loadIcon(context); 812 813 if (summary != null) { 814 final boolean alwaysShowSummary = getResources() 815 .getBoolean(R.bool.always_show_summary); 816 if (alwaysShowSummary) { 817 summary.setText(root.getDirectoryString()); 818 summary.setVisibility(View.VISIBLE); 819 hasLine2 = true; 820 } else { 821 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) { 822 // No summary needed if icon speaks for itself 823 summary.setVisibility(View.INVISIBLE); 824 } else { 825 summary.setText(root.getDirectoryString()); 826 summary.setVisibility(View.VISIBLE); 827 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END); 828 hasLine2 = true; 829 } 830 } 831 } 832 } else { 833 // Directories showing thumbnails in grid mode get a little icon 834 // hint to remind user they're a directory. 835 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID 836 && showThumbnail) { 837 iconDrawable = context.getResources().getDrawable(R.drawable.ic_root_folder); 838 } 839 840 if (summary != null) { 841 if (docSummary != null) { 842 summary.setText(docSummary); 843 summary.setVisibility(View.VISIBLE); 844 hasLine2 = true; 845 } else { 846 summary.setVisibility(View.INVISIBLE); 847 } 848 } 849 } 850 851 if (icon1 != null) icon1.setVisibility(View.GONE); 852 if (icon2 != null) icon2.setVisibility(View.GONE); 853 854 if (iconDrawable != null) { 855 if (hasLine1) { 856 icon1.setVisibility(View.VISIBLE); 857 icon1.setImageDrawable(iconDrawable); 858 } else { 859 icon2.setVisibility(View.VISIBLE); 860 icon2.setImageDrawable(iconDrawable); 861 } 862 } 863 864 if (docLastModified == -1) { 865 date.setText(null); 866 } else { 867 date.setText(formatTime(context, docLastModified)); 868 hasLine2 = true; 869 } 870 871 if (state.showSize) { 872 size.setVisibility(View.VISIBLE); 873 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { 874 size.setText(null); 875 } else { 876 size.setText(Formatter.formatFileSize(context, docSize)); 877 hasLine2 = true; 878 } 879 } else { 880 size.setVisibility(View.GONE); 881 } 882 883 if (line1 != null) { 884 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE); 885 } 886 if (line2 != null) { 887 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE); 888 } 889 890 final boolean enabled = Document.MIME_TYPE_DIR.equals(docMimeType) 891 || MimePredicate.mimeMatches(state.acceptMimes, docMimeType); 892 if (enabled) { 893 setEnabledRecursive(convertView, true); 894 icon.setAlpha(1f); 895 if (icon1 != null) icon1.setAlpha(1f); 896 if (icon2 != null) icon2.setAlpha(1f); 897 } else { 898 setEnabledRecursive(convertView, false); 899 icon.setAlpha(0.5f); 900 if (icon1 != null) icon1.setAlpha(0.5f); 901 if (icon2 != null) icon2.setAlpha(0.5f); 902 } 903 904 return convertView; 905 } 906 907 @Override 908 public int getCount() { 909 return mCursorCount + mFooters.size(); 910 } 911 912 @Override 913 public Cursor getItem(int position) { 914 if (position < mCursorCount) { 915 mCursor.moveToPosition(position); 916 return mCursor; 917 } else { 918 return null; 919 } 920 } 921 922 @Override 923 public long getItemId(int position) { 924 return position; 925 } 926 927 @Override 928 public int getViewTypeCount() { 929 return 4; 930 } 931 932 @Override 933 public int getItemViewType(int position) { 934 if (position < mCursorCount) { 935 return 0; 936 } else { 937 position -= mCursorCount; 938 return mFooters.get(position).getItemViewType(); 939 } 940 } 941 } 942 943 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> { 944 private final Uri mUri; 945 private final ImageView mIconMime; 946 private final ImageView mIconThumb; 947 private final Point mThumbSize; 948 private final CancellationSignal mSignal; 949 950 public ThumbnailAsyncTask( 951 Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize) { 952 mUri = uri; 953 mIconMime = iconMime; 954 mIconThumb = iconThumb; 955 mThumbSize = thumbSize; 956 mSignal = new CancellationSignal(); 957 } 958 959 public void reallyCancel() { 960 cancel(false); 961 mSignal.cancel(); 962 } 963 964 @Override 965 protected Bitmap doInBackground(Uri... params) { 966 final Context context = mIconThumb.getContext(); 967 968 Bitmap result = null; 969 try { 970 // TODO: switch to using unstable provider 971 result = DocumentsContract.getDocumentThumbnail( 972 context.getContentResolver(), mUri, mThumbSize, mSignal); 973 if (result != null) { 974 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 975 context, mThumbSize); 976 thumbs.put(mUri, result); 977 } 978 } catch (Exception e) { 979 Log.w(TAG, "Failed to load thumbnail: " + e); 980 } 981 return result; 982 } 983 984 @Override 985 protected void onPostExecute(Bitmap result) { 986 if (mIconThumb.getTag() == this && result != null) { 987 mIconThumb.setTag(null); 988 mIconThumb.setImageBitmap(result); 989 990 mIconMime.setAlpha(1f); 991 mIconMime.animate().alpha(0f).start(); 992 mIconThumb.setAlpha(0f); 993 mIconThumb.animate().alpha(1f).start(); 994 } 995 } 996 } 997 998 private static String formatTime(Context context, long when) { 999 // TODO: DateUtils should make this easier 1000 Time then = new Time(); 1001 then.set(when); 1002 Time now = new Time(); 1003 now.setToNow(); 1004 1005 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT 1006 | DateUtils.FORMAT_ABBREV_ALL; 1007 1008 if (then.year != now.year) { 1009 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 1010 } else if (then.yearDay != now.yearDay) { 1011 flags |= DateUtils.FORMAT_SHOW_DATE; 1012 } else { 1013 flags |= DateUtils.FORMAT_SHOW_TIME; 1014 } 1015 1016 return DateUtils.formatDateTime(context, when, flags); 1017 } 1018 1019 private String findCommonMimeType(List<String> mimeTypes) { 1020 String[] commonType = mimeTypes.get(0).split("/"); 1021 if (commonType.length != 2) { 1022 return "*/*"; 1023 } 1024 1025 for (int i = 1; i < mimeTypes.size(); i++) { 1026 String[] type = mimeTypes.get(i).split("/"); 1027 if (type.length != 2) continue; 1028 1029 if (!commonType[1].equals(type[1])) { 1030 commonType[1] = "*"; 1031 } 1032 1033 if (!commonType[0].equals(type[0])) { 1034 commonType[0] = "*"; 1035 commonType[1] = "*"; 1036 break; 1037 } 1038 } 1039 1040 return commonType[0] + "/" + commonType[1]; 1041 } 1042 1043 private void setEnabledRecursive(View v, boolean enabled) { 1044 if (v == null) return; 1045 if (v.isEnabled() == enabled) return; 1046 v.setEnabled(enabled); 1047 1048 if (v instanceof ViewGroup) { 1049 final ViewGroup vg = (ViewGroup) v; 1050 for (int i = vg.getChildCount() - 1; i >= 0; i--) { 1051 setEnabledRecursive(vg.getChildAt(i), enabled); 1052 } 1053 } 1054 } 1055} 1056