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