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