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