DirectoryFragment.java revision a82c2e24bc226c123075331cdaafb171b3b07b85
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 // When launched into empty recents, show drawer 295 if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) { 296 ((DocumentsActivity) context).setRootsDrawerOpen(true); 297 } 298 299 // Restore any previous instance state 300 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); 301 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { 302 getView().restoreHierarchyState(container); 303 } else if (mLastSortOrder != state.derivedSortOrder) { 304 mListView.smoothScrollToPosition(0); 305 mGridView.smoothScrollToPosition(0); 306 } 307 308 mLastSortOrder = state.derivedSortOrder; 309 } 310 311 @Override 312 public void onLoaderReset(Loader<DirectoryResult> loader) { 313 mAdapter.swapResult(null); 314 } 315 }; 316 317 // Kick off loader at least once 318 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); 319 320 updateDisplayState(); 321 } 322 323 @Override 324 public void onStop() { 325 super.onStop(); 326 327 // Remember last scroll location 328 final SparseArray<Parcelable> container = new SparseArray<Parcelable>(); 329 getView().saveHierarchyState(container); 330 final State state = getDisplayState(this); 331 state.dirState.put(mStateKey, container); 332 } 333 334 @Override 335 public void onResume() { 336 super.onResume(); 337 updateDisplayState(); 338 } 339 340 public void onUserSortOrderChanged() { 341 // Sort order change always triggers reload; we'll trigger state change 342 // on the flip side. 343 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); 344 } 345 346 public void onUserModeChanged() { 347 final ContentResolver resolver = getActivity().getContentResolver(); 348 final State state = getDisplayState(this); 349 350 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); 351 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); 352 353 if (root != null && doc != null) { 354 final Uri stateUri = RecentsProvider.buildState( 355 root.authority, root.rootId, doc.documentId); 356 final ContentValues values = new ContentValues(); 357 values.put(StateColumns.MODE, state.userMode); 358 359 new AsyncTask<Void, Void, Void>() { 360 @Override 361 protected Void doInBackground(Void... params) { 362 resolver.insert(stateUri, values); 363 return null; 364 } 365 }.execute(); 366 } 367 368 // Mode change is just visual change; no need to kick loader, and 369 // deliver change event immediately. 370 state.derivedMode = state.userMode; 371 ((DocumentsActivity) getActivity()).onStateChanged(); 372 373 updateDisplayState(); 374 } 375 376 private void updateDisplayState() { 377 final State state = getDisplayState(this); 378 379 if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return; 380 mLastMode = state.derivedMode; 381 mLastShowSize = state.showSize; 382 383 mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE); 384 mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE); 385 386 final int choiceMode; 387 if (state.allowMultiple) { 388 choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; 389 } else { 390 choiceMode = ListView.CHOICE_MODE_NONE; 391 } 392 393 final int thumbSize; 394 if (state.derivedMode == MODE_GRID) { 395 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); 396 mListView.setAdapter(null); 397 mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); 398 mGridView.setAdapter(mAdapter); 399 mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width)); 400 mGridView.setNumColumns(GridView.AUTO_FIT); 401 mGridView.setChoiceMode(choiceMode); 402 mCurrentView = mGridView; 403 } else if (state.derivedMode == MODE_LIST) { 404 thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size); 405 mGridView.setAdapter(null); 406 mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE); 407 mListView.setAdapter(mAdapter); 408 mListView.setChoiceMode(choiceMode); 409 mCurrentView = mListView; 410 } else { 411 throw new IllegalStateException("Unknown state " + state.derivedMode); 412 } 413 414 mThumbSize = new Point(thumbSize, thumbSize); 415 } 416 417 private OnItemClickListener mItemListener = new OnItemClickListener() { 418 @Override 419 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 420 final Cursor cursor = mAdapter.getItem(position); 421 if (cursor != null) { 422 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 423 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 424 if (isDocumentEnabled(docMimeType, docFlags)) { 425 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 426 ((DocumentsActivity) getActivity()).onDocumentPicked(doc); 427 } 428 } 429 } 430 }; 431 432 private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() { 433 @Override 434 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 435 mode.getMenuInflater().inflate(R.menu.mode_directory, menu); 436 return true; 437 } 438 439 @Override 440 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 441 final State state = getDisplayState(DirectoryFragment.this); 442 443 final MenuItem open = menu.findItem(R.id.menu_open); 444 final MenuItem share = menu.findItem(R.id.menu_share); 445 final MenuItem delete = menu.findItem(R.id.menu_delete); 446 447 final boolean manageMode = state.action == ACTION_MANAGE; 448 open.setVisible(!manageMode); 449 share.setVisible(manageMode); 450 delete.setVisible(manageMode); 451 452 return true; 453 } 454 455 @Override 456 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 457 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); 458 final ArrayList<DocumentInfo> docs = Lists.newArrayList(); 459 final int size = checked.size(); 460 for (int i = 0; i < size; i++) { 461 if (checked.valueAt(i)) { 462 final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); 463 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 464 docs.add(doc); 465 } 466 } 467 468 final int id = item.getItemId(); 469 if (id == R.id.menu_open) { 470 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); 471 mode.finish(); 472 return true; 473 474 } else if (id == R.id.menu_share) { 475 onShareDocuments(docs); 476 mode.finish(); 477 return true; 478 479 } else if (id == R.id.menu_delete) { 480 onDeleteDocuments(docs); 481 mode.finish(); 482 return true; 483 484 } else { 485 return false; 486 } 487 } 488 489 @Override 490 public void onDestroyActionMode(ActionMode mode) { 491 // ignored 492 } 493 494 @Override 495 public void onItemCheckedStateChanged( 496 ActionMode mode, int position, long id, boolean checked) { 497 if (checked) { 498 // Directories and footer items cannot be checked 499 boolean valid = false; 500 501 final Cursor cursor = mAdapter.getItem(position); 502 if (cursor != null) { 503 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 504 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 505 if (!Document.MIME_TYPE_DIR.equals(docMimeType)) { 506 valid = isDocumentEnabled(docMimeType, docFlags); 507 } 508 } 509 510 if (!valid) { 511 mCurrentView.setItemChecked(position, false); 512 } 513 } 514 515 mode.setTitle(getResources() 516 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 517 } 518 }; 519 520 private RecyclerListener mRecycleListener = new RecyclerListener() { 521 @Override 522 public void onMovedToScrapHeap(View view) { 523 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb); 524 if (iconThumb != null) { 525 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); 526 if (oldTask != null) { 527 oldTask.reallyCancel(); 528 iconThumb.setTag(null); 529 } 530 } 531 } 532 }; 533 534 private void onShareDocuments(List<DocumentInfo> docs) { 535 Intent intent; 536 if (docs.size() == 1) { 537 final DocumentInfo doc = docs.get(0); 538 539 intent = new Intent(Intent.ACTION_SEND); 540 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 541 intent.addCategory(Intent.CATEGORY_DEFAULT); 542 intent.setType(doc.mimeType); 543 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri); 544 545 } else if (docs.size() > 1) { 546 intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 547 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 548 intent.addCategory(Intent.CATEGORY_DEFAULT); 549 550 final ArrayList<String> mimeTypes = Lists.newArrayList(); 551 final ArrayList<Uri> uris = Lists.newArrayList(); 552 for (DocumentInfo doc : docs) { 553 mimeTypes.add(doc.mimeType); 554 uris.add(doc.derivedUri); 555 } 556 557 intent.setType(findCommonMimeType(mimeTypes)); 558 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 559 560 } else { 561 return; 562 } 563 564 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); 565 startActivity(intent); 566 } 567 568 private void onDeleteDocuments(List<DocumentInfo> docs) { 569 final Context context = getActivity(); 570 final ContentResolver resolver = context.getContentResolver(); 571 572 boolean hadTrouble = false; 573 for (DocumentInfo doc : docs) { 574 if (!doc.isDeleteSupported()) { 575 Log.w(TAG, "Skipping " + doc); 576 hadTrouble = true; 577 continue; 578 } 579 580 ContentProviderClient client = null; 581 try { 582 client = DocumentsApplication.acquireUnstableProviderOrThrow( 583 resolver, doc.derivedUri.getAuthority()); 584 DocumentsContract.deleteDocument(client, doc.derivedUri); 585 } catch (Exception e) { 586 Log.w(TAG, "Failed to delete " + doc); 587 hadTrouble = true; 588 } finally { 589 ContentProviderClient.releaseQuietly(client); 590 } 591 } 592 593 if (hadTrouble) { 594 Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show(); 595 } 596 } 597 598 private static State getDisplayState(Fragment fragment) { 599 return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); 600 } 601 602 private static abstract class Footer { 603 private final int mItemViewType; 604 605 public Footer(int itemViewType) { 606 mItemViewType = itemViewType; 607 } 608 609 public abstract View getView(View convertView, ViewGroup parent); 610 611 public int getItemViewType() { 612 return mItemViewType; 613 } 614 } 615 616 private class LoadingFooter extends Footer { 617 public LoadingFooter() { 618 super(1); 619 } 620 621 @Override 622 public View getView(View convertView, ViewGroup parent) { 623 final Context context = parent.getContext(); 624 final State state = getDisplayState(DirectoryFragment.this); 625 626 if (convertView == null) { 627 final LayoutInflater inflater = LayoutInflater.from(context); 628 if (state.derivedMode == MODE_LIST) { 629 convertView = inflater.inflate(R.layout.item_loading_list, parent, false); 630 } else if (state.derivedMode == MODE_GRID) { 631 convertView = inflater.inflate(R.layout.item_loading_grid, parent, false); 632 } else { 633 throw new IllegalStateException(); 634 } 635 } 636 637 return convertView; 638 } 639 } 640 641 private class MessageFooter extends Footer { 642 private final int mIcon; 643 private final String mMessage; 644 645 public MessageFooter(int itemViewType, int icon, String message) { 646 super(itemViewType); 647 mIcon = icon; 648 mMessage = message; 649 } 650 651 @Override 652 public View getView(View convertView, ViewGroup parent) { 653 final Context context = parent.getContext(); 654 final State state = getDisplayState(DirectoryFragment.this); 655 656 if (convertView == null) { 657 final LayoutInflater inflater = LayoutInflater.from(context); 658 if (state.derivedMode == MODE_LIST) { 659 convertView = inflater.inflate(R.layout.item_message_list, parent, false); 660 } else if (state.derivedMode == MODE_GRID) { 661 convertView = inflater.inflate(R.layout.item_message_grid, parent, false); 662 } else { 663 throw new IllegalStateException(); 664 } 665 } 666 667 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 668 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 669 icon.setImageResource(mIcon); 670 title.setText(mMessage); 671 return convertView; 672 } 673 } 674 675 private class DocumentsAdapter extends BaseAdapter { 676 private Cursor mCursor; 677 private int mCursorCount; 678 679 private List<Footer> mFooters = Lists.newArrayList(); 680 681 public void swapResult(DirectoryResult result) { 682 mCursor = result != null ? result.cursor : null; 683 mCursorCount = mCursor != null ? mCursor.getCount() : 0; 684 685 mFooters.clear(); 686 687 final Bundle extras = mCursor != null ? mCursor.getExtras() : null; 688 if (extras != null) { 689 final String info = extras.getString(DocumentsContract.EXTRA_INFO); 690 if (info != null) { 691 mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info)); 692 } 693 final String error = extras.getString(DocumentsContract.EXTRA_ERROR); 694 if (error != null) { 695 mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error)); 696 } 697 if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) { 698 mFooters.add(new LoadingFooter()); 699 } 700 } 701 702 if (result != null && result.exception != null) { 703 mFooters.add(new MessageFooter( 704 3, R.drawable.ic_dialog_alert, getString(R.string.query_error))); 705 } 706 707 if (isEmpty()) { 708 mEmptyView.setVisibility(View.VISIBLE); 709 } else { 710 mEmptyView.setVisibility(View.GONE); 711 } 712 713 notifyDataSetChanged(); 714 } 715 716 @Override 717 public View getView(int position, View convertView, ViewGroup parent) { 718 if (position < mCursorCount) { 719 return getDocumentView(position, convertView, parent); 720 } else { 721 position -= mCursorCount; 722 convertView = mFooters.get(position).getView(convertView, parent); 723 // Only the view itself is disabled; contents inside shouldn't 724 // be dimmed. 725 convertView.setEnabled(false); 726 return convertView; 727 } 728 } 729 730 private View getDocumentView(int position, View convertView, ViewGroup parent) { 731 final Context context = parent.getContext(); 732 final State state = getDisplayState(DirectoryFragment.this); 733 734 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); 735 736 final RootsCache roots = DocumentsApplication.getRootsCache(context); 737 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 738 context, mThumbSize); 739 740 if (convertView == null) { 741 final LayoutInflater inflater = LayoutInflater.from(context); 742 if (state.derivedMode == MODE_LIST) { 743 convertView = inflater.inflate(R.layout.item_doc_list, parent, false); 744 } else if (state.derivedMode == MODE_GRID) { 745 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); 746 747 // Apply padding to grid items 748 final FrameLayout grid = (FrameLayout) convertView; 749 final int gridPadding = getResources() 750 .getDimensionPixelSize(R.dimen.grid_padding); 751 752 // Tricksy hobbitses! We need to fully clear the drawable so 753 // the view doesn't clobber the new InsetDrawable callback 754 // when setting back later. 755 final Drawable fg = grid.getForeground(); 756 final Drawable bg = grid.getBackground(); 757 grid.setForeground(null); 758 grid.setBackground(null); 759 grid.setForeground(new InsetDrawable(fg, gridPadding)); 760 grid.setBackground(new InsetDrawable(bg, gridPadding)); 761 } else { 762 throw new IllegalStateException(); 763 } 764 } 765 766 final Cursor cursor = getItem(position); 767 768 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); 769 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); 770 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); 771 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 772 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 773 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); 774 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); 775 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 776 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); 777 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); 778 779 final View line1 = convertView.findViewById(R.id.line1); 780 final View line2 = convertView.findViewById(R.id.line2); 781 782 final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime); 783 final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb); 784 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 785 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); 786 final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2); 787 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 788 final TextView date = (TextView) convertView.findViewById(R.id.date); 789 final TextView size = (TextView) convertView.findViewById(R.id.size); 790 791 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); 792 if (oldTask != null) { 793 oldTask.reallyCancel(); 794 iconThumb.setTag(null); 795 } 796 797 iconMime.animate().cancel(); 798 iconThumb.animate().cancel(); 799 800 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0; 801 final boolean allowThumbnail = (state.derivedMode == MODE_GRID) 802 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType); 803 final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents; 804 805 boolean cacheHit = false; 806 if (showThumbnail) { 807 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); 808 final Bitmap cachedResult = thumbs.get(uri); 809 if (cachedResult != null) { 810 iconThumb.setImageBitmap(cachedResult); 811 cacheHit = true; 812 } else { 813 iconThumb.setImageDrawable(null); 814 final ThumbnailAsyncTask task = new ThumbnailAsyncTask( 815 uri, iconMime, iconThumb, mThumbSize); 816 iconThumb.setTag(task); 817 task.executeOnExecutor(ProviderExecutor.forAuthority(docAuthority)); 818 } 819 } 820 821 // Always throw MIME icon into place, even when a thumbnail is being 822 // loaded in background. 823 if (cacheHit) { 824 iconMime.setAlpha(0f); 825 iconMime.setImageDrawable(null); 826 iconThumb.setAlpha(1f); 827 } else { 828 iconMime.setAlpha(1f); 829 iconThumb.setAlpha(0f); 830 iconThumb.setImageDrawable(null); 831 if (docIcon != 0) { 832 iconMime.setImageDrawable( 833 IconUtils.loadPackageIcon(context, docAuthority, docIcon)); 834 } else { 835 iconMime.setImageDrawable(IconUtils.loadMimeIcon( 836 context, docMimeType, docAuthority, docId, state.derivedMode)); 837 } 838 } 839 840 boolean hasLine1 = false; 841 boolean hasLine2 = false; 842 843 final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles; 844 if (!hideTitle) { 845 title.setText(docDisplayName); 846 hasLine1 = true; 847 } 848 849 Drawable iconDrawable = null; 850 if (mType == TYPE_RECENT_OPEN) { 851 // We've already had to enumerate roots before any results can 852 // be shown, so this will never block. 853 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId); 854 iconDrawable = root.loadIcon(context); 855 856 if (summary != null) { 857 final boolean alwaysShowSummary = getResources() 858 .getBoolean(R.bool.always_show_summary); 859 if (alwaysShowSummary) { 860 summary.setText(root.getDirectoryString()); 861 summary.setVisibility(View.VISIBLE); 862 hasLine2 = true; 863 } else { 864 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) { 865 // No summary needed if icon speaks for itself 866 summary.setVisibility(View.INVISIBLE); 867 } else { 868 summary.setText(root.getDirectoryString()); 869 summary.setVisibility(View.VISIBLE); 870 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END); 871 hasLine2 = true; 872 } 873 } 874 } 875 } else { 876 // Directories showing thumbnails in grid mode get a little icon 877 // hint to remind user they're a directory. 878 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID 879 && showThumbnail) { 880 iconDrawable = context.getResources().getDrawable(R.drawable.ic_root_folder); 881 } 882 883 if (summary != null) { 884 if (docSummary != null) { 885 summary.setText(docSummary); 886 summary.setVisibility(View.VISIBLE); 887 hasLine2 = true; 888 } else { 889 summary.setVisibility(View.INVISIBLE); 890 } 891 } 892 } 893 894 if (icon1 != null) icon1.setVisibility(View.GONE); 895 if (icon2 != null) icon2.setVisibility(View.GONE); 896 897 if (iconDrawable != null) { 898 if (hasLine1) { 899 icon1.setVisibility(View.VISIBLE); 900 icon1.setImageDrawable(iconDrawable); 901 } else { 902 icon2.setVisibility(View.VISIBLE); 903 icon2.setImageDrawable(iconDrawable); 904 } 905 } 906 907 if (docLastModified == -1) { 908 date.setText(null); 909 } else { 910 date.setText(formatTime(context, docLastModified)); 911 hasLine2 = true; 912 } 913 914 if (state.showSize) { 915 size.setVisibility(View.VISIBLE); 916 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { 917 size.setText(null); 918 } else { 919 size.setText(Formatter.formatFileSize(context, docSize)); 920 hasLine2 = true; 921 } 922 } else { 923 size.setVisibility(View.GONE); 924 } 925 926 if (line1 != null) { 927 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE); 928 } 929 if (line2 != null) { 930 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE); 931 } 932 933 final boolean enabled = isDocumentEnabled(docMimeType, docFlags); 934 if (enabled) { 935 setEnabledRecursive(convertView, true); 936 iconMime.setAlpha(1f); 937 iconThumb.setAlpha(1f); 938 if (icon1 != null) icon1.setAlpha(1f); 939 if (icon2 != null) icon2.setAlpha(1f); 940 } else { 941 setEnabledRecursive(convertView, false); 942 iconMime.setAlpha(0.5f); 943 iconThumb.setAlpha(0.5f); 944 if (icon1 != null) icon1.setAlpha(0.5f); 945 if (icon2 != null) icon2.setAlpha(0.5f); 946 } 947 948 return convertView; 949 } 950 951 @Override 952 public int getCount() { 953 return mCursorCount + mFooters.size(); 954 } 955 956 @Override 957 public Cursor getItem(int position) { 958 if (position < mCursorCount) { 959 mCursor.moveToPosition(position); 960 return mCursor; 961 } else { 962 return null; 963 } 964 } 965 966 @Override 967 public long getItemId(int position) { 968 return position; 969 } 970 971 @Override 972 public int getViewTypeCount() { 973 return 4; 974 } 975 976 @Override 977 public int getItemViewType(int position) { 978 if (position < mCursorCount) { 979 return 0; 980 } else { 981 position -= mCursorCount; 982 return mFooters.get(position).getItemViewType(); 983 } 984 } 985 } 986 987 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> { 988 private final Uri mUri; 989 private final ImageView mIconMime; 990 private final ImageView mIconThumb; 991 private final Point mThumbSize; 992 private final CancellationSignal mSignal; 993 994 public ThumbnailAsyncTask( 995 Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize) { 996 mUri = uri; 997 mIconMime = iconMime; 998 mIconThumb = iconThumb; 999 mThumbSize = thumbSize; 1000 mSignal = new CancellationSignal(); 1001 } 1002 1003 public void reallyCancel() { 1004 cancel(false); 1005 mSignal.cancel(); 1006 } 1007 1008 @Override 1009 protected Bitmap doInBackground(Uri... params) { 1010 if (isCancelled()) return null; 1011 1012 final Context context = mIconThumb.getContext(); 1013 final ContentResolver resolver = context.getContentResolver(); 1014 1015 ContentProviderClient client = null; 1016 Bitmap result = null; 1017 try { 1018 client = DocumentsApplication.acquireUnstableProviderOrThrow( 1019 resolver, mUri.getAuthority()); 1020 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal); 1021 if (result != null) { 1022 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 1023 context, mThumbSize); 1024 thumbs.put(mUri, result); 1025 } 1026 } catch (Exception e) { 1027 Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e); 1028 } finally { 1029 ContentProviderClient.releaseQuietly(client); 1030 } 1031 return result; 1032 } 1033 1034 @Override 1035 protected void onPostExecute(Bitmap result) { 1036 if (mIconThumb.getTag() == this && result != null) { 1037 mIconThumb.setTag(null); 1038 mIconThumb.setImageBitmap(result); 1039 1040 final float targetAlpha = mIconMime.isEnabled() ? 1f : 0.5f; 1041 mIconMime.setAlpha(targetAlpha); 1042 mIconMime.animate().alpha(0f).start(); 1043 mIconThumb.setAlpha(0f); 1044 mIconThumb.animate().alpha(targetAlpha).start(); 1045 } 1046 } 1047 } 1048 1049 private static String formatTime(Context context, long when) { 1050 // TODO: DateUtils should make this easier 1051 Time then = new Time(); 1052 then.set(when); 1053 Time now = new Time(); 1054 now.setToNow(); 1055 1056 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT 1057 | DateUtils.FORMAT_ABBREV_ALL; 1058 1059 if (then.year != now.year) { 1060 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 1061 } else if (then.yearDay != now.yearDay) { 1062 flags |= DateUtils.FORMAT_SHOW_DATE; 1063 } else { 1064 flags |= DateUtils.FORMAT_SHOW_TIME; 1065 } 1066 1067 return DateUtils.formatDateTime(context, when, flags); 1068 } 1069 1070 private String findCommonMimeType(List<String> mimeTypes) { 1071 String[] commonType = mimeTypes.get(0).split("/"); 1072 if (commonType.length != 2) { 1073 return "*/*"; 1074 } 1075 1076 for (int i = 1; i < mimeTypes.size(); i++) { 1077 String[] type = mimeTypes.get(i).split("/"); 1078 if (type.length != 2) continue; 1079 1080 if (!commonType[1].equals(type[1])) { 1081 commonType[1] = "*"; 1082 } 1083 1084 if (!commonType[0].equals(type[0])) { 1085 commonType[0] = "*"; 1086 commonType[1] = "*"; 1087 break; 1088 } 1089 } 1090 1091 return commonType[0] + "/" + commonType[1]; 1092 } 1093 1094 private void setEnabledRecursive(View v, boolean enabled) { 1095 if (v == null) return; 1096 if (v.isEnabled() == enabled) return; 1097 v.setEnabled(enabled); 1098 1099 if (v instanceof ViewGroup) { 1100 final ViewGroup vg = (ViewGroup) v; 1101 for (int i = vg.getChildCount() - 1; i >= 0; i--) { 1102 setEnabledRecursive(vg.getChildAt(i), enabled); 1103 } 1104 } 1105 } 1106 1107 private boolean isDocumentEnabled(String docMimeType, int docFlags) { 1108 final State state = getDisplayState(DirectoryFragment.this); 1109 1110 // Directories are always enabled 1111 if (Document.MIME_TYPE_DIR.equals(docMimeType)) { 1112 return true; 1113 } 1114 1115 // Read-only files are disabled when creating 1116 if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) { 1117 return false; 1118 } 1119 1120 return MimePredicate.mimeMatches(state.acceptMimes, docMimeType); 1121 } 1122} 1123