DirectoryFragment.java revision 348ad6866b91afa4d59d45df533ef88094c74d13
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_MANAGE; 21import static com.android.documentsui.DocumentsActivity.State.MODE_GRID; 22import static com.android.documentsui.DocumentsActivity.State.MODE_LIST; 23import static com.android.documentsui.model.DocumentInfo.getCursorInt; 24import static com.android.documentsui.model.DocumentInfo.getCursorLong; 25import static com.android.documentsui.model.DocumentInfo.getCursorString; 26 27import android.app.Fragment; 28import android.app.FragmentManager; 29import android.app.FragmentTransaction; 30import android.app.LoaderManager.LoaderCallbacks; 31import android.content.ContentResolver; 32import android.content.Context; 33import android.content.Intent; 34import android.content.Loader; 35import android.database.Cursor; 36import android.graphics.Bitmap; 37import android.graphics.Point; 38import android.net.Uri; 39import android.os.AsyncTask; 40import android.os.Bundle; 41import android.provider.DocumentsContract; 42import android.provider.DocumentsContract.Document; 43import android.text.format.DateUtils; 44import android.text.format.Formatter; 45import android.text.format.Time; 46import android.util.Log; 47import android.util.SparseBooleanArray; 48import android.view.ActionMode; 49import android.view.LayoutInflater; 50import android.view.Menu; 51import android.view.MenuItem; 52import android.view.View; 53import android.view.ViewGroup; 54import android.widget.AbsListView; 55import android.widget.AbsListView.MultiChoiceModeListener; 56import android.widget.AdapterView; 57import android.widget.AdapterView.OnItemClickListener; 58import android.widget.BaseAdapter; 59import android.widget.GridView; 60import android.widget.ImageView; 61import android.widget.ListView; 62import android.widget.TextView; 63import android.widget.Toast; 64 65import com.android.documentsui.DocumentsActivity.State; 66import com.android.documentsui.model.DocumentInfo; 67import com.android.documentsui.model.RootInfo; 68import com.android.internal.util.Predicate; 69import com.google.android.collect.Lists; 70 71import java.util.ArrayList; 72import java.util.List; 73import java.util.concurrent.atomic.AtomicInteger; 74 75/** 76 * Display the documents inside a single directory. 77 */ 78public class DirectoryFragment extends Fragment { 79 80 private View mEmptyView; 81 private ListView mListView; 82 private GridView mGridView; 83 84 private AbsListView mCurrentView; 85 86 private Predicate<DocumentInfo> mFilter; 87 88 public static final int TYPE_NORMAL = 1; 89 public static final int TYPE_SEARCH = 2; 90 public static final int TYPE_RECENT_OPEN = 3; 91 92 private int mType = TYPE_NORMAL; 93 94 private Point mThumbSize; 95 96 private DocumentsAdapter mAdapter; 97 private LoaderCallbacks<DirectoryResult> mCallbacks; 98 99 private static final String EXTRA_TYPE = "type"; 100 private static final String EXTRA_AUTHORITY = "authority"; 101 private static final String EXTRA_ROOT_ID = "rootId"; 102 private static final String EXTRA_DOC_ID = "docId"; 103 private static final String EXTRA_QUERY = "query"; 104 105 private static AtomicInteger sLoaderId = new AtomicInteger(4000); 106 107 private int mLastSortOrder = -1; 108 109 private final int mLoaderId = sLoaderId.incrementAndGet(); 110 111 public static void showNormal(FragmentManager fm, Uri uri) { 112 show(fm, TYPE_NORMAL, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), null); 113 } 114 115 public static void showSearch(FragmentManager fm, Uri uri, String query) { 116 show(fm, TYPE_SEARCH, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), 117 query); 118 } 119 120 public static void showRecentsOpen(FragmentManager fm) { 121 show(fm, TYPE_RECENT_OPEN, null, null, null, null); 122 } 123 124 private static void show(FragmentManager fm, int type, String authority, String rootId, 125 String docId, String query) { 126 final Bundle args = new Bundle(); 127 args.putInt(EXTRA_TYPE, type); 128 args.putString(EXTRA_AUTHORITY, authority); 129 args.putString(EXTRA_ROOT_ID, rootId); 130 args.putString(EXTRA_DOC_ID, docId); 131 args.putString(EXTRA_QUERY, query); 132 133 final DirectoryFragment fragment = new DirectoryFragment(); 134 fragment.setArguments(args); 135 136 final FragmentTransaction ft = fm.beginTransaction(); 137 ft.replace(R.id.container_directory, fragment); 138 ft.commitAllowingStateLoss(); 139 } 140 141 public static DirectoryFragment get(FragmentManager fm) { 142 // TODO: deal with multiple directories shown at once 143 return (DirectoryFragment) fm.findFragmentById(R.id.container_directory); 144 } 145 146 @Override 147 public View onCreateView( 148 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 149 final Context context = inflater.getContext(); 150 final View view = inflater.inflate(R.layout.fragment_directory, container, false); 151 152 mEmptyView = view.findViewById(android.R.id.empty); 153 154 mListView = (ListView) view.findViewById(R.id.list); 155 mListView.setOnItemClickListener(mItemListener); 156 mListView.setMultiChoiceModeListener(mMultiListener); 157 158 mGridView = (GridView) view.findViewById(R.id.grid); 159 mGridView.setOnItemClickListener(mItemListener); 160 mGridView.setMultiChoiceModeListener(mMultiListener); 161 162 return view; 163 } 164 165 @Override 166 public void onActivityCreated(Bundle savedInstanceState) { 167 super.onActivityCreated(savedInstanceState); 168 169 final Context context = getActivity(); 170 171 mAdapter = new DocumentsAdapter(); 172 mType = getArguments().getInt(EXTRA_TYPE); 173 174 mCallbacks = new LoaderCallbacks<DirectoryResult>() { 175 @Override 176 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { 177 final State state = getDisplayState(DirectoryFragment.this); 178 179 final String authority = getArguments().getString(EXTRA_AUTHORITY); 180 final String rootId = getArguments().getString(EXTRA_ROOT_ID); 181 final String docId = getArguments().getString(EXTRA_DOC_ID); 182 final String query = getArguments().getString(EXTRA_QUERY); 183 184 Uri contentsUri; 185 switch (mType) { 186 case TYPE_NORMAL: 187 contentsUri = DocumentsContract.buildChildDocumentsUri(authority, docId); 188 return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder); 189 case TYPE_SEARCH: 190 contentsUri = DocumentsContract.buildSearchDocumentsUri( 191 authority, docId, query); 192 return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder); 193 case TYPE_RECENT_OPEN: 194 final RootsCache roots = DocumentsApplication.getRootsCache(context); 195 final List<RootInfo> matchingRoots = roots.getMatchingRoots(state); 196 return new RecentLoader(context, matchingRoots); 197 default: 198 throw new IllegalStateException("Unknown type " + mType); 199 200 } 201 } 202 203 @Override 204 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { 205 mAdapter.swapCursor(result.cursor); 206 } 207 208 @Override 209 public void onLoaderReset(Loader<DirectoryResult> loader) { 210 mAdapter.swapCursor(null); 211 } 212 }; 213 214 updateDisplayState(); 215 } 216 217 public void updateDisplayState() { 218 final State state = getDisplayState(this); 219 220 if (mLastSortOrder != state.sortOrder) { 221 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); 222 mLastSortOrder = state.sortOrder; 223 } 224 225 mListView.smoothScrollToPosition(0); 226 mGridView.smoothScrollToPosition(0); 227 228 mListView.setVisibility(state.mode == MODE_LIST ? View.VISIBLE : View.GONE); 229 mGridView.setVisibility(state.mode == MODE_GRID ? View.VISIBLE : View.GONE); 230 231 mFilter = new MimePredicate(state.acceptMimes); 232 233 final int choiceMode; 234 if (state.allowMultiple) { 235 choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; 236 } else { 237 choiceMode = ListView.CHOICE_MODE_NONE; 238 } 239 240 final int thumbSize; 241 if (state.mode == MODE_GRID) { 242 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); 243 mListView.setAdapter(null); 244 mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); 245 mGridView.setAdapter(mAdapter); 246 mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width)); 247 mGridView.setNumColumns(GridView.AUTO_FIT); 248 mGridView.setChoiceMode(choiceMode); 249 mCurrentView = mGridView; 250 } else if (state.mode == MODE_LIST) { 251 thumbSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); 252 mGridView.setAdapter(null); 253 mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE); 254 mListView.setAdapter(mAdapter); 255 mListView.setChoiceMode(choiceMode); 256 mCurrentView = mListView; 257 } else { 258 throw new IllegalStateException(); 259 } 260 261 mThumbSize = new Point(thumbSize, thumbSize); 262 } 263 264 private OnItemClickListener mItemListener = new OnItemClickListener() { 265 @Override 266 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 267 final Cursor cursor = mAdapter.getItem(position); 268 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 269 if (mFilter.apply(doc)) { 270 ((DocumentsActivity) getActivity()).onDocumentPicked(doc); 271 } 272 } 273 }; 274 275 private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() { 276 @Override 277 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 278 mode.getMenuInflater().inflate(R.menu.mode_directory, menu); 279 return true; 280 } 281 282 @Override 283 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 284 final State state = getDisplayState(DirectoryFragment.this); 285 286 final MenuItem open = menu.findItem(R.id.menu_open); 287 final MenuItem share = menu.findItem(R.id.menu_share); 288 final MenuItem delete = menu.findItem(R.id.menu_delete); 289 290 final boolean manageMode = state.action == ACTION_MANAGE; 291 open.setVisible(!manageMode); 292 share.setVisible(manageMode); 293 delete.setVisible(manageMode); 294 295 return true; 296 } 297 298 @Override 299 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 300 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); 301 final ArrayList<DocumentInfo> docs = Lists.newArrayList(); 302 final int size = checked.size(); 303 for (int i = 0; i < size; i++) { 304 if (checked.valueAt(i)) { 305 final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); 306 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 307 docs.add(doc); 308 } 309 } 310 311 final int id = item.getItemId(); 312 if (id == R.id.menu_open) { 313 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); 314 mode.finish(); 315 return true; 316 317 } else if (id == R.id.menu_share) { 318 onShareDocuments(docs); 319 mode.finish(); 320 return true; 321 322 } else if (id == R.id.menu_delete) { 323 onDeleteDocuments(docs); 324 mode.finish(); 325 return true; 326 327 } else { 328 return false; 329 } 330 } 331 332 @Override 333 public void onDestroyActionMode(ActionMode mode) { 334 // ignored 335 } 336 337 @Override 338 public void onItemCheckedStateChanged( 339 ActionMode mode, int position, long id, boolean checked) { 340 if (checked) { 341 // Directories cannot be checked 342 final Cursor cursor = mAdapter.getItem(position); 343 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 344 if (Document.MIME_TYPE_DIR.equals(docMimeType)) { 345 mCurrentView.setItemChecked(position, false); 346 } 347 } 348 349 mode.setTitle(getResources() 350 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 351 } 352 }; 353 354 private void onShareDocuments(List<DocumentInfo> docs) { 355 Intent intent; 356 if (docs.size() == 1) { 357 final DocumentInfo doc = docs.get(0); 358 359 intent = new Intent(Intent.ACTION_SEND); 360 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 361 intent.addCategory(Intent.CATEGORY_DEFAULT); 362 intent.setType(doc.mimeType); 363 intent.putExtra(Intent.EXTRA_STREAM, doc.uri); 364 365 } else if (docs.size() > 1) { 366 intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 367 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 368 intent.addCategory(Intent.CATEGORY_DEFAULT); 369 370 final ArrayList<String> mimeTypes = Lists.newArrayList(); 371 final ArrayList<Uri> uris = Lists.newArrayList(); 372 for (DocumentInfo doc : docs) { 373 mimeTypes.add(doc.mimeType); 374 uris.add(doc.uri); 375 } 376 377 intent.setType(findCommonMimeType(mimeTypes)); 378 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 379 380 } else { 381 return; 382 } 383 384 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); 385 startActivity(intent); 386 } 387 388 private void onDeleteDocuments(List<DocumentInfo> docs) { 389 final Context context = getActivity(); 390 final ContentResolver resolver = context.getContentResolver(); 391 392 boolean hadTrouble = false; 393 for (DocumentInfo doc : docs) { 394 if (!doc.isDeleteSupported()) { 395 Log.w(TAG, "Skipping " + doc); 396 hadTrouble = true; 397 continue; 398 } 399 400 try { 401 if (resolver.delete(doc.uri, null, null) != 1) { 402 Log.w(TAG, "Failed to delete " + doc); 403 hadTrouble = true; 404 } 405 } catch (Exception e) { 406 Log.w(TAG, "Failed to delete " + doc + ": " + e); 407 hadTrouble = true; 408 } 409 } 410 411 if (hadTrouble) { 412 Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show(); 413 } 414 } 415 416 private static State getDisplayState(Fragment fragment) { 417 return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); 418 } 419 420 private class DocumentsAdapter extends BaseAdapter { 421 private Cursor mCursor; 422 423 public void swapCursor(Cursor cursor) { 424 mCursor = cursor; 425 426 if (isEmpty()) { 427 mEmptyView.setVisibility(View.VISIBLE); 428 } else { 429 mEmptyView.setVisibility(View.GONE); 430 } 431 432 notifyDataSetChanged(); 433 } 434 435 @Override 436 public View getView(int position, View convertView, ViewGroup parent) { 437 final Context context = parent.getContext(); 438 final State state = getDisplayState(DirectoryFragment.this); 439 440 final RootsCache roots = DocumentsApplication.getRootsCache(context); 441 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 442 context, mThumbSize); 443 444 if (convertView == null) { 445 final LayoutInflater inflater = LayoutInflater.from(context); 446 if (state.mode == MODE_LIST) { 447 convertView = inflater.inflate(R.layout.item_doc_list, parent, false); 448 } else if (state.mode == MODE_GRID) { 449 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); 450 } else { 451 throw new IllegalStateException(); 452 } 453 } 454 455 final Cursor cursor = getItem(position); 456 457 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); 458 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); 459 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); 460 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 461 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 462 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); 463 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); 464 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 465 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); 466 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); 467 468 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 469 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 470 final View summaryGrid = convertView.findViewById(R.id.summary_grid); 471 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); 472 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 473 final TextView date = (TextView) convertView.findViewById(R.id.date); 474 final TextView size = (TextView) convertView.findViewById(R.id.size); 475 476 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) icon.getTag(); 477 if (oldTask != null) { 478 oldTask.cancel(false); 479 } 480 481 if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) { 482 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); 483 final Bitmap cachedResult = thumbs.get(uri); 484 if (cachedResult != null) { 485 icon.setImageBitmap(cachedResult); 486 } else { 487 final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize); 488 icon.setImageBitmap(null); 489 icon.setTag(task); 490 task.execute(uri); 491 } 492 } else if (docIcon != 0) { 493 icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon)); 494 } else { 495 icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType)); 496 } 497 498 title.setText(docDisplayName); 499 500 if (mType == TYPE_RECENT_OPEN) { 501 final RootInfo root = roots.getRoot(docAuthority, docRootId); 502 icon1.setVisibility(View.VISIBLE); 503 icon1.setImageDrawable(root.loadIcon(context)); 504 summary.setText(root.getDirectoryString()); 505 summary.setVisibility(View.VISIBLE); 506 } else { 507 icon1.setVisibility(View.GONE); 508 if (docSummary != null) { 509 summary.setText(docSummary); 510 summary.setVisibility(View.VISIBLE); 511 } else { 512 summary.setVisibility(View.INVISIBLE); 513 } 514 } 515 516 if (summaryGrid != null) { 517 summaryGrid.setVisibility( 518 (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE); 519 } 520 521 if (docLastModified == -1) { 522 date.setText(null); 523 } else { 524 date.setText(formatTime(context, docLastModified)); 525 } 526 527 if (state.showSize) { 528 size.setVisibility(View.VISIBLE); 529 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { 530 size.setText(null); 531 } else { 532 size.setText(Formatter.formatFileSize(context, docSize)); 533 } 534 } else { 535 size.setVisibility(View.GONE); 536 } 537 538 return convertView; 539 } 540 541 @Override 542 public int getCount() { 543 return mCursor != null ? mCursor.getCount() : 0; 544 } 545 546 @Override 547 public Cursor getItem(int position) { 548 if (mCursor != null) { 549 mCursor.moveToPosition(position); 550 } 551 return mCursor; 552 } 553 554 @Override 555 public long getItemId(int position) { 556 return position; 557 } 558 } 559 560 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> { 561 private final ImageView mTarget; 562 private final Point mThumbSize; 563 564 public ThumbnailAsyncTask(ImageView target, Point thumbSize) { 565 mTarget = target; 566 mThumbSize = thumbSize; 567 } 568 569 @Override 570 protected void onPreExecute() { 571 mTarget.setTag(this); 572 } 573 574 @Override 575 protected Bitmap doInBackground(Uri... params) { 576 final Context context = mTarget.getContext(); 577 final Uri uri = params[0]; 578 579 Bitmap result = null; 580 try { 581 result = DocumentsContract.getDocumentThumbnail( 582 context.getContentResolver(), uri, mThumbSize, null); 583 if (result != null) { 584 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 585 context, mThumbSize); 586 thumbs.put(uri, result); 587 } 588 } catch (Exception e) { 589 Log.w(TAG, "Failed to load thumbnail: " + e); 590 } 591 return result; 592 } 593 594 @Override 595 protected void onPostExecute(Bitmap result) { 596 if (mTarget.getTag() == this) { 597 mTarget.setImageBitmap(result); 598 mTarget.setTag(null); 599 } 600 } 601 } 602 603 private static String formatTime(Context context, long when) { 604 // TODO: DateUtils should make this easier 605 Time then = new Time(); 606 then.set(when); 607 Time now = new Time(); 608 now.setToNow(); 609 610 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT 611 | DateUtils.FORMAT_ABBREV_ALL; 612 613 if (then.year != now.year) { 614 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 615 } else if (then.yearDay != now.yearDay) { 616 flags |= DateUtils.FORMAT_SHOW_DATE; 617 } else { 618 flags |= DateUtils.FORMAT_SHOW_TIME; 619 } 620 621 return DateUtils.formatDateTime(context, when, flags); 622 } 623 624 private String findCommonMimeType(List<String> mimeTypes) { 625 String[] commonType = mimeTypes.get(0).split("/"); 626 if (commonType.length != 2) { 627 return "*/*"; 628 } 629 630 for (int i = 1; i < mimeTypes.size(); i++) { 631 String[] type = mimeTypes.get(i).split("/"); 632 if (type.length != 2) continue; 633 634 if (!commonType[1].equals(type[1])) { 635 commonType[1] = "*"; 636 } 637 638 if (!commonType[0].equals(type[0])) { 639 commonType[0] = "*"; 640 commonType[1] = "*"; 641 break; 642 } 643 } 644 645 return commonType[0] + "/" + commonType[1]; 646 } 647} 648