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