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