DirectoryFragment.java revision 251097b3789632000ccdaf7fb7d66a82ff37d882
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 return new RecentLoader(context); 195 default: 196 throw new IllegalStateException("Unknown type " + mType); 197 198 } 199 } 200 201 @Override 202 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { 203 mAdapter.swapCursor(result.cursor); 204 } 205 206 @Override 207 public void onLoaderReset(Loader<DirectoryResult> loader) { 208 mAdapter.swapCursor(null); 209 } 210 }; 211 212 updateDisplayState(); 213 } 214 215 public void updateDisplayState() { 216 final State state = getDisplayState(this); 217 218 if (mLastSortOrder != state.sortOrder) { 219 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); 220 mLastSortOrder = state.sortOrder; 221 } 222 223 mListView.smoothScrollToPosition(0); 224 mGridView.smoothScrollToPosition(0); 225 226 mListView.setVisibility(state.mode == MODE_LIST ? View.VISIBLE : View.GONE); 227 mGridView.setVisibility(state.mode == MODE_GRID ? View.VISIBLE : View.GONE); 228 229 mFilter = new MimePredicate(state.acceptMimes); 230 231 final int choiceMode; 232 if (state.allowMultiple) { 233 choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; 234 } else { 235 choiceMode = ListView.CHOICE_MODE_NONE; 236 } 237 238 final int thumbSize; 239 if (state.mode == MODE_GRID) { 240 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); 241 mListView.setAdapter(null); 242 mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); 243 mGridView.setAdapter(mAdapter); 244 mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width)); 245 mGridView.setNumColumns(GridView.AUTO_FIT); 246 mGridView.setChoiceMode(choiceMode); 247 mCurrentView = mGridView; 248 } else if (state.mode == MODE_LIST) { 249 thumbSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); 250 mGridView.setAdapter(null); 251 mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE); 252 mListView.setAdapter(mAdapter); 253 mListView.setChoiceMode(choiceMode); 254 mCurrentView = mListView; 255 } else { 256 throw new IllegalStateException(); 257 } 258 259 mThumbSize = new Point(thumbSize, thumbSize); 260 } 261 262 private OnItemClickListener mItemListener = new OnItemClickListener() { 263 @Override 264 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 265 final Cursor cursor = mAdapter.getItem(position); 266 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 267 if (mFilter.apply(doc)) { 268 ((DocumentsActivity) getActivity()).onDocumentPicked(doc); 269 } 270 } 271 }; 272 273 private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() { 274 @Override 275 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 276 mode.getMenuInflater().inflate(R.menu.mode_directory, menu); 277 return true; 278 } 279 280 @Override 281 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 282 final State state = getDisplayState(DirectoryFragment.this); 283 284 final MenuItem open = menu.findItem(R.id.menu_open); 285 final MenuItem share = menu.findItem(R.id.menu_share); 286 final MenuItem delete = menu.findItem(R.id.menu_delete); 287 288 final boolean manageMode = state.action == ACTION_MANAGE; 289 open.setVisible(!manageMode); 290 share.setVisible(manageMode); 291 delete.setVisible(manageMode); 292 293 return true; 294 } 295 296 @Override 297 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 298 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); 299 final ArrayList<DocumentInfo> docs = Lists.newArrayList(); 300 final int size = checked.size(); 301 for (int i = 0; i < size; i++) { 302 if (checked.valueAt(i)) { 303 final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); 304 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); 305 docs.add(doc); 306 } 307 } 308 309 final int id = item.getItemId(); 310 if (id == R.id.menu_open) { 311 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); 312 mode.finish(); 313 return true; 314 315 } else if (id == R.id.menu_share) { 316 onShareDocuments(docs); 317 mode.finish(); 318 return true; 319 320 } else if (id == R.id.menu_delete) { 321 onDeleteDocuments(docs); 322 mode.finish(); 323 return true; 324 325 } else { 326 return false; 327 } 328 } 329 330 @Override 331 public void onDestroyActionMode(ActionMode mode) { 332 // ignored 333 } 334 335 @Override 336 public void onItemCheckedStateChanged( 337 ActionMode mode, int position, long id, boolean checked) { 338 if (checked) { 339 // Directories cannot be checked 340 final Cursor cursor = mAdapter.getItem(position); 341 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 342 if (Document.MIME_TYPE_DIR.equals(docMimeType)) { 343 mCurrentView.setItemChecked(position, false); 344 } 345 } 346 347 mode.setTitle(getResources() 348 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 349 } 350 }; 351 352 private void onShareDocuments(List<DocumentInfo> docs) { 353 Intent intent; 354 if (docs.size() == 1) { 355 final DocumentInfo doc = docs.get(0); 356 357 intent = new Intent(Intent.ACTION_SEND); 358 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 359 intent.addCategory(Intent.CATEGORY_DEFAULT); 360 intent.setType(doc.mimeType); 361 intent.putExtra(Intent.EXTRA_STREAM, doc.uri); 362 363 } else if (docs.size() > 1) { 364 intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 365 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 366 intent.addCategory(Intent.CATEGORY_DEFAULT); 367 368 final ArrayList<String> mimeTypes = Lists.newArrayList(); 369 final ArrayList<Uri> uris = Lists.newArrayList(); 370 for (DocumentInfo doc : docs) { 371 mimeTypes.add(doc.mimeType); 372 uris.add(doc.uri); 373 } 374 375 intent.setType(findCommonMimeType(mimeTypes)); 376 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 377 378 } else { 379 return; 380 } 381 382 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); 383 startActivity(intent); 384 } 385 386 private void onDeleteDocuments(List<DocumentInfo> docs) { 387 final Context context = getActivity(); 388 final ContentResolver resolver = context.getContentResolver(); 389 390 boolean hadTrouble = false; 391 for (DocumentInfo doc : docs) { 392 if (!doc.isDeleteSupported()) { 393 Log.w(TAG, "Skipping " + doc); 394 hadTrouble = true; 395 continue; 396 } 397 398 try { 399 if (resolver.delete(doc.uri, null, null) != 1) { 400 Log.w(TAG, "Failed to delete " + doc); 401 hadTrouble = true; 402 } 403 } catch (Exception e) { 404 Log.w(TAG, "Failed to delete " + doc + ": " + e); 405 hadTrouble = true; 406 } 407 } 408 409 if (hadTrouble) { 410 Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show(); 411 } 412 } 413 414 private static State getDisplayState(Fragment fragment) { 415 return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); 416 } 417 418 private class DocumentsAdapter extends BaseAdapter { 419 private Cursor mCursor; 420 421 public void swapCursor(Cursor cursor) { 422 mCursor = cursor; 423 424 if (isEmpty()) { 425 mEmptyView.setVisibility(View.VISIBLE); 426 } else { 427 mEmptyView.setVisibility(View.GONE); 428 } 429 430 notifyDataSetChanged(); 431 } 432 433 @Override 434 public View getView(int position, View convertView, ViewGroup parent) { 435 final Context context = parent.getContext(); 436 final State state = getDisplayState(DirectoryFragment.this); 437 438 final RootsCache roots = DocumentsApplication.getRootsCache(context); 439 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 440 context, mThumbSize); 441 442 if (convertView == null) { 443 final LayoutInflater inflater = LayoutInflater.from(context); 444 if (state.mode == MODE_LIST) { 445 convertView = inflater.inflate(R.layout.item_doc_list, parent, false); 446 } else if (state.mode == MODE_GRID) { 447 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); 448 } else { 449 throw new IllegalStateException(); 450 } 451 } 452 453 final Cursor cursor = getItem(position); 454 455 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); 456 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); 457 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); 458 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 459 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 460 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); 461 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); 462 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); 463 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); 464 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); 465 466 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 467 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 468 final View summaryGrid = convertView.findViewById(R.id.summary_grid); 469 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); 470 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 471 final TextView date = (TextView) convertView.findViewById(R.id.date); 472 final TextView size = (TextView) convertView.findViewById(R.id.size); 473 474 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) icon.getTag(); 475 if (oldTask != null) { 476 oldTask.cancel(false); 477 } 478 479 if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) { 480 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); 481 final Bitmap cachedResult = thumbs.get(uri); 482 if (cachedResult != null) { 483 icon.setImageBitmap(cachedResult); 484 } else { 485 final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize); 486 icon.setImageBitmap(null); 487 icon.setTag(task); 488 task.execute(uri); 489 } 490 } else if (docIcon != 0) { 491 icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon)); 492 } else { 493 icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType)); 494 } 495 496 title.setText(docDisplayName); 497 498 if (mType == TYPE_RECENT_OPEN) { 499 final RootInfo root = roots.getRoot(docAuthority, docRootId); 500 icon1.setVisibility(View.VISIBLE); 501 icon1.setImageDrawable(root.loadIcon(context)); 502 summary.setText(root.getDirectoryString()); 503 summary.setVisibility(View.VISIBLE); 504 } else { 505 icon1.setVisibility(View.GONE); 506 if (docSummary != null) { 507 summary.setText(docSummary); 508 summary.setVisibility(View.VISIBLE); 509 } else { 510 summary.setVisibility(View.INVISIBLE); 511 } 512 } 513 514 if (summaryGrid != null) { 515 summaryGrid.setVisibility( 516 (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE); 517 } 518 519 if (docLastModified == -1) { 520 date.setText(null); 521 } else { 522 date.setText(formatTime(context, docLastModified)); 523 } 524 525 if (state.showSize) { 526 size.setVisibility(View.VISIBLE); 527 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { 528 size.setText(null); 529 } else { 530 size.setText(Formatter.formatFileSize(context, docSize)); 531 } 532 } else { 533 size.setVisibility(View.GONE); 534 } 535 536 return convertView; 537 } 538 539 @Override 540 public int getCount() { 541 return mCursor != null ? mCursor.getCount() : 0; 542 } 543 544 @Override 545 public Cursor getItem(int position) { 546 if (mCursor != null) { 547 mCursor.moveToPosition(position); 548 } 549 return mCursor; 550 } 551 552 @Override 553 public long getItemId(int position) { 554 return position; 555 } 556 } 557 558 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> { 559 private final ImageView mTarget; 560 private final Point mThumbSize; 561 562 public ThumbnailAsyncTask(ImageView target, Point thumbSize) { 563 mTarget = target; 564 mThumbSize = thumbSize; 565 } 566 567 @Override 568 protected void onPreExecute() { 569 mTarget.setTag(this); 570 } 571 572 @Override 573 protected Bitmap doInBackground(Uri... params) { 574 final Context context = mTarget.getContext(); 575 final Uri uri = params[0]; 576 577 Bitmap result = null; 578 try { 579 result = DocumentsContract.getDocumentThumbnail( 580 context.getContentResolver(), uri, mThumbSize, null); 581 if (result != null) { 582 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( 583 context, mThumbSize); 584 thumbs.put(uri, result); 585 } 586 } catch (Exception e) { 587 Log.w(TAG, "Failed to load thumbnail: " + e); 588 } 589 return result; 590 } 591 592 @Override 593 protected void onPostExecute(Bitmap result) { 594 if (mTarget.getTag() == this) { 595 mTarget.setImageBitmap(result); 596 mTarget.setTag(null); 597 } 598 } 599 } 600 601 private static String formatTime(Context context, long when) { 602 // TODO: DateUtils should make this easier 603 Time then = new Time(); 604 then.set(when); 605 Time now = new Time(); 606 now.setToNow(); 607 608 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT 609 | DateUtils.FORMAT_ABBREV_ALL; 610 611 if (then.year != now.year) { 612 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 613 } else if (then.yearDay != now.yearDay) { 614 flags |= DateUtils.FORMAT_SHOW_DATE; 615 } else { 616 flags |= DateUtils.FORMAT_SHOW_TIME; 617 } 618 619 return DateUtils.formatDateTime(context, when, flags); 620 } 621 622 private String findCommonMimeType(List<String> mimeTypes) { 623 String[] commonType = mimeTypes.get(0).split("/"); 624 if (commonType.length != 2) { 625 return "*/*"; 626 } 627 628 for (int i = 1; i < mimeTypes.size(); i++) { 629 String[] type = mimeTypes.get(i).split("/"); 630 if (type.length != 2) continue; 631 632 if (!commonType[1].equals(type[1])) { 633 commonType[1] = "*"; 634 } 635 636 if (!commonType[0].equals(type[0])) { 637 commonType[0] = "*"; 638 commonType[1] = "*"; 639 break; 640 } 641 } 642 643 return commonType[0] + "/" + commonType[1]; 644 } 645} 646