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