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