DirectoryFragment.java revision 22d3990273a1cb5e61360ac42d1d4e6156d762ed
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.OperationCanceledException; 51import android.os.Parcelable; 52import android.provider.DocumentsContract; 53import android.provider.DocumentsContract.Document; 54import android.text.format.DateUtils; 55import android.text.format.Formatter; 56import android.text.format.Time; 57import android.util.Log; 58import android.util.SparseArray; 59import android.util.SparseBooleanArray; 60import android.view.ActionMode; 61import android.view.LayoutInflater; 62import android.view.Menu; 63import android.view.MenuItem; 64import android.view.View; 65import android.view.ViewGroup; 66import android.widget.AbsListView; 67import android.widget.AbsListView.MultiChoiceModeListener; 68import android.widget.AbsListView.RecyclerListener; 69import android.widget.AdapterView; 70import android.widget.AdapterView.OnItemClickListener; 71import android.widget.BaseAdapter; 72import android.widget.FrameLayout; 73import android.widget.GridView; 74import android.widget.ImageView; 75import android.widget.ListView; 76import android.widget.TextView; 77import android.widget.Toast; 78 79import com.android.documentsui.DocumentsActivity.State; 80import com.android.documentsui.ProviderExecutor.Preemptable; 81import com.android.documentsui.RecentsProvider.StateColumns; 82import com.android.documentsui.model.DocumentInfo; 83import com.android.documentsui.model.RootInfo; 84import com.google.android.collect.Lists; 85 86import java.util.ArrayList; 87import java.util.List; 88 89/** 90 * Display the documents inside a single directory. 91 */ 92public 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 mode.setTitle(getResources() 440 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 441 return true; 442 } 443 444 @Override 445 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 446 final State state = getDisplayState(DirectoryFragment.this); 447 448 final MenuItem open = menu.findItem(R.id.menu_open); 449 final MenuItem share = menu.findItem(R.id.menu_share); 450 final MenuItem delete = menu.findItem(R.id.menu_delete); 451 452 final boolean manageMode = state.action == ACTION_MANAGE; 453 open.setVisible(!manageMode); 454 share.setVisible(manageMode); 455 delete.setVisible(manageMode); 456 457 return true; 458 } 459 460 @Override 461 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 462 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); 463 final ArrayList<DocumentInfo> docs = Lists.newArrayList(); 464 final int size = checked.size(); 465 for (int i = 0; i < size; i++) { 466 if (checked.valueAt(i)) { 467 final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); 468 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 469 docs.add(doc); 470 } 471 } 472 473 final int id = item.getItemId(); 474 if (id == R.id.menu_open) { 475 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); 476 mode.finish(); 477 return true; 478 479 } else if (id == R.id.menu_share) { 480 onShareDocuments(docs); 481 mode.finish(); 482 return true; 483 484 } else if (id == R.id.menu_delete) { 485 onDeleteDocuments(docs); 486 mode.finish(); 487 return true; 488 489 } else { 490 return false; 491 } 492 } 493 494 @Override 495 public void onDestroyActionMode(ActionMode mode) { 496 // ignored 497 } 498 499 @Override 500 public void onItemCheckedStateChanged( 501 ActionMode mode, int position, long id, boolean checked) { 502 if (checked) { 503 // Directories and footer items cannot be checked 504 boolean valid = false; 505 506 final Cursor cursor = mAdapter.getItem(position); 507 if (cursor != null) { 508 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 509 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 510 if (!Document.MIME_TYPE_DIR.equals(docMimeType)) { 511 valid = isDocumentEnabled(docMimeType, docFlags); 512 } 513 } 514 515 if (!valid) { 516 mCurrentView.setItemChecked(position, false); 517 } 518 } 519 520 mode.setTitle(getResources() 521 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 522 } 523 }; 524 525 private RecyclerListener mRecycleListener = new RecyclerListener() { 526 @Override 527 public void onMovedToScrapHeap(View view) { 528 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb); 529 if (iconThumb != null) { 530 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); 531 if (oldTask != null) { 532 oldTask.preempt(); 533 iconThumb.setTag(null); 534 } 535 } 536 } 537 }; 538 539 private void onShareDocuments(List<DocumentInfo> docs) { 540 Intent intent; 541 if (docs.size() == 1) { 542 final DocumentInfo doc = docs.get(0); 543 544 intent = new Intent(Intent.ACTION_SEND); 545 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 546 intent.addCategory(Intent.CATEGORY_DEFAULT); 547 intent.setType(doc.mimeType); 548 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri); 549 550 } else if (docs.size() > 1) { 551 intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 552 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 553 intent.addCategory(Intent.CATEGORY_DEFAULT); 554 555 final ArrayList<String> mimeTypes = Lists.newArrayList(); 556 final ArrayList<Uri> uris = Lists.newArrayList(); 557 for (DocumentInfo doc : docs) { 558 mimeTypes.add(doc.mimeType); 559 uris.add(doc.derivedUri); 560 } 561 562 intent.setType(findCommonMimeType(mimeTypes)); 563 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 564 565 } else { 566 return; 567 } 568 569 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); 570 startActivity(intent); 571 } 572 573 private void onDeleteDocuments(List<DocumentInfo> docs) { 574 final Context context = getActivity(); 575 final ContentResolver resolver = context.getContentResolver(); 576 577 boolean hadTrouble = false; 578 for (DocumentInfo doc : docs) { 579 if (!doc.isDeleteSupported()) { 580 Log.w(TAG, "Skipping " + doc); 581 hadTrouble = true; 582 continue; 583 } 584 585 ContentProviderClient client = null; 586 try { 587 client = DocumentsApplication.acquireUnstableProviderOrThrow( 588 resolver, doc.derivedUri.getAuthority()); 589 DocumentsContract.deleteDocument(client, doc.derivedUri); 590 } catch (Exception e) { 591 Log.w(TAG, "Failed to delete " + doc); 592 hadTrouble = true; 593 } finally { 594 ContentProviderClient.releaseQuietly(client); 595 } 596 } 597 598 if (hadTrouble) { 599 Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show(); 600 } 601 } 602 603 private static State getDisplayState(Fragment fragment) { 604 return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); 605 } 606 607 private static abstract class Footer { 608 private final int mItemViewType; 609 610 public Footer(int itemViewType) { 611 mItemViewType = itemViewType; 612 } 613 614 public abstract View getView(View convertView, ViewGroup parent); 615 616 public int getItemViewType() { 617 return mItemViewType; 618 } 619 } 620 621 private class LoadingFooter extends Footer { 622 public LoadingFooter() { 623 super(1); 624 } 625 626 @Override 627 public View getView(View convertView, ViewGroup parent) { 628 final Context context = parent.getContext(); 629 final State state = getDisplayState(DirectoryFragment.this); 630 631 if (convertView == null) { 632 final LayoutInflater inflater = LayoutInflater.from(context); 633 if (state.derivedMode == MODE_LIST) { 634 convertView = inflater.inflate(R.layout.item_loading_list, parent, false); 635 } else if (state.derivedMode == MODE_GRID) { 636 convertView = inflater.inflate(R.layout.item_loading_grid, parent, false); 637 } else { 638 throw new IllegalStateException(); 639 } 640 } 641 642 return convertView; 643 } 644 } 645 646 private class MessageFooter extends Footer { 647 private final int mIcon; 648 private final String mMessage; 649 650 public MessageFooter(int itemViewType, int icon, String message) { 651 super(itemViewType); 652 mIcon = icon; 653 mMessage = message; 654 } 655 656 @Override 657 public View getView(View convertView, ViewGroup parent) { 658 final Context context = parent.getContext(); 659 final State state = getDisplayState(DirectoryFragment.this); 660 661 if (convertView == null) { 662 final LayoutInflater inflater = LayoutInflater.from(context); 663 if (state.derivedMode == MODE_LIST) { 664 convertView = inflater.inflate(R.layout.item_message_list, parent, false); 665 } else if (state.derivedMode == MODE_GRID) { 666 convertView = inflater.inflate(R.layout.item_message_grid, parent, false); 667 } else { 668 throw new IllegalStateException(); 669 } 670 } 671 672 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 673 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 674 icon.setImageResource(mIcon); 675 title.setText(mMessage); 676 return convertView; 677 } 678 } 679 680 private class DocumentsAdapter extends BaseAdapter { 681 private Cursor mCursor; 682 private int mCursorCount; 683 684 private List<Footer> mFooters = Lists.newArrayList(); 685 686 public void swapResult(DirectoryResult result) { 687 mCursor = result != null ? result.cursor : null; 688 mCursorCount = mCursor != null ? mCursor.getCount() : 0; 689 690 mFooters.clear(); 691 692 final Bundle extras = mCursor != null ? mCursor.getExtras() : null; 693 if (extras != null) { 694 final String info = extras.getString(DocumentsContract.EXTRA_INFO); 695 if (info != null) { 696 mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info)); 697 } 698 final String error = extras.getString(DocumentsContract.EXTRA_ERROR); 699 if (error != null) { 700 mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error)); 701 } 702 if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) { 703 mFooters.add(new LoadingFooter()); 704 } 705 } 706 707 if (result != null && result.exception != null) { 708 mFooters.add(new MessageFooter( 709 3, R.drawable.ic_dialog_alert, getString(R.string.query_error))); 710 } 711 712 if (isEmpty()) { 713 mEmptyView.setVisibility(View.VISIBLE); 714 } else { 715 mEmptyView.setVisibility(View.GONE); 716 } 717 718 notifyDataSetChanged(); 719 } 720 721 @Override 722 public View getView(int position, View convertView, ViewGroup parent) { 723 if (position < mCursorCount) { 724 return getDocumentView(position, convertView, parent); 725 } else { 726 position -= mCursorCount; 727 convertView = mFooters.get(position).getView(convertView, parent); 728 // Only the view itself is disabled; contents inside shouldn't 729 // be dimmed. 730 convertView.setEnabled(false); 731 return convertView; 732 } 733 } 734 735 private View getDocumentView(int position, View convertView, ViewGroup parent) { 736 final Context context = parent.getContext(); 737 final State state = getDisplayState(DirectoryFragment.this); 738 739 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); 740 741 final RootsCache roots = DocumentsApplication.getRootsCache(context); 742 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 743 context, mThumbSize); 744 745 if (convertView == null) { 746 final LayoutInflater inflater = LayoutInflater.from(context); 747 if (state.derivedMode == MODE_LIST) { 748 convertView = inflater.inflate(R.layout.item_doc_list, parent, false); 749 } else if (state.derivedMode == MODE_GRID) { 750 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); 751 752 // Apply padding to grid items 753 final FrameLayout grid = (FrameLayout) convertView; 754 final int gridPadding = getResources() 755 .getDimensionPixelSize(R.dimen.grid_padding); 756 757 // Tricksy hobbitses! We need to fully clear the drawable so 758 // the view doesn't clobber the new InsetDrawable callback 759 // when setting back later. 760 final Drawable fg = grid.getForeground(); 761 final Drawable bg = grid.getBackground(); 762 grid.setForeground(null); 763 grid.setBackground(null); 764 grid.setForeground(new InsetDrawable(fg, gridPadding)); 765 grid.setBackground(new InsetDrawable(bg, gridPadding)); 766 } else { 767 throw new IllegalStateException(); 768 } 769 } 770 771 final Cursor cursor = getItem(position); 772 773 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); 774 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); 775 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); 776 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 777 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 778 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); 779 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); 780 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 781 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); 782 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); 783 784 final View line1 = convertView.findViewById(R.id.line1); 785 final View line2 = convertView.findViewById(R.id.line2); 786 787 final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime); 788 final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb); 789 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 790 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); 791 final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2); 792 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 793 final TextView date = (TextView) convertView.findViewById(R.id.date); 794 final TextView size = (TextView) convertView.findViewById(R.id.size); 795 796 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); 797 if (oldTask != null) { 798 oldTask.preempt(); 799 iconThumb.setTag(null); 800 } 801 802 iconMime.animate().cancel(); 803 iconThumb.animate().cancel(); 804 805 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0; 806 final boolean allowThumbnail = (state.derivedMode == MODE_GRID) 807 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType); 808 final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents; 809 810 boolean cacheHit = false; 811 if (showThumbnail) { 812 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); 813 final Bitmap cachedResult = thumbs.get(uri); 814 if (cachedResult != null) { 815 iconThumb.setImageBitmap(cachedResult); 816 cacheHit = true; 817 } else { 818 iconThumb.setImageDrawable(null); 819 final ThumbnailAsyncTask task = new ThumbnailAsyncTask( 820 uri, iconMime, iconThumb, mThumbSize); 821 iconThumb.setTag(task); 822 ProviderExecutor.forAuthority(docAuthority).execute(task); 823 } 824 } 825 826 // Always throw MIME icon into place, even when a thumbnail is being 827 // loaded in background. 828 if (cacheHit) { 829 iconMime.setAlpha(0f); 830 iconMime.setImageDrawable(null); 831 iconThumb.setAlpha(1f); 832 } else { 833 iconMime.setAlpha(1f); 834 iconThumb.setAlpha(0f); 835 iconThumb.setImageDrawable(null); 836 if (docIcon != 0) { 837 iconMime.setImageDrawable( 838 IconUtils.loadPackageIcon(context, docAuthority, docIcon)); 839 } else { 840 iconMime.setImageDrawable(IconUtils.loadMimeIcon( 841 context, docMimeType, docAuthority, docId, state.derivedMode)); 842 } 843 } 844 845 boolean hasLine1 = false; 846 boolean hasLine2 = false; 847 848 final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles; 849 if (!hideTitle) { 850 title.setText(docDisplayName); 851 hasLine1 = true; 852 } 853 854 Drawable iconDrawable = null; 855 if (mType == TYPE_RECENT_OPEN) { 856 // We've already had to enumerate roots before any results can 857 // be shown, so this will never block. 858 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId); 859 iconDrawable = root.loadIcon(context); 860 861 if (summary != null) { 862 final boolean alwaysShowSummary = getResources() 863 .getBoolean(R.bool.always_show_summary); 864 if (alwaysShowSummary) { 865 summary.setText(root.getDirectoryString()); 866 summary.setVisibility(View.VISIBLE); 867 hasLine2 = true; 868 } else { 869 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) { 870 // No summary needed if icon speaks for itself 871 summary.setVisibility(View.INVISIBLE); 872 } else { 873 summary.setText(root.getDirectoryString()); 874 summary.setVisibility(View.VISIBLE); 875 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END); 876 hasLine2 = true; 877 } 878 } 879 } 880 } else { 881 // Directories showing thumbnails in grid mode get a little icon 882 // hint to remind user they're a directory. 883 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID 884 && showThumbnail) { 885 iconDrawable = context.getResources().getDrawable(R.drawable.ic_root_folder); 886 } 887 888 if (summary != null) { 889 if (docSummary != null) { 890 summary.setText(docSummary); 891 summary.setVisibility(View.VISIBLE); 892 hasLine2 = true; 893 } else { 894 summary.setVisibility(View.INVISIBLE); 895 } 896 } 897 } 898 899 if (icon1 != null) icon1.setVisibility(View.GONE); 900 if (icon2 != null) icon2.setVisibility(View.GONE); 901 902 if (iconDrawable != null) { 903 if (hasLine1) { 904 icon1.setVisibility(View.VISIBLE); 905 icon1.setImageDrawable(iconDrawable); 906 } else { 907 icon2.setVisibility(View.VISIBLE); 908 icon2.setImageDrawable(iconDrawable); 909 } 910 } 911 912 if (docLastModified == -1) { 913 date.setText(null); 914 } else { 915 date.setText(formatTime(context, docLastModified)); 916 hasLine2 = true; 917 } 918 919 if (state.showSize) { 920 size.setVisibility(View.VISIBLE); 921 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { 922 size.setText(null); 923 } else { 924 size.setText(Formatter.formatFileSize(context, docSize)); 925 hasLine2 = true; 926 } 927 } else { 928 size.setVisibility(View.GONE); 929 } 930 931 if (line1 != null) { 932 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE); 933 } 934 if (line2 != null) { 935 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE); 936 } 937 938 final boolean enabled = isDocumentEnabled(docMimeType, docFlags); 939 if (enabled) { 940 setEnabledRecursive(convertView, true); 941 iconMime.setAlpha(1f); 942 iconThumb.setAlpha(1f); 943 if (icon1 != null) icon1.setAlpha(1f); 944 if (icon2 != null) icon2.setAlpha(1f); 945 } else { 946 setEnabledRecursive(convertView, false); 947 iconMime.setAlpha(0.5f); 948 iconThumb.setAlpha(0.5f); 949 if (icon1 != null) icon1.setAlpha(0.5f); 950 if (icon2 != null) icon2.setAlpha(0.5f); 951 } 952 953 return convertView; 954 } 955 956 @Override 957 public int getCount() { 958 return mCursorCount + mFooters.size(); 959 } 960 961 @Override 962 public Cursor getItem(int position) { 963 if (position < mCursorCount) { 964 mCursor.moveToPosition(position); 965 return mCursor; 966 } else { 967 return null; 968 } 969 } 970 971 @Override 972 public long getItemId(int position) { 973 return position; 974 } 975 976 @Override 977 public int getViewTypeCount() { 978 return 4; 979 } 980 981 @Override 982 public int getItemViewType(int position) { 983 if (position < mCursorCount) { 984 return 0; 985 } else { 986 position -= mCursorCount; 987 return mFooters.get(position).getItemViewType(); 988 } 989 } 990 } 991 992 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> 993 implements Preemptable { 994 private final Uri mUri; 995 private final ImageView mIconMime; 996 private final ImageView mIconThumb; 997 private final Point mThumbSize; 998 private final CancellationSignal mSignal; 999 1000 public ThumbnailAsyncTask( 1001 Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize) { 1002 mUri = uri; 1003 mIconMime = iconMime; 1004 mIconThumb = iconThumb; 1005 mThumbSize = thumbSize; 1006 mSignal = new CancellationSignal(); 1007 } 1008 1009 @Override 1010 public void preempt() { 1011 cancel(false); 1012 mSignal.cancel(); 1013 } 1014 1015 @Override 1016 protected Bitmap doInBackground(Uri... params) { 1017 if (isCancelled()) return null; 1018 1019 final Context context = mIconThumb.getContext(); 1020 final ContentResolver resolver = context.getContentResolver(); 1021 1022 ContentProviderClient client = null; 1023 Bitmap result = null; 1024 try { 1025 client = DocumentsApplication.acquireUnstableProviderOrThrow( 1026 resolver, mUri.getAuthority()); 1027 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal); 1028 if (result != null) { 1029 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 1030 context, mThumbSize); 1031 thumbs.put(mUri, result); 1032 } 1033 } catch (Exception e) { 1034 if (!(e instanceof OperationCanceledException)) { 1035 Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e); 1036 } 1037 } finally { 1038 ContentProviderClient.releaseQuietly(client); 1039 } 1040 return result; 1041 } 1042 1043 @Override 1044 protected void onPostExecute(Bitmap result) { 1045 if (mIconThumb.getTag() == this && result != null) { 1046 mIconThumb.setTag(null); 1047 mIconThumb.setImageBitmap(result); 1048 1049 final float targetAlpha = mIconMime.isEnabled() ? 1f : 0.5f; 1050 mIconMime.setAlpha(targetAlpha); 1051 mIconMime.animate().alpha(0f).start(); 1052 mIconThumb.setAlpha(0f); 1053 mIconThumb.animate().alpha(targetAlpha).start(); 1054 } 1055 } 1056 } 1057 1058 private static String formatTime(Context context, long when) { 1059 // TODO: DateUtils should make this easier 1060 Time then = new Time(); 1061 then.set(when); 1062 Time now = new Time(); 1063 now.setToNow(); 1064 1065 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT 1066 | DateUtils.FORMAT_ABBREV_ALL; 1067 1068 if (then.year != now.year) { 1069 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 1070 } else if (then.yearDay != now.yearDay) { 1071 flags |= DateUtils.FORMAT_SHOW_DATE; 1072 } else { 1073 flags |= DateUtils.FORMAT_SHOW_TIME; 1074 } 1075 1076 return DateUtils.formatDateTime(context, when, flags); 1077 } 1078 1079 private String findCommonMimeType(List<String> mimeTypes) { 1080 String[] commonType = mimeTypes.get(0).split("/"); 1081 if (commonType.length != 2) { 1082 return "*/*"; 1083 } 1084 1085 for (int i = 1; i < mimeTypes.size(); i++) { 1086 String[] type = mimeTypes.get(i).split("/"); 1087 if (type.length != 2) continue; 1088 1089 if (!commonType[1].equals(type[1])) { 1090 commonType[1] = "*"; 1091 } 1092 1093 if (!commonType[0].equals(type[0])) { 1094 commonType[0] = "*"; 1095 commonType[1] = "*"; 1096 break; 1097 } 1098 } 1099 1100 return commonType[0] + "/" + commonType[1]; 1101 } 1102 1103 private void setEnabledRecursive(View v, boolean enabled) { 1104 if (v == null) return; 1105 if (v.isEnabled() == enabled) return; 1106 v.setEnabled(enabled); 1107 1108 if (v instanceof ViewGroup) { 1109 final ViewGroup vg = (ViewGroup) v; 1110 for (int i = vg.getChildCount() - 1; i >= 0; i--) { 1111 setEnabledRecursive(vg.getChildAt(i), enabled); 1112 } 1113 } 1114 } 1115 1116 private boolean isDocumentEnabled(String docMimeType, int docFlags) { 1117 final State state = getDisplayState(DirectoryFragment.this); 1118 1119 // Directories are always enabled 1120 if (Document.MIME_TYPE_DIR.equals(docMimeType)) { 1121 return true; 1122 } 1123 1124 // Read-only files are disabled when creating 1125 if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) { 1126 return false; 1127 } 1128 1129 return MimePredicate.mimeMatches(state.acceptMimes, docMimeType); 1130 } 1131} 1132