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