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